From e880613f8ffda6888ef40c630805781affc728b9 Mon Sep 17 00:00:00 2001 From: walkpan Date: Tue, 7 Apr 2026 19:00:30 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=B8=BA=E6=B8=B8=E6=88=8F=E5=9C=B0?= =?UTF-8?q?=E5=9B=BE=E6=A8=A1=E5=9D=97=E6=B7=BB=E5=8A=A0=E8=AF=A6=E7=BB=86?= =?UTF-8?q?=E7=9A=84=E4=BB=A3=E7=A0=81=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为游戏地图模块的脚本文件添加全面的注释,说明每个组件的职责、关键设计、依赖关系和使用方式。注释覆盖了英雄信息面板、技能卡槽位管理器、排行榜弹窗、卡牌控制器、背景滚动组件等核心功能模块,提高了代码的可读性和维护性。 同时修复了英雄预制体的激活状态和技能效果预制体的尺寸参数。 --- assets/resources/game/heros/ha1.prefab | 2 +- .../resources/game/skill/buff/shielded.prefab | 6 +- assets/script/game/map/CardComp.ts | 280 ++++++++++++++++-- assets/script/game/map/CardController.ts | 56 +++- assets/script/game/map/GameMap.ts | 35 ++- assets/script/game/map/HInfoComp.ts | 95 +++++- assets/script/game/map/HlistComp.ts | 138 ++++++++- assets/script/game/map/IBoxComp.ts | 92 ++++++ assets/script/game/map/MissSkillsComp.ts | 79 ++++- assets/script/game/map/MissionCardComp.ts | 41 ++- assets/script/game/map/MissionComp.ts | 230 +++++++++++--- assets/script/game/map/MissionHeroComp.ts | 214 ++++++++++--- assets/script/game/map/MissionHomeComp.ts | 58 +++- assets/script/game/map/MissionMonComp.ts | 208 +++++++++++-- assets/script/game/map/RanksComp.ts | 38 ++- assets/script/game/map/RogueConfig.ts | 142 ++++++--- assets/script/game/map/SIconComp.ts | 32 +- assets/script/game/map/SkillBoxComp.ts | 123 +++++++- assets/script/game/map/TopComp.ts | 33 +++ assets/script/game/map/VictoryComp.ts | 100 +++++-- assets/script/game/map/move.ts | 80 ++++- 21 files changed, 1840 insertions(+), 242 deletions(-) diff --git a/assets/resources/game/heros/ha1.prefab b/assets/resources/game/heros/ha1.prefab index 6a9c9904..5c13ac2e 100644 --- a/assets/resources/game/heros/ha1.prefab +++ b/assets/resources/game/heros/ha1.prefab @@ -440,7 +440,7 @@ "propertyPath": [ "_active" ], - "value": false + "value": true }, { "__type__": "CCPropertyOverrideInfo", diff --git a/assets/resources/game/skill/buff/shielded.prefab b/assets/resources/game/skill/buff/shielded.prefab index 6c0ebbb5..341d8d2b 100644 --- a/assets/resources/game/skill/buff/shielded.prefab +++ b/assets/resources/game/skill/buff/shielded.prefab @@ -127,8 +127,8 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 110, - "height": 110 + "width": 90, + "height": 90 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -169,7 +169,7 @@ }, "_type": 1, "_fillType": 1, - "_sizeMode": 1, + "_sizeMode": 0, "_fillCenter": { "__type__": "cc.Vec2", "x": 0, diff --git a/assets/script/game/map/CardComp.ts b/assets/script/game/map/CardComp.ts index 16c4bf23..590101bb 100644 --- a/assets/script/game/map/CardComp.ts +++ b/assets/script/game/map/CardComp.ts @@ -1,3 +1,24 @@ +/** + * @file CardComp.ts + * @description 单张卡牌槽位组件(UI 视图层) + * + * 职责: + * 1. 管理单个卡牌槽位的 **显示** 和 **交互**(触摸拖拽 / 点击使用 / 锁定切换)。 + * 2. 接收来自 MissionCardComp 分发的 CardConfig 数据,渲染对应卡面(英雄 / 技能 / 特殊卡)。 + * 3. 在玩家触发"使用卡牌"操作时,执行 **费用扣除 → 动画表现 → 效果分发** 的完整流程。 + * + * 关键设计: + * - 槽位可 **锁定**:锁定后刷新卡池不会覆盖旧卡,由 `isLocked` 控制。 + * - 卡牌使用支持 **上划手势** 与 **点击** 两种触发方式。 + * - 英雄卡图标使用 AnimationClip 动态加载 idle 动画;技能 / 特殊卡使用 SpriteAtlas 静态图标。 + * - 通过 `iconVisualToken` 防止异步加载资源回调与当前显示不一致(竞态保护)。 + * + * 依赖: + * - CardConfig / CardType / CKind 等卡牌数据结构(CardSet) + * - HeroInfo(heroSet)、SkillSet(SkillSet)—— 用于渲染卡面信息 + * - GameEvent 事件总线 —— 分发 CallHero / UseSkillCard / UseSpecialCard 等效果 + * - smc.vmdata.mission_data —— 读写局内金币 + */ import { mLogger } from "../common/Logger"; import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, resources, Light } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; @@ -14,55 +35,99 @@ import { UIID } from "../common/config/GameUIConfig"; const { ccclass, property } = _decorator; -/** 视图层对象 */ +/** + * CardComp —— 单张卡牌槽位视图组件 + * + * 挂载在每个卡牌槽位节点上,由 MissionCardComp 统一管理 4 个实例。 + * 负责单卡的渲染、交互、动画以及使用逻辑。 + */ @ccclass('CardComp') @ecs.register('CardComp', false) export class CardComp extends CCComp { + /** 是否开启调试日志 */ private debugMode: boolean = true; + + // ======================== 编辑器绑定节点 ======================== + /** 锁定态图标节点(显示时表示本槽位锁定) */ @property(Node) Lock: Node = null! + /** 解锁态图标节点(显示时表示本槽位未锁定,可点击上锁) */ @property(Node) unLock: Node = null! + /** 英雄卡信息面板(显示 AP / HP) */ @property(Node) info_node=null! + /** 非英雄卡信息面板(显示技能 / 特殊卡描述文本) */ @property(Node) oinfo_node=null! + /** 卡牌名称标签节点 */ @property(Node) name_node=null! + /** 卡牌图标节点(英雄动画 / 技能图标) */ @property(Node) icon_node=null! + /** 费用显示节点 */ @property(Node) cost_node=null! + /** 卡牌种类标识节点(如近战 / 远程 / 辅助等分类子节点的容器) */ @property(Node) Ckind_node=null! + /** 卡牌背景底框节点(按卡池等级切换子节点显示) */ @property(Node) BG_node=null! + /** 普通品质边框节点(hero_lv / card_lv <= 1 时使用) */ @property(Node) NF_node=null! + /** 高品质边框节点(hero_lv / card_lv > 1 时使用) */ @property(Node) HF_node=null! - + + // ======================== 运行时状态 ======================== + + /** 当前卡牌的金币费用 */ card_cost:number=0 + /** 当前卡牌类型(英雄 / 技能 / 特殊升级 / 特殊刷新) */ card_type:CardType=CardType.Hero + /** 当前卡牌的唯一标识 UUID */ card_uuid:number=0 /** 是否处于锁定状态(锁定且有卡时,抽卡分发会被跳过) */ private isLocked: boolean = false; - /** 图标图集缓存(后续接图标资源时直接复用) */ + /** 图标图集缓存(首次加载后复用,避免重复 IO) */ private uiconsAtlas: SpriteAtlas | null = null; /** 当前槽位承载的卡牌数据,null 表示空槽 */ private cardData: CardConfig | null = null; + /** 上划使用阈值(像素):拖拽距离 >= 此值视为"使用卡牌" */ private readonly dragUseThreshold: number = 70; + /** 触摸起始 Y 坐标,用于计算拖拽距离 */ private touchStartY: number = 0; + /** 当前是否正在拖拽 */ private isDragging: boolean = false; + /** 当前是否正在执行"使用"流程(防止重复触发) */ private isUsing: boolean = false; + /** 卡牌的静止位置(未拖拽时应在此位置) */ private restPosition: Vec3 = new Vec3(); + /** 是否已缓存基准 Y/Z 坐标(首次 setSlotPosition 时确定) */ private hasFixedBasePosition: boolean = false; + /** 基准 Y 坐标(由场景布局决定) */ private fixedBaseY: number = 0; + /** 基准 Z 坐标 */ private fixedBaseZ: number = 0; + /** UIOpacity 组件引用,用于使用消失动画中的渐隐 */ private opacityComp: UIOpacity | null = null; + /** + * 图标视觉令牌:每次更新图标时 +1, + * 异步加载回调通过比对 token 判断是否仍为当前可见内容, + * 防止快速切卡时旧回调错误覆盖新图标。 + */ private iconVisualToken: number = 0; + // ======================== 生命周期 ======================== + + /** + * 组件加载时:绑定交互事件,初始化基础 UI 状态。 + * 此阶段不触发业务逻辑。 + */ onLoad() { /** 初始阶段只做UI状态准备,不触发业务逻辑 */ this.bindEvents(); @@ -74,36 +139,53 @@ export class CardComp extends CCComp { } + /** 组件销毁时解绑所有事件,防止残留回调 */ onDestroy() { this.unbindEvents(); } + + /** 外部初始化入口(由 MissionCardComp 调用) */ init(){ this.onMissionStart(); } - /** 游戏开始初始化 */ + /** 游戏开始初始化(预留扩展) */ onMissionStart() { } - /** 游戏结束清理 */ + /** 游戏结束清理(预留扩展) */ onMissionEnd() { } + + /** 节点启动时确保可见 */ start() { /** 单卡节点常驻,由数据控制显示内容 */ this.node.active = true; } - /** 兼容旧接口:外部通过该入口更新卡牌 */ + // ======================== 对外接口 ======================== + + /** + * 兼容旧接口:外部通过该入口更新卡牌 + * @param card 卡牌节点引用(历史遗留参数,当前未使用) + * @param data 卡牌配置数据 + */ updateCardInfo(card:Node, data: CardConfig){ this.applyDrawCard(data); } + /** + * 更新卡牌图标:先尝试从缓存图集获取,未缓存则异步加载图集后获取。 + * @param node 图标所在节点 + * @param iconId 图标在 SpriteAtlas 中的帧名称 + */ private updateIcon(node: Node, iconId: string) { if (!node || !iconId) return; const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite); if (!sprite) return; + // 已缓存图集 → 直接获取帧 if (this.uiconsAtlas) { const frame = this.uiconsAtlas.getSpriteFrame(iconId); if (frame) { @@ -113,6 +195,7 @@ export class CardComp extends CCComp { } return; } + // 首次加载图集 resources.load("gui/uicons", SpriteAtlas, (err, atlas) => { if (err || !atlas) { mLogger.log(this.debugMode, "CardComp", "load uicons atlas failed", err); @@ -128,25 +211,42 @@ export class CardComp extends CCComp { }); } - /** 兼容旧接口:按索引更新卡牌(当前由 MissionCardComp 顺序分发) */ + /** + * 兼容旧接口:按索引更新卡牌(当前由 MissionCardComp 顺序分发) + * @param index 槽位索引(0~3) + * @param data 卡牌配置数据 + */ updateCardData(index: number, data: CardConfig) { this.applyDrawCard(data); } - /** 兼容按钮回调入口:触发单卡使用 */ + /** + * 兼容按钮回调入口:触发单卡使用 + * @param e 事件对象 + * @param index 索引字符串(历史遗留参数) + */ selectCard(e: any, index: string) { this.useCard(); } - /** - * 关闭界面 - */ + /** 关闭界面(预留) */ close() { } - /** 抽卡分发入口:返回 true 表示本次已成功接收新卡 */ + // ======================== 核心业务方法 ======================== + + /** + * 抽卡分发入口:由 MissionCardComp 调用以向本槽位放入新卡。 + * + * 流程: + * 1. 若本槽位已锁定且已有卡 → 跳过,返回 false。 + * 2. 更新 cardData 及派生字段 → 刷新 UI → 播放入场动画。 + * + * @param data 要放入的卡牌数据,null 表示无卡可放 + * @returns true = 成功接收;false = 跳过(锁定 / 无数据) + */ applyDrawCard(data: CardConfig | null): boolean { if (!data) return false; /** 锁定且已有旧卡时,跳过本次刷新,保持老卡 */ @@ -169,10 +269,24 @@ export class CardComp extends CCComp { return true; } + /** + * 使用当前卡牌的核心逻辑。 + * + * 完整流程: + * 1. 前置校验:是否有卡、是否正在使用中。 + * 2. 金币检查:不够则 toast 提示 + 回弹动画。 + * 3. 英雄卡额外校验:通过 GameEvent.UseHeroCard 事件向 MissionCardComp 询问 + * 英雄上限是否允许(guard 模式:外部可设 cancel=true 阻止使用)。 + * 4. 扣除金币,同步 CoinAdd 事件。 + * 5. 播放消失动画 → 动画结束后清空槽位并分发卡牌效果。 + * + * @returns 被使用的卡牌数据,若未成功使用则返回 null + */ useCard(): CardConfig | null { if (!this.cardData || this.isUsing) return null; const cardCost = Math.max(0, Math.floor(this.cardData.cost ?? 0)); const currentCoin = this.getMissionCoin(); + // 金币不足 → 提示并回弹 if (currentCoin < cardCost) { oops.gui.toast(`金币不足,召唤需要${cardCost}`); this.playReboundAnim(); @@ -184,6 +298,7 @@ export class CardComp extends CCComp { }); return null; } + // 英雄卡特殊校验:通过 guard 对象实现"可取消"模式 if (this.cardData.type === CardType.Hero) { const guard = { cancel: false, @@ -198,11 +313,13 @@ export class CardComp extends CCComp { return null; } } + // 扣除金币 this.setMissionCoin(currentCoin - cardCost); oops.message.dispatchEvent(GameEvent.CoinAdd, { syncOnly: true, delta: -cardCost }); + // 标记使用中,阻止并发操作 this.isUsing = true; const used = this.cardData; mLogger.log(this.debugMode, "CardComp", "use card", { @@ -211,6 +328,7 @@ export class CardComp extends CCComp { cost: cardCost, leftCoin: this.getMissionCoin() }); + // 播放消失动画 → 动画结束后清槽并分发效果 this.playUseDisappearAnim(() => { this.clearAfterUse(); this.isUsing = false; @@ -219,6 +337,14 @@ export class CardComp extends CCComp { return used; } + /** + * 根据卡牌类型分发对应的游戏效果事件。 + * - 英雄卡 → CallHero + * - 技能卡 → UseSkillCard + * - 特殊升级 / 特殊刷新 → UseSpecialCard + * + * @param payload 被使用的卡牌数据 + */ private executeCardEffectEntry(payload: CardConfig) { switch (payload.type) { case CardType.Hero: @@ -239,7 +365,10 @@ export class CardComp extends CCComp { return !!this.cardData; } - /** 外部设置锁定态 */ + /** + * 外部设置锁定态 + * @param value true=锁定(刷新时保留旧卡),false=解锁 + */ setLocked(value: boolean) { this.isLocked = value; this.updateLockUI(); @@ -250,6 +379,11 @@ export class CardComp extends CCComp { return this.isLocked; } + /** + * 设置槽位的水平位置(由 MissionCardComp 根据槽位数量计算布局后调用)。 + * 首次调用时会记录基准 Y/Z,后续只更新 X。 + * @param x 目标水平坐标 + */ setSlotPosition(x: number) { const current = this.node.position; if (!this.hasFixedBasePosition) { @@ -258,12 +392,16 @@ export class CardComp extends CCComp { this.hasFixedBasePosition = true; } this.restPosition = new Vec3(x, this.fixedBaseY, this.fixedBaseZ); + // 拖拽/使用中不立即移动,等状态结束后归位 if (!this.isDragging && !this.isUsing) { this.node.setPosition(this.restPosition); } } - /** 系统清槽:用于任务开始/结束等强制重置场景 */ + /** + * 系统清槽:用于任务开始/结束等强制重置场景。 + * 停止所有动画 → 重置全部状态 → 清空显示 → 隐藏节点。 + */ clearBySystem() { Tween.stopAllByTarget(this.node); if (this.opacityComp) { @@ -284,7 +422,12 @@ export class CardComp extends CCComp { this.node.active = false; } - /** 卡牌被玩家使用后的清槽行为 */ + // ======================== 内部清理 ======================== + + /** + * 卡牌被玩家使用后的清槽行为。 + * 与 clearBySystem 类似,但不重置锁定态。 + */ private clearAfterUse() { Tween.stopAllByTarget(this.node); if (this.opacityComp) { @@ -304,7 +447,9 @@ export class CardComp extends CCComp { this.node.active = false; } - /** 绑定触控:卡面点击使用,锁按钮点击切换锁定 */ + // ======================== 事件绑定 ======================== + + /** 绑定触控事件:卡面点击/ 拖拽使用,锁按钮点击切换锁定 */ private bindEvents() { this.node.on(NodeEventType.TOUCH_START, this.onCardTouchStart, this); this.node.on(NodeEventType.TOUCH_MOVE, this.onCardTouchMove, this); @@ -324,12 +469,18 @@ export class CardComp extends CCComp { this.unLock?.off(NodeEventType.TOUCH_END, this.onToggleLock, this); } + // ======================== 触摸交互 ======================== + + /** 触摸开始:记录起点 Y,进入拖拽状态 */ private onCardTouchStart(event: EventTouch) { if (!this.cardData || this.isUsing) return; this.touchStartY = event.getUILocation().y; this.isDragging = true; } + /** + * 触摸移动:跟随手指向上偏移卡牌(仅允许向上拖拽,deltaY < 0 被 clamp 为 0) + */ private onCardTouchMove(event: EventTouch) { if (!this.isDragging || !this.cardData || this.isUsing) return; const currentY = event.getUILocation().y; @@ -337,7 +488,11 @@ export class CardComp extends CCComp { this.node.setPosition(this.restPosition.x, this.restPosition.y + deltaY, this.restPosition.z); } - /** 上拉超过阈值才视为使用,否则回弹原位 */ + /** + * 触摸结束: + * - 上拉距离 >= dragUseThreshold → 视为"使用卡牌" + * - 否则视为"点击",打开英雄信息弹窗(仅英雄卡)并回弹 + */ private onCardTouchEnd(event: EventTouch) { if (!this.isDragging || !this.cardData || this.isUsing) return; const endY = event.getUILocation().y; @@ -351,13 +506,17 @@ export class CardComp extends CCComp { this.playReboundAnim(); } + /** 触摸取消:回弹至原位 */ private onCardTouchCancel() { if (!this.isDragging || this.isUsing) return; this.isDragging = false; this.playReboundAnim(); } - /** 点击锁控件:切换锁态;空槽不允许锁定 */ + /** + * 点击锁控件:切换锁态(空槽不允许锁定)。 + * 阻止事件冒泡,避免触发卡面的点击使用。 + */ private onToggleLock(event?: EventTouch) { if (!this.cardData) return; this.isLocked = !this.isLocked; @@ -372,22 +531,38 @@ export class CardComp extends CCComp { } } - /** 根据锁态刷新 Lock / unLock 显示(Lock=可点击上锁,unLock=可点击解锁) */ + // ======================== UI 渲染 ======================== + + /** + * 根据锁态刷新 Lock / unLock 节点显示。 + * 当前功能已注释(锁定 UI 暂未启用),保留接口以备后续启用。 + */ private updateLockUI() { // if (this.Lock) this.Lock.active = !this.isLocked; // if (this.unLock) this.unLock.active = this.isLocked; } - /** 根据当前 cardData 渲染卡面文字与图标 */ + /** + * 根据当前 cardData 渲染卡面文字与图标。 + * + * 渲染逻辑根据卡牌类型分三路: + * 1. 英雄卡:显示英雄名(带星级后缀)、AP / HP 数值、idle 动画。 + * 2. 技能卡:显示技能名(带品质后缀)、描述文本、静态图标。 + * 3. 特殊卡:显示卡名(带品质后缀)、描述文本、静态图标。 + * + * 同时根据 pool_lv 切换背景底框,根据 hero_lv / card_lv 切换普通/高级边框。 + */ private applyCardUI() { if (!this.cardData) { this.applyEmptyUI(); return; } + // 递增视觉令牌,用于异步加载竞态保护 this.iconVisualToken += 1; if (this.opacityComp) this.opacityComp.opacity = 255; this.node.setPosition(this.restPosition); + // ---- 卡牌种类标识(近战 / 远程 / 辅助等) ---- if (this.Ckind_node) { const kindName = CKind[this.cardData.kind]; this.Ckind_node.children.forEach(child => { @@ -395,6 +570,7 @@ export class CardComp extends CCComp { }); } + // ---- 背景底框(按卡池等级显示对应子节点) ---- const cardLvStr = `lv${this.cardData.pool_lv}`; if (this.BG_node) { this.BG_node.children.forEach(child => { @@ -402,6 +578,7 @@ export class CardComp extends CCComp { }); } + // ---- 品质边框(高级 vs 普通) ---- const card_lv_val = this.cardData.card_lv ?? 1; const isHighLevel = (this.cardData.hero_lv ?? 0) > 1 || card_lv_val > 1; if (this.HF_node) this.HF_node.active = isHighLevel; @@ -415,7 +592,9 @@ export class CardComp extends CCComp { if(isHighLevel){activeFrameNode.getChildByName("light").active=true} } + // ---- 按卡牌类型渲染具体内容 ---- if(this.card_type===CardType.Hero){ + // 英雄卡:显示英雄名 + 星级 + AP/HP const hero = HeroInfo[this.card_uuid]; const heroLv = Math.max(1, Math.floor(this.cardData.hero_lv ?? hero?.lv ?? 1)); const suffix = heroLv >= 2 ? "★".repeat(heroLv - 1) : ""; @@ -425,6 +604,7 @@ export class CardComp extends CCComp { this.info_node.getChildByName("ap").getChildByName("val").getComponent(Label).string = `${(hero?.ap ?? 0) * heroLv}`; this.info_node.getChildByName("hp").getChildByName("val").getComponent(Label).string = `${(hero?.hp ?? 0) * heroLv}`; }else if(this.card_type===CardType.Skill){ + // 技能卡:显示技能名 + 品质后缀 + 描述 const skill = SkillSet[this.card_uuid]; const skillCard = CardPoolList.find(c => c.uuid === this.card_uuid); const card_lv = Math.max(1, Math.floor(this.cardData.card_lv ?? 1)); @@ -434,6 +614,7 @@ export class CardComp extends CCComp { this.oinfo_node.active = true; this.oinfo_node.getChildByName("info").getComponent(Label).string = `${skillCard?.info || skill?.info || ""}`; }else{ + // 特殊卡(升级 / 刷新):显示卡名 + 品质后缀 + 描述 const specialCard = this.card_type === CardType.SpecialUpgrade ? SpecialUpgradeCardList[this.card_uuid] : SpecialRefreshCardList[this.card_uuid]; @@ -445,13 +626,17 @@ export class CardComp extends CCComp { this.oinfo_node.getChildByName("info").getComponent(Label).string = `${specialCard?.info || ""}`; } + // ---- 费用标签 ---- this.setLabel(this.cost_node, `${this.card_cost}`); + // ---- 图标 ---- const iconNode = this.icon_node as Node; if (this.card_type === CardType.Hero) { + // 英雄卡使用 AnimationClip,加载 idle 动画 this.updateHeroAnimation(iconNode, this.card_uuid, this.iconVisualToken); return; } + // 非英雄卡使用静态图标 this.clearIconAnimation(iconNode); const iconId = this.resolveCardIconId(this.card_type, this.card_uuid); if (iconId) { @@ -462,6 +647,9 @@ export class CardComp extends CCComp { } } + // ======================== 动画表现 ======================== + + /** 卡牌入场动画:先缩小再弹大再回归正常比例 */ private playRefreshAnim() { Tween.stopAllByTarget(this.node); this.node.setPosition(this.restPosition); @@ -472,6 +660,7 @@ export class CardComp extends CCComp { .start(); } + /** 回弹动画:从当前位置平滑回到静止位并恢复缩放 */ private playReboundAnim() { Tween.stopAllByTarget(this.node); tween(this.node) @@ -482,6 +671,14 @@ export class CardComp extends CCComp { .start(); } + /** + * 使用消失动画: + * - 节点向上移动 120px 并缩小至 0.8 + * - 同时 UIOpacity 渐隐至 0 + * - 动画完成后调用 onComplete 回调 + * + * @param onComplete 动画完成后的回调 + */ private playUseDisappearAnim(onComplete: () => void) { const targetPos = new Vec3(this.restPosition.x, this.restPosition.y + 120, this.restPosition.z); Tween.stopAllByTarget(this.node); @@ -501,7 +698,10 @@ export class CardComp extends CCComp { .start(); } - /** 渲染空槽状态 */ + /** + * 渲染空槽状态: + * 清空名称、费用、信息面板、种类标识、背景底框、边框、图标。 + */ private applyEmptyUI() { this.iconVisualToken += 1; this.setLabel(this.name_node, ""); @@ -523,7 +723,13 @@ export class CardComp extends CCComp { if (sprite) sprite.spriteFrame = null; } - /** 安全设置文本,兼容节点上或子节点上的 Label */ + // ======================== 工具方法 ======================== + + /** + * 安全设置文本,兼容节点上或子节点上的 Label + * @param node 标签所在节点 + * @param value 要设置的文本 + */ private setLabel(node: Node | null, value: string) { if (!node) return; const label = node.getComponent(Label) || node.getComponentInChildren(Label); @@ -531,6 +737,12 @@ export class CardComp extends CCComp { } + /** + * 根据卡牌类型和 UUID 解析出图标 ID(在 SpriteAtlas 中的帧名)。 + * @param type 卡牌类型 + * @param uuid 卡牌 UUID + * @returns 图标帧名称 + */ private resolveCardIconId(type: CardType, uuid: number): string { if (type === CardType.Skill) { return SkillSet[uuid]?.icon || `${uuid}`; @@ -541,6 +753,10 @@ export class CardComp extends CCComp { return `${uuid}`; } + /** + * 打开英雄信息弹窗(IBox)。 + * 仅当当前卡为英雄卡且 HeroInfo 有效时生效。 + */ private openHeroInfoIBox() { if (!this.cardData) return; if (this.cardData.type !== CardType.Hero) return; @@ -554,6 +770,14 @@ export class CardComp extends CCComp { }); } + /** + * 为英雄卡图标加载并播放 idle 动画。 + * 使用 token 做竞态保护,确保异步回调时不会覆盖已更新的图标。 + * + * @param node 图标节点 + * @param uuid 英雄 UUID + * @param token 当前视觉令牌 + */ private updateHeroAnimation(node: Node, uuid: number, token: number) { const sprite = node?.getComponent(Sprite) || node?.getComponentInChildren(Sprite); if (sprite) sprite.spriteFrame = null; @@ -567,6 +791,7 @@ export class CardComp extends CCComp { mLogger.log(this.debugMode, "CardComp", `load hero animation failed ${uuid}`, err); return; } + // 竞态保护:若加载期间卡面已变更则丢弃 if (token !== this.iconVisualToken || !this.cardData || this.card_type !== CardType.Hero || this.card_uuid !== uuid) { return; } @@ -576,6 +801,7 @@ export class CardComp extends CCComp { }); } + /** 清除图标节点上的动画(停止播放并移除所有 clip) */ private clearIconAnimation(node: Node) { const anim = node?.getComponent(Animation); if (!anim) return; @@ -583,24 +809,32 @@ export class CardComp extends CCComp { this.clearAnimationClips(anim); } + /** + * 移除 Animation 组件上的全部 AnimationClip。 + * 通过浅拷贝数组避免遍历时修改集合。 + */ private clearAnimationClips(anim: Animation) { const clips = (anim as any).clips as AnimationClip[] | undefined; if (!clips || clips.length === 0) return; [...clips].forEach(clip => anim.removeClip(clip, true)); } + // ======================== 数据访问 ======================== + + /** 从全局单例获取当前局内金币数量 */ private getMissionCoin(): number { const missionData = smc?.vmdata?.mission_data; return Math.max(0, Math.floor(missionData?.coin ?? 0)); } + /** 设置当前局内金币数量(自动向下取整并 clamp 至 >= 0) */ private setMissionCoin(value: number) { const missionData = smc?.vmdata?.mission_data; if (!missionData) return; missionData.coin = Math.max(0, Math.floor(value)); } - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ + /** ECS 组件移除时的释放钩子:销毁节点 */ reset() { this.node.destroy(); } diff --git a/assets/script/game/map/CardController.ts b/assets/script/game/map/CardController.ts index 6b846ac1..c2f8cd76 100644 --- a/assets/script/game/map/CardController.ts +++ b/assets/script/game/map/CardController.ts @@ -1,3 +1,21 @@ +/** + * @file CardController.ts + * @description 卡牌控制器组件(UI 视图层) + * + * 职责: + * 1. 作为卡牌系统的顶层控制器容器节点对应的脚本。 + * 2. 管理 "任务主页" 与 "任务中" 两个子界面的激活切换。 + * 3. 在 ECS 实体挂载后隐藏 loading 遮罩,标志地图加载完毕。 + * + * 设计说明: + * - 本组件挂载在 CardController 预制体根节点上, + * 子节点 `mission_home` 与 `mission` 分别对应主页和战斗界面。 + * - update 中检查全局暂停 / 结束标志,预留帧逻辑扩展点。 + * + * 依赖: + * - smc.map.MapView.scene.mapLayer —— 获取地图层以隐藏 loading 节点 + * - smc.vmdata —— 读取全局游戏状态(game_over / game_pause) + */ import { _decorator,Button,EventHandler,EventTouch,Label,NodeEventType,resources,Sprite,SpriteAtlas,tween,UITransform,v3 } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; @@ -5,40 +23,74 @@ import { smc } from "../common/SingletonModuleComp"; import { mLogger } from "../common/Logger"; const { ccclass, property } = _decorator; -/** 视图层对象 */ +/** + * CardControllerComp —— 卡牌系统控制器视图组件 + * + * 负责初始化卡牌界面的顶层布局切换(主页 vs 战斗), + * 并在 ECS 实体加入场景时处理 loading 状态。 + */ @ccclass('CardControllerComp') @ecs.register('CardController', false) export class CardControllerComp extends CCComp { + /** 是否启用调试日志 */ @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = false; + /** 触控计时器(预留,可用于长按等交互逻辑) */ touch_time:number = 0 + /** 是否正在触控中(预留) */ in_touch:boolean = false + /** 底部背景引用(预留) */ bbg:any=null + /** 底部背景 Y 坐标(预留) */ bbg_y:number=40 + /** 卡牌槽位 X 坐标数组(5 个位置,预留) */ bbg_x:any=[-300,-150,0,150,300] + protected onLoad(): void { } + + /** + * start:组件启动时打印日志并初始化页面。 + * 默认显示任务主页(mission_home),隐藏战斗界面(mission)。 + */ start() { mLogger.log(this.debugMode, 'CardController', "CardControllerComp start",this.node) this.page_init() } + + /** + * onAdded:当本组件对应的 ECS 实体被挂载到场景后触发。 + * 主要作用:关闭地图层上的 loading 遮罩。 + * @param args ECS 实体附加参数 + */ onAdded(args:any){ mLogger.log(this.debugMode, 'CardController', "CardControllerComp onAdded",args) smc.map.MapView.scene.mapLayer.node.getChildByName("loading").active=false; } + + /** + * update:每帧更新。 + * 若全局标记 game_over 或 game_pause 时直接跳过(预留扩展位)。 + */ protected update(dt: number): void { if(smc.vmdata.game_over||smc.vmdata.game_pause){ return } } + + /** + * 页面初始化: + * - 显示任务主页(mission_home) + * - 隐藏任务战斗界面(mission) + */ page_init(){ this.node.getChildByName("mission_home").active=true; this.node.getChildByName("mission").active=false; } - /** 视图对象通过 ecs.Entity.remove(ControllerComp) 删除组件是触发组件处理自定义释放逻辑 */ + /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy(); } diff --git a/assets/script/game/map/GameMap.ts b/assets/script/game/map/GameMap.ts index 93788a4c..c876ad83 100644 --- a/assets/script/game/map/GameMap.ts +++ b/assets/script/game/map/GameMap.ts @@ -1,3 +1,23 @@ +/** + * @file GameMap.ts + * @description 游戏地图 ECS 实体 + * + * 职责: + * 1. 作为地图模块的 **ECS 根实体**,聚合 MapModelComp(数据层)与 MapViewComp(视图层)。 + * 2. 在 init 阶段仅注册 MapModelComp(纯数据,不依赖节点)。 + * 3. 显式调用 load() 时,异步加载地图预制(Prefab),实例化后将 MapViewComp 挂载到实体上。 + * + * 设计说明: + * - 视图层的挂载延后到 load() 完成后,确保 Prefab 加载成功且节点树就绪。 + * - 实例化的地图节点被添加至 oops.game.root,作为场景显示层的子节点。 + * - MapViewComp 从预制体中名为 "map" 的子节点上获取。 + * + * 依赖: + * - MapModelComp(model/MapModelComp.ts)—— 存放地图资源路径等数据配置 + * - MapViewComp(view/MapViewComp.ts)—— 地图视图逻辑 + * - oops.res —— 资源加载 + * - oops.game.root —— 全局显示根节点 + */ /* * @Author: dgflash * @Date: 2022-02-12 11:02:21 @@ -10,18 +30,29 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec import { MapModelComp } from "./model/MapModelComp"; import { MapViewComp } from "./view/MapViewComp"; -/** 游戏地图 */ +/** 游戏地图 ECS 实体 */ @ecs.register(`GameMap`) export class GameMap extends ecs.Entity { + /** 地图数据模型组件(通过 ECS 自动注入) */ MapModel!: MapModelComp; + /** 地图视图组件(通过 ECS 自动注入,load() 完成后可用) */ MapView!: MapViewComp; + /** + * ECS 实体初始化:注册数据层组件。 + * 视图层在 load() 中异步挂载。 + */ protected init(): void { this.addComponents( MapModelComp); } - /** 加载地图显示资源 */ + /** + * 加载地图显示资源: + * 1. 通过 MapModel.resPrefab 路径加载 Prefab。 + * 2. 实例化预制体并挂载到全局根节点。 + * 3. 从预制体中提取 MapViewComp 并通过 this.add() 注册到实体。 + */ load() { oops.res.load(this.MapModel.resPrefab, Prefab, (err: Error | null, res: Prefab) => { if (err) { diff --git a/assets/script/game/map/HInfoComp.ts b/assets/script/game/map/HInfoComp.ts index 562d622b..e3cb4cb9 100644 --- a/assets/script/game/map/HInfoComp.ts +++ b/assets/script/game/map/HInfoComp.ts @@ -1,3 +1,24 @@ +/** + * @file HInfoComp.ts + * @description 场上英雄信息卡片组件(UI 视图层) + * + * 职责: + * 1. 显示单个场上英雄的实时属性信息(AP / HP / 边框品质 / idle 动画)。 + * 2. 由 MissionCardComp.ensureHeroInfoPanel() 在英雄上场时实例化并绑定数据。 + * 3. 支持点击打开英雄详情弹窗(IBox)、出售英雄等交互(当前已注释)。 + * + * 关键设计: + * - 通过 bindData(eid, model) 将组件与某个英雄 ECS 实体绑定。 + * - refresh() 被 MissionCardComp 定时调用以同步实时属性变化。 + * - iconVisualToken 机制与 CardComp 一致,用于异步动画加载的竞态保护。 + * - isModelAlive() 检测绑定的英雄实体是否仍存活(ECS ent 引用是否有效)。 + * + * 依赖: + * - HeroAttrsComp —— 英雄属性数据模型 + * - HeroInfo(heroSet)—— 英雄静态配置 + * - Hero —— 英雄 ECS 实体类(用于出售删除) + * - UIID.IBox —— 英雄详情弹窗 ID + */ import { _decorator, Animation, AnimationClip, Button, Event, Label, Node, NodeEventType, Sprite, resources } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; @@ -10,37 +31,55 @@ import { mLogger } from "../common/Logger"; const {property, ccclass } = _decorator; -/** 视图层对象 */ +/** + * HInfoComp —— 场上英雄信息面板视图组件 + * + * 每个实例对应一个出战英雄,在卡牌面板下方横向排列。 + * 实时显示英雄的 AP / HP 和 idle 动画图标。 + */ @ccclass('HInfoComp') @ecs.register('HInfoComp', false) export class HInfoComp extends CCComp { + /** 英雄 idle 动画图标节点 */ @property(Node) icon_node=null! + /** 出售按钮节点(预留,当前交互已注释) */ @property(Node) sell_node=null! - + /** 普通品质边框 */ @property(Node) NF_node=null! + /** 高品质边框 */ @property(Node) HF_node=null! - + /** 绑定的英雄 ECS 实体 ID */ private eid: number = 0; + /** 绑定的英雄属性数据模型引用 */ private model: HeroAttrsComp | null = null; + /** AP 标签缓存引用 */ private apLabel: Label | null = null; + /** HP 标签缓存引用 */ private hpLabel: Label | null = null; + /** 图标视觉令牌(异步加载竞态保护) */ private iconVisualToken: number = 0; + /** 当前显示的英雄 UUID(避免相同 UUID 重复加载动画) */ private iconHeroUuid: number = 0; + /** 调试日志开关 */ private debugMode: boolean = true; + onLoad() { this.cacheLabels(); - // this.bindEvents(); } onDestroy() { - // this.unbindEvents(); } + /** + * 绑定英雄数据:关联实体 ID 和属性模型,并立即刷新显示。 + * @param eid 英雄 ECS 实体 ID + * @param model 英雄属性组件引用 + */ bindData(eid: number, model: HeroAttrsComp) { this.eid = eid; this.model = model; @@ -48,13 +87,21 @@ export class HInfoComp extends CCComp { this.refresh(); } + /** + * 刷新显示: + * 1. 根据英雄等级切换高级 / 普通边框。 + * 2. 若英雄 UUID 发生变化,重新加载 idle 动画。 + * 3. 更新 AP / HP 数值标签。 + */ refresh() { if (!this.model) return; + // ---- 品质边框切换 ---- const isHighLevel = (this.model.lv ?? 0) > 1; if (this.HF_node) this.HF_node.active = isHighLevel; if (this.NF_node) this.NF_node.active = !isHighLevel; + // 按卡池等级显示对应子节点 const activeFrameNode = isHighLevel ? this.HF_node : this.NF_node; if (activeFrameNode) { const cardLvStr = `lv${this.model.pool_lv ?? 1}`; @@ -63,12 +110,15 @@ export class HInfoComp extends CCComp { }); } + // ---- 图标动画(仅在 UUID 变化时重新加载) ---- const heroUuid = this.model.hero_uuid ?? 0; if (heroUuid !== this.iconHeroUuid) { this.iconHeroUuid = heroUuid; this.iconVisualToken += 1; this.updateHeroAnimation(this.icon_node, heroUuid, this.iconVisualToken); } + + // ---- 数值标签 ---- if (this.apLabel) { this.apLabel.string = `${Math.max(0, Math.floor(this.model.ap ?? 0))}`; } @@ -77,10 +127,17 @@ export class HInfoComp extends CCComp { } } + /** + * 检测绑定的英雄实体是否仍存活。 + * 通过检查 model 上的 ent 引用判断 ECS 实体是否已被回收。 + */ isModelAlive(): boolean { return !!(this.model as any)?.ent; } + // ======================== 内部工具方法 ======================== + + /** 缓存 AP / HP Label 引用,避免每次刷新都遍历节点树 */ private cacheLabels() { if (!this.apLabel) { this.apLabel = this.findLabelByPath(["ap", "val"]); @@ -90,6 +147,11 @@ export class HInfoComp extends CCComp { } } + /** + * 按节点路径查找 Label 组件 + * @param path 从当前节点开始的子节点名称路径数组 + * @returns 找到的 Label 组件,或 null + */ private findLabelByPath(path: string[]): Label | null { let current: Node | null = this.node; for (let i = 0; i < path.length; i++) { @@ -99,8 +161,14 @@ export class HInfoComp extends CCComp { return current.getComponent(Label) || current.getComponentInChildren(Label); } + // ======================== 英雄动画 ======================== - + /** + * 为英雄图标加载并播放 idle 动画(带竞态保护)。 + * @param node 图标节点 + * @param uuid 英雄 UUID + * @param token 视觉令牌 + */ private updateHeroAnimation(node: Node, uuid: number, token: number) { if (!node) return; const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite); @@ -115,6 +183,7 @@ export class HInfoComp extends CCComp { const path = `game/heros/hero/${hero.path}/idle`; resources.load(path, AnimationClip, (err, clip) => { if (err || !clip) return; + // 竞态保护 if (token !== this.iconVisualToken || !this.model || this.model.hero_uuid !== uuid) { return; } @@ -124,6 +193,7 @@ export class HInfoComp extends CCComp { }); } + /** 停止并清除图标节点上的动画 */ private clearIconAnimation(node: Node) { const anim = node?.getComponent(Animation); if (!anim) return; @@ -131,12 +201,15 @@ export class HInfoComp extends CCComp { this.clearAnimationClips(anim); } + /** 移除 Animation 组件上的全部 AnimationClip */ private clearAnimationClips(anim: Animation) { const clips = (anim as any).clips as AnimationClip[] | undefined; if (!clips || clips.length === 0) return; [...clips].forEach(clip => anim.removeClip(clip, true)); } + // ======================== 交互(当前已注释) ======================== + // private bindEvents() { // this.sell_node?.on(Button.EventType.CLICK, this.onSellHero, this); // this.node.on(NodeEventType.TOUCH_END, this.onOpenIBox, this); @@ -147,6 +220,10 @@ export class HInfoComp extends CCComp { // this.node.off(NodeEventType.TOUCH_END, this.onOpenIBox, this); // } + /** + * 点击面板时打开英雄详情弹窗(IBox)。 + * 传入英雄 UUID、等级和技能列表。 + */ private onOpenIBox() { if (!this.model) return; if (!this.isModelAlive()) return; @@ -161,6 +238,10 @@ export class HInfoComp extends CCComp { }); } + /** + * 出售英雄:通过 Hero.removeByEid 移除 ECS 实体, + * 并关闭详情弹窗。 + */ private onSellHero(event?: Event) { if (!this.eid) return; const removed = Hero.removeByEid(this.eid); @@ -173,7 +254,7 @@ export class HInfoComp extends CCComp { oops.gui.remove(UIID.IBox); } - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ + /** ECS 组件移除时的释放钩子:清理动画资源并销毁节点 */ reset() { this.clearIconAnimation(this.icon_node); this.iconVisualToken = 0; diff --git a/assets/script/game/map/HlistComp.ts b/assets/script/game/map/HlistComp.ts index 3823d693..7a006577 100644 --- a/assets/script/game/map/HlistComp.ts +++ b/assets/script/game/map/HlistComp.ts @@ -1,3 +1,23 @@ +/** + * @file HlistComp.ts + * @description 英雄列表轮播组件(UI 视图层) + * + * 职责: + * 1. 在主页界面以 **5 格轮播** 形式展示英雄图鉴。 + * 2. 支持左右切换,用 tween 动画平滑过渡节点位置。 + * 3. 点击切换时自动更新当前选中英雄的名称、AP、HP 和技能信息。 + * + * 关键设计: + * - carouselNodes[0..4] 对应 5 个展示位(最左-2 到最右+2), + * 中间位 [2] 为当前选中英雄。 + * - 切换时:将即将移出屏幕的节点瞬间跳转到另一端,再用 tween 滑入。 + * - 切换完成后重排 carouselNodes 数组保持逻辑顺序。 + * - iconVisualTokens 按节点独立管理竞态令牌,防止异步动画回调错乱。 + * + * 依赖: + * - HeroInfo / HeroList(heroSet)—— 英雄静态配置与全量英雄 UUID 列表 + * - SkillSet / IType(SkillSet)—— 技能配置与类型枚举 + */ import { _decorator, Animation, AnimationClip, Button, Event, Label, Node, NodeEventType, Sprite, resources, tween, Vec3 } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; @@ -8,53 +28,83 @@ import { mLogger } from "../common/Logger"; const {property, ccclass } = _decorator; -/** 视图层对象 */ +/** + * HListComp —— 英雄图鉴轮播视图组件 + * + * 在任务主页展示所有可用英雄,玩家可左右滑动查看: + * - 中间位显示完整信息(名称 / AP / HP / 技能列表) + * - 两侧位显示缩略 idle 动画 + */ @ccclass('HListComp') @ecs.register('HListComp', false) export class HListComp extends CCComp { + // ======================== 编辑器绑定节点 ======================== + + /** 中间位英雄 idle 图标节点 */ @property(Node) hero_icon=null! + /** 左侧第 1 位英雄图标 */ @property(Node) phero_icon=null! + /** 右侧第 1 位英雄图标 */ @property(Node) nhero_icon=null! + /** 左侧第 2 位英雄图标(最远) */ @property(Node) phero1_icon=null! + /** 右侧第 2 位英雄图标(最远) */ @property(Node) nhero1_icon=null! + /** 攻击力标签节点 */ @property(Node) ap_node=null! + /** 生命值标签节点 */ @property(Node) hp_node=null! + /** 技能信息容器节点(包含 Line1~Line5 子节点) */ @property(Node) info_node=null! + /** 英雄名称标签节点 */ @property(Node) name_node=null! - + /** 向左切换按钮 */ @property(Node) pre_btn=null! + /** 向右切换按钮 */ @property(Node) next_btn=null! + // ======================== 运行时状态 ======================== + + /** 当前选中英雄在 HeroList 中的索引 */ huuid:number=null! + /** 当前选中英雄在 HeroList 数组中的下标 */ private currentIndex: number = 0; + /** 各图标节点的视觉令牌映射(防止异步动画竞态) */ private iconVisualTokens: Map = new Map(); + /** 是否正在播放切换动画(防止快速连点) */ private isAnimating: boolean = false; + /** 轮播节点数组,顺序为 [左2, 左1, 中, 右1, 右2] */ private carouselNodes: Node[] = []; + /** 5 个固定位置坐标(从场景中读取初始值) */ private fixedPositions: Vec3[] = []; + /** 调试日志开关 */ debugMode: boolean = false; onLoad() { + // 绑定左右切换按钮事件 this.pre_btn?.on(NodeEventType.TOUCH_END, this.onPreClick, this); this.next_btn?.on(NodeEventType.TOUCH_END, this.onNextClick, this); } start() { + // 初始化轮播节点数组和固定位置 if (this.phero1_icon && this.phero_icon && this.hero_icon && this.nhero_icon && this.nhero1_icon) { this.carouselNodes = [this.phero1_icon, this.phero_icon, this.hero_icon, this.nhero_icon, this.nhero1_icon]; this.fixedPositions = this.carouselNodes.map(n => n.position.clone()); } + // 设置初始选中并加载所有位置的英雄动画 if (HeroList && HeroList.length > 0) { this.currentIndex = 0; this.initAllNodes(); @@ -62,6 +112,15 @@ export class HListComp extends CCComp { } } + // ======================== 切换逻辑 ======================== + + /** + * 向左切换(查看上一个英雄): + * 1. currentIndex 左移。 + * 2. 最右节点 n4 瞬间跳到最左位置,加载新英雄动画。 + * 3. 所有节点 tween 向右滑动一格。 + * 4. 动画完成后重排 carouselNodes 数组。 + */ private onPreClick() { if (!HeroList || HeroList.length === 0 || this.isAnimating || this.carouselNodes.length < 5) return; this.isAnimating = true; @@ -70,22 +129,31 @@ export class HListComp extends CCComp { const [n0, n1, n2, n3, n4] = this.carouselNodes; - // n4 instantly jumps from rightmost to leftmost position to get ready to slide in + // n4 瞬间跳至最左位置,准备滑入 n4.setPosition(new Vec3(this.fixedPositions[0].x, n4.position.y, n4.position.z)); this.updateNodeAnimationByOffset(n4, -2); this.updateHeroInfo(); + // 所有节点向右滑动一格 tween(n0).to(0.2, { position: new Vec3(this.fixedPositions[1].x, n0.position.y, n0.position.z) }).start(); tween(n1).to(0.2, { position: new Vec3(this.fixedPositions[2].x, n1.position.y, n1.position.z) }).start(); tween(n2).to(0.2, { position: new Vec3(this.fixedPositions[3].x, n2.position.y, n2.position.z) }).start(); tween(n3).to(0.2, { position: new Vec3(this.fixedPositions[4].x, n3.position.y, n3.position.z) }) .call(() => { + // 重排数组:n4 成为新的最左节点 this.carouselNodes = [n4, n0, n1, n2, n3]; this.isAnimating = false; }) .start(); } + /** + * 向右切换(查看下一个英雄): + * 1. currentIndex 右移。 + * 2. 最左节点 n0 瞬间跳到最右位置,加载新英雄动画。 + * 3. 所有节点 tween 向左滑动一格。 + * 4. 动画完成后重排 carouselNodes 数组。 + */ private onNextClick() { if (!HeroList || HeroList.length === 0 || this.isAnimating || this.carouselNodes.length < 5) return; this.isAnimating = true; @@ -94,32 +162,47 @@ export class HListComp extends CCComp { const [n0, n1, n2, n3, n4] = this.carouselNodes; - // n0 instantly jumps from leftmost to rightmost position to get ready to slide in + // n0 瞬间跳至最右位置,准备滑入 n0.setPosition(new Vec3(this.fixedPositions[4].x, n0.position.y, n0.position.z)); this.updateNodeAnimationByOffset(n0, 2); this.updateHeroInfo(); + // 所有节点向左滑动一格 tween(n1).to(0.2, { position: new Vec3(this.fixedPositions[0].x, n1.position.y, n1.position.z) }).start(); tween(n2).to(0.2, { position: new Vec3(this.fixedPositions[1].x, n2.position.y, n2.position.z) }).start(); tween(n3).to(0.2, { position: new Vec3(this.fixedPositions[2].x, n3.position.y, n3.position.z) }).start(); tween(n4).to(0.2, { position: new Vec3(this.fixedPositions[3].x, n4.position.y, n4.position.z) }) .call(() => { + // 重排数组:n0 成为新的最右节点 this.carouselNodes = [n1, n2, n3, n4, n0]; this.isAnimating = false; }) .start(); } + // ======================== 数据查询 ======================== + + /** + * 根据偏移量获取英雄 UUID。 + * @param offset 相对于当前选中英雄的偏移(-2, -1, 0, 1, 2) + * @returns HeroList 中对应位置的英雄 UUID + */ private getHeroUuid(offset: number): number { const len = HeroList.length; return HeroList[(this.currentIndex + offset + len * 5) % len]; } + /** + * 按偏移量更新指定节点的英雄动画。 + * @param node 目标图标节点 + * @param offset 偏移量 + */ private updateNodeAnimationByOffset(node: Node, offset: number) { const uuid = this.getHeroUuid(offset); this.updateHeroAnimation(node, uuid); } + /** 更新当前选中英雄的详细信息(名称、AP、HP、技能列表) */ private updateHeroInfo() { this.huuid = this.getHeroUuid(0); const hero = HeroInfo[this.huuid]; @@ -131,6 +214,7 @@ export class HListComp extends CCComp { this.updateSkillInfo(hero); } + /** 初始化 5 个轮播位的英雄动画 */ private initAllNodes() { if (this.carouselNodes.length < 5) return; this.updateNodeAnimationByOffset(this.carouselNodes[0], -2); @@ -140,6 +224,13 @@ export class HListComp extends CCComp { this.updateNodeAnimationByOffset(this.carouselNodes[4], 2); } + // ======================== UI 工具 ======================== + + /** + * 安全设置 Label 文本 + * @param node 标签所在节点 + * @param text 文本内容 + */ private setLabelText(node: Node, text: string) { if (!node) return; const label = node.getComponent(Label) || node.getComponentInChildren(Label); @@ -148,6 +239,15 @@ export class HListComp extends CCComp { } } + // ======================== 英雄动画 ======================== + + /** + * 为指定节点加载并播放英雄 idle 动画。 + * 使用 iconVisualTokens 做节点级竞态保护。 + * + * @param node 图标节点 + * @param uuid 英雄 UUID + */ private updateHeroAnimation(node: Node, uuid: number) { if (!node) return; const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite); @@ -159,6 +259,7 @@ export class HListComp extends CCComp { const anim = node.getComponent(Animation) || node.addComponent(Animation); this.clearAnimationClips(anim); + // 递增该节点的视觉令牌 let token = (this.iconVisualTokens.get(node) || 0) + 1; this.iconVisualTokens.set(node, token); const path = `game/heros/hero/${hero.path}/idle`; @@ -168,6 +269,7 @@ export class HListComp extends CCComp { mLogger.log(this.debugMode, "HListComp", `load hero animation failed ${uuid}`, err); return; } + // 竞态保护:令牌不匹配则丢弃 if (token !== this.iconVisualTokens.get(node)) { return; } @@ -177,6 +279,7 @@ export class HListComp extends CCComp { }); } + /** 移除 Animation 上的全部 clip(倒序遍历避免索引偏移) */ private clearAnimationClips(anim: Animation) { const clips = anim.clips; if (clips && clips.length > 0) { @@ -187,6 +290,11 @@ export class HListComp extends CCComp { } } + /** + * 递归查找子节点(按名称) + * @param root 起始节点 + * @param name 目标节点名 + */ private findNodeByName(root: Node, name: string): Node | null { if (!root) return null; if (root.name === name) return root; @@ -197,6 +305,15 @@ export class HListComp extends CCComp { return null; } + // ======================== 技能信息 ======================== + + /** + * 更新技能信息面板: + * 遍历英雄的技能列表,为 Line1~Line5 节点填充技能名、等级、CD、描述。 + * 同时根据技能类型(近战 / 远程 / 辅助)切换对应图标。 + * + * @param hero 英雄配置数据(含 skills 字段) + */ private updateSkillInfo(hero: any) { if (!this.info_node) return; @@ -212,6 +329,7 @@ export class HListComp extends CCComp { const skillId = skill.uuid; const config = SkillSet[skillId]; + // 拼接技能信息文本 const text = config ? `${config.name} Lv.${skill.lv} CD:${skill.cd}s ${config.info}` : `未知技能 CD:${skill.cd}s`; const noteNode = this.findNodeByName(line, "note"); @@ -220,6 +338,7 @@ export class HListComp extends CCComp { label.string = text; } + // 切换技能类型图标 this.updateLineTypeIcon(line, config?.IType); } else { line.active = false; @@ -227,6 +346,15 @@ export class HListComp extends CCComp { } } + /** + * 更新技能行的类型图标(互斥显示): + * - Melee → 近战图标 + * - remote → 远程图标 + * - support → 辅助图标 + * + * @param line 技能行节点 + * @param iType 技能类型枚举 + */ private updateLineTypeIcon(line: Node, iType?: IType) { const meleeNode = this.findNodeByName(line, "Melee"); const remoteNode = this.findNodeByName(line, "remote"); @@ -236,7 +364,7 @@ export class HListComp extends CCComp { if (supportNode) supportNode.active = iType === IType.support; } - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ + /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy(); } diff --git a/assets/script/game/map/IBoxComp.ts b/assets/script/game/map/IBoxComp.ts index abadd86b..f66e5715 100644 --- a/assets/script/game/map/IBoxComp.ts +++ b/assets/script/game/map/IBoxComp.ts @@ -1,3 +1,24 @@ +/** + * @file IBoxComp.ts + * @description 英雄信息弹窗组件(IBox,UI 视图层) + * + * 职责: + * 1. 作为全局弹窗,展示某个英雄的 **详细技能信息**。 + * 2. 通过 onAdded(args) 接收英雄 UUID、等级和运行时技能数据。 + * 3. 自动计算技能行数并动态调整弹窗背景高度和名称位置。 + * 4. 点击弹窗任意区域关闭自身。 + * + * 关键设计: + * - Line1~Line5 为预设的 5 行技能节点,按需显示/隐藏。 + * - 每行包含技能名、等级、CD、描述文本和类型图标(近战/远程/辅助)。 + * - 背景高度 = baseHeight + (行数 - 1) × extraLineHeight。 + * - 若传入 args.skills(运行时技能),优先使用;否则回退到英雄静态配置。 + * + * 依赖: + * - HeroInfo(heroSet)—— 英雄静态配置 + * - SkillSet / IType(SkillSet)—— 技能静态配置 + * - UIID —— 在 oops.gui 系统中注册的弹窗 ID + */ import { _decorator, Label, Node, NodeEventType, UITransform } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; @@ -6,9 +27,16 @@ import { IType, SkillSet } from "../common/config/SkillSet"; import { oops } from "db://oops-framework/core/Oops"; const { ccclass, property } = _decorator; +/** + * IBoxComp —— 英雄信息弹窗视图组件 + * + * 通过 oops.gui.open(UIID.IBox, { heroUuid, heroLv, skills? }) 打开。 + * 展示英雄技能列表,支持最多 5 行。 + */ @ccclass('IBoxComp') @ecs.register('IBoxComp', false) export class IBoxComp extends CCComp { + // ======================== 编辑器绑定节点(5 行技能) ======================== @property(Node) Line1: Node = null! @property(Node) @@ -19,11 +47,23 @@ export class IBoxComp extends CCComp { Line4: Node = null! @property(Node) Line5: Node = null! + + // ======================== 布局常量 ======================== + /** 弹窗背景基础高度(只有 1 行技能时的高度) */ private readonly baseHeight: number = 100; + /** 每增加一行技能,背景增加的高度 */ private readonly extraLineHeight: number = 50; + /** 英雄名称标签的基准 Y 坐标 */ private readonly nameBaseY: number = 50; + /** 每增加一行技能,名称标签额外上移的 Y 偏移 */ private readonly nameExtraLineOffsetY: number = 25; + /** + * ECS 实体挂载回调:接收外部传入的英雄参数并渲染。 + * @param args.heroUuid 英雄 UUID + * @param args.heroLv 英雄当前等级 + * @param args.skills (可选)运行时技能数据,若无则使用英雄静态配置 + */ onAdded(args: { heroUuid?: number; heroLv?: number; @@ -32,18 +72,32 @@ export class IBoxComp extends CCComp { this.renderHeroInfo(args); } + /** 绑定点击关闭事件 */ onLoad() { this.node.on(NodeEventType.TOUCH_END, this.onTapClose, this); } + /** 解绑点击事件 */ onDestroy() { this.node.off(NodeEventType.TOUCH_END, this.onTapClose, this); } + /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy(); } + // ======================== 渲染逻辑 ======================== + + /** + * 主渲染方法:解析英雄数据并填充技能行。 + * + * 流程: + * 1. 从 args 中取英雄 UUID 和等级。 + * 2. 优先使用运行时技能数据(args.skills),否则回退英雄静态配置。 + * 3. 将每个技能映射为 { text, iType },过滤无效项。 + * 4. 调用 applyLineData 渲染到 Line1~Line5。 + */ private renderHeroInfo(args: { heroUuid?: number; heroLv?: number; @@ -58,13 +112,18 @@ export class IBoxComp extends CCComp { return; } this.setHeroName(hero.name); + + // 运行时技能 vs 静态配置 const runtimeSkills = args?.skills ? Object.values(args.skills) : []; const sourceSkills = runtimeSkills.length > 0 ? runtimeSkills : Object.values(hero.skills ?? {}); + + // 将每个技能转换为显示数据 const lineData = sourceSkills.map(skill => { const skillId = Math.floor(skill?.uuid ?? 0); if (!skillId) return null; const config = SkillSet[skillId]; if (!config) return null; + // 运行时技能直接使用其等级;静态配置的技能等级需叠加英雄等级修正 const runtimeLv = runtimeSkills.length > 0 ? Math.max(0, Math.floor(skill.lv ?? 0)) : Math.max(0, Math.floor((skill.lv ?? 1) + heroLv - 2)); const cd = Number(skill?.cd ?? 0); return { @@ -72,9 +131,18 @@ export class IBoxComp extends CCComp { iType: config.IType }; }).filter(item => !!item) as Array<{ text: string; iType: IType }>; + this.applyLineData(lineData.length > 0 ? lineData : [{ text: "暂无技能信息" }]); } + /** + * 将技能数据应用到 Line1~Line5: + * 1. 按顺序填充文本和类型图标。 + * 2. 超出的行隐藏。 + * 3. 根据显示行数调整弹窗高度和名称位置。 + * + * @param skillLines 技能行数据数组 + */ private applyLineData(skillLines: Array<{ text: string; iType?: IType }>) { const lines = [this.Line1, this.Line2, this.Line3, this.Line4, this.Line5]; const showCount = Math.max(1, Math.min(lines.length, skillLines.length)); @@ -86,16 +154,24 @@ export class IBoxComp extends CCComp { if (!active) continue; const data = skillLines[i]; const text = data?.text ?? ""; + // 查找技能文本节点并设置内容 const noteNode = line.getChildByName("note"); const label = noteNode?.getComponent(Label) || noteNode?.getComponentInChildren(Label) || line.getComponentInChildren(Label); if (label) label.string = text; + // 更新类型图标 this.updateLineTypeIcon(line, data?.iType); } + // 动态调整弹窗背景高度 const targetHeight = this.baseHeight + Math.max(0, showCount - 1) * this.extraLineHeight; this.updateIBoxHeight(targetHeight); + // 动态调整名称位置 this.updateNamePosition(showCount); } + /** + * 设置弹窗背景 Bg 节点的高度 + * @param height 目标高度 + */ private updateIBoxHeight(height: number) { const bgNode = this.node.getChildByName("Bg"); const bgTransform = bgNode?.getComponent(UITransform); @@ -104,6 +180,10 @@ export class IBoxComp extends CCComp { } } + /** + * 设置英雄名称标签 + * @param name 英雄名称 + */ private setHeroName(name: string) { const bgNode = this.node.getChildByName("Bg"); const nameNode = bgNode?.getChildByName("name"); @@ -114,6 +194,12 @@ export class IBoxComp extends CCComp { } } + /** + * 根据显示行数调整名称节点的 Y 坐标。 + * 行数越多,名称越往上移,保持视觉居中。 + * + * @param showCount 当前显示的技能行数 + */ private updateNamePosition(showCount: number) { const bgNode = this.node.getChildByName("Bg"); const nameNode = bgNode?.getChildByName("name"); @@ -123,6 +209,11 @@ export class IBoxComp extends CCComp { nameNode.setPosition(current.x, targetY, current.z); } + /** + * 更新技能行的类型图标(互斥显示)。 + * @param line 技能行节点 + * @param iType 技能类型(近战 / 远程 / 辅助) + */ private updateLineTypeIcon(line: Node, iType?: IType) { const meleeNode = line.getChildByName("Melee"); const remoteNode = line.getChildByName("remote"); @@ -132,6 +223,7 @@ export class IBoxComp extends CCComp { if (supportNode) supportNode.active = iType === IType.support; } + /** 点击弹窗任意区域关闭自身 */ private onTapClose() { oops.gui.removeByNode(this.node); } diff --git a/assets/script/game/map/MissSkillsComp.ts b/assets/script/game/map/MissSkillsComp.ts index 12d82833..5c8e1910 100644 --- a/assets/script/game/map/MissSkillsComp.ts +++ b/assets/script/game/map/MissSkillsComp.ts @@ -1,3 +1,25 @@ +/** + * @file MissSkillsComp.ts + * @description 场上技能卡槽位管理器组件(UI 视图层) + * + * 职责: + * 1. 管理场上已使用的 **技能卡** 的可视化槽位(最多 10 个)。 + * 2. 监听 UseSkillCard 事件,当玩家使用技能卡时实例化 SkillBoxComp 并放入空闲槽位。 + * 3. 监听 RemoveSkillBox 事件,当技能生效完毕后回收槽位并重新排列。 + * + * 关键设计: + * - slots 数组预定义了 10 个固定坐标位(2 行 × 5 列), + * 每个槽位记录是否占用及对应节点引用。 + * - 当某个 SkillBox 销毁时,触发 rearrangeSlots 将剩余节点 + * 紧凑地重排到前置槽位,避免视觉空洞。 + * - SkillBox 的实例化使用 skill_box Prefab,在编辑器中绑定。 + * + * 依赖: + * - SkillBoxComp(SkillBoxComp.ts)—— 单个技能卡的效果控制组件 + * - GameEvent.UseSkillCard —— 技能卡使用事件 + * - GameEvent.RemoveSkillBox —— 技能卡移除事件 + * - smc.map.MapView.scene.entityLayer —— 技能节点的父容器(SKILL 节点) + */ import { mLogger } from "../common/Logger"; import { _decorator, Node, Prefab, instantiate, Vec3 } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; @@ -8,21 +30,39 @@ import { GameEvent } from "../common/config/GameEvent"; import { smc } from "../common/SingletonModuleComp"; const { ccclass, property } = _decorator; +/** 技能槽位数据结构 */ interface SkillBoxSlot { + /** 该槽位的固定 X 坐标 */ x: number; + /** 该槽位的固定 Y 坐标 */ y: number; + /** 是否已被占用 */ used: boolean; + /** 占用该槽位的节点引用 */ node: Node | null; } -/** 视图层对象 */ +/** + * MissSkillsComp —— 场上技能卡槽位管理器 + * + * 在战斗场景中管理已激活的技能卡显示位置。 + * 2 行 × 5 列 = 10 个槽位,不足时提示已满。 + */ @ccclass('MissSkillsComp') @ecs.register('MissSkillsComp', false) export class MissSkillsComp extends CCComp { + /** 调试日志开关 */ private debugMode: boolean = true; + + /** 技能卡 Prefab(在编辑器中赋值) */ @property({type: Prefab}) private skill_box: Prefab = null; + /** + * 预定义的 10 个槽位坐标(2 行 × 5 列): + * 第 1 行 y=240:x = -320, -240, -160, -80, 0 + * 第 2 行 y=320:x = -320, -240, -160, -80, 0 + */ private slots: SkillBoxSlot[] = [ { x: -320, y: 240, used: false, node: null }, { x: -240, y: 240, used: false, node: null }, @@ -36,16 +76,26 @@ export class MissSkillsComp extends CCComp { { x: 0, y: 320, used: false, node: null }, ]; + /** 注册事件监听 */ onLoad() { oops.message.on(GameEvent.UseSkillCard, this.onUseSkillCard, this); oops.message.on(GameEvent.RemoveSkillBox, this.onRemoveSkillBox, this); } + /** 移除事件监听 */ onDestroy() { oops.message.off(GameEvent.UseSkillCard, this.onUseSkillCard, this); oops.message.off(GameEvent.RemoveSkillBox, this.onRemoveSkillBox, this); } + /** + * 处理技能卡移除事件: + * 1. 在 slots 中找到对应节点并释放。 + * 2. 调用 rearrangeSlots 紧凑重排。 + * + * @param event 事件名 + * @param args 要移除的节点引用 + */ private onRemoveSkillBox(event: string, args: any) { const node = args as Node; let removed = false; @@ -62,7 +112,12 @@ export class MissSkillsComp extends CCComp { } } + /** + * 紧凑重排:将所有有效节点按顺序移到前置槽位。 + * 确保视觉上不会出现中间空洞。 + */ private rearrangeSlots() { + // 收集所有有效节点 const validNodes: Node[] = []; for (let i = 0; i < this.slots.length; i++) { if (this.slots[i].used && this.slots[i].node && this.slots[i].node.isValid) { @@ -71,7 +126,7 @@ export class MissSkillsComp extends CCComp { this.slots[i].used = false; this.slots[i].node = null; } - + // 按顺序重新分配 for (let i = 0; i < validNodes.length; i++) { if (i < this.slots.length) { this.slots[i].used = true; @@ -81,6 +136,11 @@ export class MissSkillsComp extends CCComp { } } + /** + * 处理使用技能卡事件:提取 uuid 和 card_lv 后调用 addSkill。 + * @param event 事件名 + * @param args 卡牌数据(含 uuid、card_lv) + */ private onUseSkillCard(event: string, args: any) { const payload = args ?? event; const uuid = Number(payload?.uuid ?? 0); @@ -93,7 +153,17 @@ export class MissSkillsComp extends CCComp { } + /** + * 在场上添加一个技能卡: + * 1. 在 slots 中查找空闲位。 + * 2. 实例化 skill_box Prefab 并放置在空闲位坐标。 + * 3. 获取或添加 SkillBoxComp 并初始化。 + * + * @param uuid 技能 UUID + * @param card_lv 技能卡等级 + */ addSkill(uuid: number, card_lv: number) { + // 技能节点的父容器 var parent = smc.map.MapView.scene.entityLayer!.node!.getChildByName("SKILL")!; if (!this.skill_box) { @@ -101,12 +171,14 @@ export class MissSkillsComp extends CCComp { return; } + // 查找空闲槽位 const emptyIndex = this.slots.findIndex(slot => !slot.used); if (emptyIndex === -1) { mLogger.warn(this.debugMode, "MissSkillsComp", "skill_box slots are full"); return; } + // 实例化并放入槽位 const node = instantiate(this.skill_box); node.parent = parent; node.setPosition(new Vec3(this.slots[emptyIndex].x, this.slots[emptyIndex].y, 0)); @@ -114,11 +186,12 @@ export class MissSkillsComp extends CCComp { this.slots[emptyIndex].used = true; this.slots[emptyIndex].node = node; + // 初始化技能效果组件 const comp = node.getComponent(SkillBoxComp) || node.addComponent(SkillBoxComp); comp.init(uuid, card_lv); } - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ + /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy(); } diff --git a/assets/script/game/map/MissionCardComp.ts b/assets/script/game/map/MissionCardComp.ts index 344b9a04..44dbc3e3 100644 --- a/assets/script/game/map/MissionCardComp.ts +++ b/assets/script/game/map/MissionCardComp.ts @@ -1,3 +1,37 @@ +/** + * @file MissionCardComp.ts + * @description 卡牌系统核心控制器(战斗 UI 层 + 业务逻辑层) + * + * 职责: + * 1. **卡牌分发管理** —— 从卡池抽取 4 张卡,分发到 4 个 CardComp 槽位。 + * 2. **卡池升级** —— 消耗金币提升卡池等级(poolLv),解锁更高稀有度的卡牌。 + * 3. **金币费用管理** —— 抽卡费用(refreshCost)、升级费用(CardsUpSet)、 + * 波次折扣(CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE)的计算与扣除。 + * 4. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许 + * 再召唤英雄(含合成后腾位的特殊判断 canUseHeroCardByMerge)。 + * 5. **场上英雄信息面板(HInfoComp 列表)同步** —— + * 英雄上场时实例化面板,死亡时移除,定时刷新属性显示。 + * 6. **特殊卡执行** —— 处理英雄升级卡(SpecialUpgrade)和英雄刷新卡(SpecialRefresh)。 + * 7. **准备/战斗阶段切换** —— 控制卡牌面板的 展开/收起 动画。 + * + * 关键设计: + * - 4 个 CardComp 通过 cacheCardComps() 映射为有序数组 cardComps[], + * 之后所有分发、清空操作均通过此数组进行。 + * - buildDrawCards() 保证每次抽取 4 张,不足时循环补齐。 + * - 英雄上限校验(onUseHeroCard)采用 guard/cancel 模式: + * CardComp 发出 UseHeroCard 事件并传入 guard 对象, + * 本组件可通过 guard.cancel=true 阻止使用。 + * - ensureHeroInfoPanel() 建立 EID → HInfoComp 的 Map 映射, + * 支持英雄合成升级后面板热更新。 + * + * 依赖: + * - CardComp —— 单卡槽位 + * - HInfoComp —— 英雄信息面板 + * - CardSet 模块 —— 卡池配置、抽卡规则、特殊卡数据 + * - HeroAttrsComp —— 英雄属性(合成校验 / 升级) + * - MissionHeroCompComp —— 获取合成规则(needCount / maxLv) + * - smc.vmdata.mission_data —— 局内数据(coin / hero_num / hero_max_num) + */ import { mLogger } from "../common/Logger"; import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, SpriteAtlas, Tween, tween, Vec3 } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; @@ -18,7 +52,12 @@ import { MissionHeroCompComp } from "./MissionHeroComp"; const { ccclass, property } = _decorator; -/** 视图层对象 */ +/** + * MissionCardComp —— 卡牌系统核心控制器 + * + * 管理 4 个卡牌槽位的抽卡分发、卡池升级、金币费用、 + * 英雄上限校验、场上英雄信息面板同步以及特殊卡执行。 + */ @ccclass('MissionCardComp') @ecs.register('MissionCard', false) export class MissionCardComp extends CCComp { diff --git a/assets/script/game/map/MissionComp.ts b/assets/script/game/map/MissionComp.ts index c28c138e..b1f098ae 100644 --- a/assets/script/game/map/MissionComp.ts +++ b/assets/script/game/map/MissionComp.ts @@ -1,3 +1,32 @@ +/** + * @file MissionComp.ts + * @description 任务(关卡)核心控制组件(UI + 逻辑层) + * + * 职责: + * 1. 管理单局游戏的 **完整生命周期**:初始化 → 准备阶段 → 战斗阶段 → 结算。 + * 2. 在战斗阶段每帧更新战斗计时器、同步怪物数量、检测英雄全灭。 + * 3. 管理怪物数量阈值(暂停 / 恢复刷怪的上下限)。 + * 4. 处理新一波事件(NewWave),进入准备阶段并发放金币奖励。 + * 5. 提供战斗结束后的结算弹窗入口(VictoryComp)。 + * 6. (可选)内建性能监控面板,显示内存、帧率、实体数量等开发信息。 + * + * 关键设计: + * - mission_start() 初始化所有游戏数据 → 进入准备阶段 → 显示 loading。 + * - 准备阶段(enterPreparePhase):停止刷怪,显示开始按钮。 + * - 战斗阶段(to_fight):开始刷怪,隐藏按钮,由 update 驱动。 + * - 怪物数量管理采用 max/resume 双阈值: + * * 超过 max → 暂停刷怪(stop_spawn_mon=true) + * * 降至 resume 以下 → 恢复刷怪 + * - cleanComponents() 在任务开始/结束时销毁所有英雄和技能 ECS 实体。 + * - clearBattlePools() 回收对象池(Monster / Skill / Tooltip)。 + * + * 依赖: + * - smc.mission —— 全局任务运行状态(play / pause / in_fight / stop_spawn_mon 等) + * - smc.vmdata.mission_data —— 局内数据(金币 / 波数 / 怪物数量等) + * - FightSet —— 战斗常量配置 + * - CardInitCoins —— 初始金币数 + * - UIID.Victory —— 结算弹窗 + */ import { _decorator, Vec3,Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; @@ -19,7 +48,12 @@ const { ccclass, property } = _decorator; //@todo 需要关注 当boss死亡的时候的动画播放完成后,需要触发事件,通知 MissionComp 进行奖励处理 -/** 视图层对象 */ +/** + * MissionComp —— 任务(关卡)核心控制器 + * + * 驱动单局游戏的完整流程:准备 → 战斗 → 结算。 + * 管理战斗计时、怪物数量控制、英雄全灭检测和金币奖励发放。 + */ @ccclass('MissionComp') @ecs.register('MissionComp', false) export class MissionComp extends CCComp { @@ -28,66 +62,105 @@ export class MissionComp extends CCComp { @property({ tooltip: "是否显示战斗内存观测面板" }) private showMemoryPanel: boolean = false; - + // ======================== 配置参数 ======================== + + /** 怪物数量上限(超过后暂停刷怪) */ private maxMonsterCount: number = 5; + /** 怪物数量恢复阈值(降至此值以下恢复刷怪) */ private resumeMonsterCount: number = 3; + /** 新一波金币奖励基础值 */ private prepareBaseCoinReward: number = 100; + /** 每一波金币增长值 */ private prepareCoinWaveGrow: number = 1; + /** 金币奖励上限 */ private prepareCoinRewardCap: number = 500; - // VictoryComp:any = null; - // reward:number = 0; - // reward_num:number = 0; + // ======================== 编辑器绑定节点 ======================== + + /** 开始战斗按钮 */ @property(Node) start_btn:Node = null! + /** 时间/波数显示节点 */ @property(Node) time_node:Node = null! + // ======================== 运行时状态 ======================== + + /** 战斗倒计时(秒) */ FightTime:number = FightSet.FiIGHT_TIME /** 剩余复活次数 */ revive_times: number = 1; + /** 掉落奖励列表 */ rewards:any[]=[] + /** 累计游戏数据 */ game_data:any={ exp:0, gold:0, diamond:0 } + /** 上一次显示的时间字符串(避免重复设置) */ private lastTimeStr: string = ""; + /** 上一次显示的秒数(避免重复计算) */ private lastTimeSecond: number = -1; + /** 性能监控面板 Label 引用 */ private memoryLabel: Label | null = null; + /** 性能监控刷新计时器 */ private memoryRefreshTimer: number = 0; + /** 上一次性能文本(避免重复渲染) */ private lastMemoryText: string = ""; + /** 帧间隔累加(用于计算平均 FPS) */ private perfDtAcc: number = 0; + /** 帧数计数 */ private perfFrameCount: number = 0; + /** 初始堆内存基准值(MB) */ private heapBaseMB: number = -1; + /** 堆内存峰值(MB) */ private heapPeakMB: number = 0; + /** 堆内存增长趋势(MB/分钟) */ private heapTrendPerMinMB: number = 0; + /** 趋势计算计时器 */ private heapTrendTimer: number = 0; + /** 趋势计算基准(MB) */ private heapTrendBaseMB: number = -1; + /** 怪物数量同步计时器(降低同步频率) */ private monsterCountSyncTimer: number = 0; + /** 当前波数 */ private currentWave: number = 0; + /** 上一次发放金币奖励的波数(防止重复发放) */ private lastPrepareCoinWave: number = 0; + + // ======================== ECS 查询匹配器(预缓存) ======================== + + /** 匹配拥有 HeroViewComp 的实体(英雄/怪物视图) */ private readonly heroViewMatcher = ecs.allOf(HeroViewComp); + /** 匹配拥有 SkillView 的实体(技能视图) */ private readonly skillViewMatcher = ecs.allOf(SkillView); + /** 匹配拥有 HeroAttrsComp 的实体(英雄/怪物属性) */ private readonly heroAttrsMatcher = ecs.allOf(HeroAttrsComp); - // 记录已触发的特殊刷怪索引 + // ======================== 生命周期 ======================== onLoad(){ this.showMemoryPanel = false + // 注册生命周期事件 this.on(GameEvent.MissionStart,this.mission_start,this) - // this.on(GameEvent.HeroDead,this.do_hero_dead,this) - // this.on(GameEvent.FightEnd,this.fight_end,this) this.on(GameEvent.MissionEnd,this.mission_end,this) this.on(GameEvent.NewWave,this.onNewWave,this) this.on(GameEvent.DO_AD_BACK,this.do_ad,this) this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this) this.removeMemoryPanel() } + onDestroy(){ this.start_btn?.off(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this) } + + /** + * 帧更新: + * - 非播放 / 暂停状态 → 跳过 + * - 战斗中 → 同步怪物状态、更新计时器 + */ protected update(dt: number): void { if(!smc.mission.play) return if(smc.mission.pause) return @@ -96,10 +169,13 @@ export class MissionComp extends CCComp { if(smc.mission.stop_mon_action) return smc.vmdata.mission_data.fight_time+=dt this.FightTime-=dt - // 检查特殊刷怪时间 this.update_time(); } } + + // ======================== 时间显示 ======================== + + /** 更新时间/波数显示(仅在秒数变化时更新以减少 Label 操作) */ update_time(){ const time = Math.max(0, this.FightTime); const remainSecond = Math.floor(time); @@ -115,12 +191,16 @@ export class MissionComp extends CCComp { } } + // ======================== 奖励与广告 ======================== - //奖励发放 + /** 奖励发放(预留) */ do_reward(){ - // 奖励发放 } + /** + * 广告回调处理: + * 成功 → 增加刷新次数;失败 → 分发失败事件。 + */ do_ad(){ if(this.ad_back()){ oops.message.dispatchEvent(GameEvent.AD_BACK_TRUE) @@ -129,15 +209,24 @@ export class MissionComp extends CCComp { oops.message.dispatchEvent(GameEvent.AD_BACK_FALSE) } } + + /** 广告观看结果(预留,默认返回 true) */ ad_back(){ return true } + // ======================== 任务生命周期 ======================== + /** + * 任务开始: + * 1. 取消上一局延迟回调。 + * 2. 清理残留实体。 + * 3. 初始化全部局内数据。 + * 4. 分发 FightReady 事件。 + * 5. 进入准备阶段并显示 loading。 + */ async mission_start(){ - // 防止上一局的 fight_end 延迟回调干扰新局 this.unscheduleAllCallbacks(); - // 确保清理上一局的残留实体 this.cleanComponents(); this.node.active=true this.data_init() @@ -148,17 +237,29 @@ export class MissionComp extends CCComp { this.scheduleOnce(()=>{ loading.active=false },0.5) - } + /** + * 进入战斗: + * - 恢复刷怪 + * - 标记战斗中 + * - 隐藏开始按钮 + * - 分发 FightStart 事件 + */ to_fight(){ smc.mission.stop_spawn_mon = false; smc.mission.in_fight=true smc.vmdata.mission_data.in_fight = true if (this.start_btn && this.start_btn.isValid) this.start_btn.active = false; - oops.message.dispatchEvent(GameEvent.FightStart) //GameSetMonComp 监听刷怪 + oops.message.dispatchEvent(GameEvent.FightStart) } + /** + * 进入准备阶段: + * - 标记非战斗 + * - 暂停刷怪 + * - 显示开始按钮 + */ private enterPreparePhase() { smc.mission.in_fight = false; smc.vmdata.mission_data.in_fight = false @@ -166,6 +267,7 @@ export class MissionComp extends CCComp { if (this.start_btn && this.start_btn.isValid) this.start_btn.active = true; } + /** 开始战斗按钮点击回调 */ private onStartFightBtnClick() { if (!smc.mission.play) return; if (smc.mission.pause) return; @@ -174,11 +276,16 @@ export class MissionComp extends CCComp { } + /** + * 打开结算弹窗: + * - 暂停游戏 + * - 打开 VictoryComp 弹窗 + * + * @param e 事件对象(未使用) + * @param is_hero_dead 是否因英雄全灭触发 + */ open_Victory(e:any,is_hero_dead: boolean = false){ - // 暂停游戏循环和怪物行为 - // smc.mission.play = false; smc.mission.pause = true; - // oops.message.dispatchEvent(GameEvent.FightEnd,{victory:false}) mLogger.log(this.debugMode, 'MissionComp', " open_Victory",is_hero_dead,this.revive_times) oops.gui.open(UIID.Victory,{ victory:false, @@ -189,10 +296,8 @@ export class MissionComp extends CCComp { } - + /** 战斗结束:延迟清理组件和对象池 */ fight_end(){ - // mLogger.log(this.debugMode, 'MissionComp', "任务结束") - // 延迟0.5秒后执行任务结束逻辑 this.scheduleOnce(() => { smc.mission.play=false this.cleanComponents() @@ -200,9 +305,14 @@ export class MissionComp extends CCComp { }, 0.5) } + /** + * 任务结束(完全退出关卡): + * - 取消所有延迟回调 + * - 重置全局标志 + * - 清理组件和对象池 + * - 隐藏节点 + */ mission_end(){ - // mLogger.log(this.debugMode, 'MissionComp', " mission_end") - // 合并 FightEnd 逻辑:清理组件、停止游戏循环 this.unscheduleAllCallbacks(); smc.mission.play=false smc.mission.pause = false; @@ -214,8 +324,14 @@ export class MissionComp extends CCComp { this.node.active=false } + /** + * 初始化全部局内数据: + * - 全局运行标志 + * - 战斗时间 / 怪物数量 / 金币 / 波数 + * - 奖励列表 / 复活次数 + * - 性能监控基准值 + */ data_init(){ - //局内数据初始化 smc 数据初始化 smc.mission.play = true; smc.mission.pause = false; smc.mission.stop_mon_action = false; @@ -227,8 +343,8 @@ export class MissionComp extends CCComp { smc.vmdata.mission_data.mon_max = Math.max(1, Math.floor(this.maxMonsterCount)) this.currentWave = 1; this.FightTime=FightSet.FiIGHT_TIME - this.rewards=[] // 改为数组,用于存储掉落物品列表 - this.revive_times = 1; // 每次任务开始重置复活次数 + this.rewards=[] + this.revive_times = 1; this.lastTimeStr = ""; this.lastTimeSecond = -1; this.memoryRefreshTimer = 0; @@ -243,15 +359,20 @@ export class MissionComp extends CCComp { this.monsterCountSyncTimer = 0; this.lastPrepareCoinWave = 0; smc.vmdata.mission_data.coin = Math.max(0, Math.floor(CardInitCoins)); - - // 重置全局属性加成和主角引用 (确保新一局数据干净) - // smc.role = null; - - // 重置英雄数据,确保新一局是初始状态 - - // mLogger.log(this.debugMode, 'MissionComp', "局内数据初始化",smc.vmdata.mission_data) } + // ======================== 波次管理 ======================== + + /** + * 新一波事件回调: + * 1. 进入准备阶段。 + * 2. 更新当前波数。 + * 3. 发放本波金币奖励。 + * 4. 刷新时间显示。 + * + * @param event 事件名 + * @param data { wave: number } + */ private onNewWave(event: string, data: any) { const wave = Number(data?.wave ?? 0); if (wave <= 0) return; @@ -263,6 +384,13 @@ export class MissionComp extends CCComp { this.update_time(); } + /** + * 按波数发放金币奖励: + * reward = min(cap, base + (wave - 1) × grow) + * 仅在波数首次到达时发放,防止重复。 + * + * @param wave 当前波数 + */ private grantPrepareCoinByWave(wave: number) { if (wave <= 0) return; if (wave <= this.lastPrepareCoinWave) return; @@ -280,12 +408,26 @@ export class MissionComp extends CCComp { mLogger.log(this.debugMode, 'MissionComp', "prepare coin reward", { wave, reward, coin: smc.vmdata.mission_data.coin }); } + // ======================== 怪物数量管理 ======================== + + /** + * 获取怪物数量阈值配置。 + * @returns { max: 刷怪上限, resume: 恢复刷怪阈值 } + */ private getMonsterThresholds(): { max: number; resume: number } { const max = Math.max(1, Math.floor(this.maxMonsterCount)); const resume = Math.min(max - 1, Math.max(0, Math.floor(this.resumeMonsterCount))); return { max, resume }; } + /** + * 同步怪物刷新状态(降频执行,每 0.2 秒一次): + * 1. 遍历所有 HeroAttrsComp 实体,统计怪物和英雄数量。 + * 2. 检测英雄全灭。 + * 3. 根据 max/resume 阈值切换 stop_spawn_mon 状态。 + * + * @param dt 帧间隔 + */ private syncMonsterSpawnState(dt: number) { this.monsterCountSyncTimer += dt; if (dt > 0 && this.monsterCountSyncTimer < 0.2) return; @@ -309,12 +451,18 @@ export class MissionComp extends CCComp { smc.vmdata.mission_data.mon_max = max; const stopSpawn = !!smc.mission.stop_spawn_mon; if (stopSpawn) { + // 降至恢复阈值以下 → 恢复刷怪 if (monsterCount <= resume) smc.mission.stop_spawn_mon = false; return; } + // 超过上限 → 暂停刷怪 if (monsterCount >= max) smc.mission.stop_spawn_mon = true; } + /** + * 英雄全灭检测:若场上无存活英雄且处于战斗中,触发结算弹窗。 + * @param heroCount 当前存活英雄数量 + */ private handleHeroWipe(heroCount: number) { if (heroCount > 0) return; if (!smc.mission.play || smc.mission.pause) return; @@ -324,6 +472,9 @@ export class MissionComp extends CCComp { this.open_Victory(null, true); } + // ======================== 清理 ======================== + + /** 清理所有英雄和技能 ECS 实体 */ private cleanComponents() { const heroEntities: ecs.Entity[] = []; ecs.query(this.heroViewMatcher).forEach(entity => { @@ -341,6 +492,7 @@ export class MissionComp extends CCComp { }); } + /** 回收所有战斗对象池(Monster / Skill / Tooltip)并清理场景节点 */ private clearBattlePools() { Monster.clearPools(); Skill.clearPools(); @@ -348,6 +500,7 @@ export class MissionComp extends CCComp { this.clearBattleSceneNodes(); } + /** 清理战斗场景中的 HERO 和 SKILL 根节点下的所有子节点 */ private clearBattleSceneNodes() { const scene = smc.map?.MapView?.scene; const layer = scene?.entityLayer?.node; @@ -366,6 +519,7 @@ export class MissionComp extends CCComp { } } + /** 获取战斗层的英雄和技能节点数量(用于性能监控) */ private getBattleLayerNodeCount() { const scene = smc.map?.MapView?.scene; const layer = scene?.entityLayer?.node; @@ -378,8 +532,11 @@ export class MissionComp extends CCComp { }; } + // ======================== 性能监控面板 ======================== + /** 性能监控相关代码 */ + /** 初始化性能监控面板:在 time_node 下创建 Label */ private initMemoryPanel() { if (!this.showMemoryPanel || !this.time_node) return; let panel = this.time_node.getChildByName("mem_panel"); @@ -397,6 +554,7 @@ export class MissionComp extends CCComp { this.memoryLabel = label; } + /** 移除性能监控面板 */ private removeMemoryPanel() { const panel = this.time_node?.getChildByName("mem_panel"); if (panel) { @@ -407,6 +565,10 @@ export class MissionComp extends CCComp { } + /** + * 更新性能监控面板内容(每 0.5 秒一次): + * 显示 堆内存 / 增长趋势 / 帧率 / 实体数量 / 对象池状态 等信息。 + */ private updateMemoryPanel(dt: number) { if (!this.showMemoryPanel || !this.memoryLabel) return; this.perfDtAcc += dt; @@ -466,7 +628,7 @@ export class MissionComp extends CCComp { - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ + /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy(); } diff --git a/assets/script/game/map/MissionHeroComp.ts b/assets/script/game/map/MissionHeroComp.ts index 30392c63..10c25b9d 100644 --- a/assets/script/game/map/MissionHeroComp.ts +++ b/assets/script/game/map/MissionHeroComp.ts @@ -1,3 +1,30 @@ +/** + * @file MissionHeroComp.ts + * @description 英雄召唤与合成管理组件(逻辑层 + 视图层) + * + * 职责: + * 1. 处理 **英雄召唤**:接收 CallHero 事件 → 通过串行队列执行召唤。 + * 2. 处理 **英雄合成**:检测同 UUID 同等级英雄是否达到合成条件 → + * 执行合成动画 → 销毁素材 → 生成高一级英雄。 + * 3. 支持 **链式合成**:合成完成后自动检测更高等级是否也满足合成条件。 + * 4. 管理英雄的出生点和掉落动画。 + * + * 关键设计: + * - summon_queue + processSummonQueue() 确保召唤请求 **串行处理**, + * 避免同帧并发导致合成判断错误。 + * - handleSingleSummon() 在每次召唤后检测是否触发合成。 + * - mergeGroupHeroes() 执行完整合成流程: + * 聚合属性 → 向出生点汇聚动画 → 爆点特效 → 生成高级英雄。 + * - merge_need_count 控制合成所需数量(2 合 1 或 3 合 1)。 + * - merge_max_lv 控制合成上限等级。 + * + * 依赖: + * - Hero(hero/Hero.ts)—— 英雄 ECS 实体类 + * - HeroAttrsComp —— 英雄属性组件 + * - HeroInfo / HeroPos / HType(heroSet)—— 英雄静态配置 + * - FightSet —— 战斗常量(MERGE_NEED / MERGE_MAX) + * - oneCom —— 一次性特效组件(控制爆点特效生命周期) + */ import { _decorator, instantiate, Prefab, v3, Vec3 } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; @@ -12,16 +39,26 @@ import { FacSet, FightSet } from "../common/config/GameSet"; import { oneCom } from "../skill/oncend"; const { ccclass } = _decorator; -/** 视图层对象 */ +/** + * MissionHeroCompComp —— 英雄召唤与合成管理器 + * + * 管理英雄的召唤请求队列、出生动画和合成系统。 + * 合成支持 2 合 1 或 3 合 1,且可链式合成至上限等级。 + */ @ccclass('MissionHeroCompComp') @ecs.register('MissionHeroComp', false) export class MissionHeroCompComp extends CCComp { - /** 英雄出生时的掉落高度,用于表现从空中落地 */ + // ======================== 常量 ======================== + + /** 英雄出生时的掉落高度(从空中落到地面的像素差) */ private static readonly HERO_DROP_HEIGHT = 260 - /** 近战起始出生 X */ + /** 近战英雄起始出生 X 坐标 */ private static readonly HERO_SPAWN_START_MELEE_X = -280 - /** 远程(含中程)起始出生 X */ + /** 远程(含中程)英雄起始出生 X 坐标 */ private static readonly HERO_SPAWN_START_RANGED_X = -280 + + // ======================== 运行时属性 ======================== + /** 预留计时器 */ timer:Timer=new Timer(2) /** 预留状态:友方是否全部死亡 */ @@ -30,29 +67,32 @@ export class MissionHeroCompComp extends CCComp { current_hero_uuid:number=0 /** 当前英雄数量缓存 */ current_hero_num:number=-1 - /** 合成规则:2 合 1 或 3 合 1 */ + /** 合成规则:需要几个同级英雄才能合成(2 或 3) */ merge_need_count:number=FightSet.MERGE_NEED - /** 允许合成的最高等级 */ + /** 允许合成的最高等级(合成产物不超过此等级) */ merge_max_lv:number=FightSet.MERGE_MAX - /** 是否正在执行一次合成流程 */ + /** 是否正在执行一次合成流程(防止并发) */ is_merging:boolean=false - /** 是否正在消费召唤队列,防止并发处理 */ + /** 是否正在消费召唤队列(防止并发) */ is_processing_queue:boolean=false - /** 召唤请求队列,保证召唤与合成串行 */ + /** 召唤请求队列:保证召唤与合成按顺序串行执行 */ summon_queue:{ uuid: number; hero_lv: number; pool_lv: number }[]=[] /** 预留英雄列表 */ heros:any=[] + + // ======================== 生命周期 ======================== + onLoad(){ - /** 节点事件监听 */ + // 注册节点级事件 this.on(GameEvent.FightReady,this.fight_ready,this) this.on(GameEvent.Zhaohuan,this.zhao_huan,this) this.on(GameEvent.MissionEnd,this.clear_heros,this) - /** 全局消息监听 */ + // 注册全局消息 oops.message.on(GameEvent.CallHero,this.call_hero,this) } onDestroy(){ - /** 清理监听,避免节点销毁后仍响应消息 */ + // 清理全部监听 oops.message.off(GameEvent.CallHero,this.call_hero,this) oops.message.off(GameEvent.FightReady,this.fight_ready,this) oops.message.off(GameEvent.Zhaohuan,this.zhao_huan,this) @@ -60,29 +100,35 @@ export class MissionHeroCompComp extends CCComp { } start() { - // this.test_call() } - /** 关卡结束时,清理全部存活英雄 */ + + // ======================== 事件处理 ======================== + + /** 关卡结束时清理全部存活英雄 ECS 实体 */ clear_heros(){ const heroes = this.getAliveHeroes(); for (let i = 0; i < heroes.length; i++) { heroes[i].destroy(); } } - /** 战斗准备阶段重置出战英雄计数 */ + + /** 战斗准备阶段:重置出战英雄计数 */ fight_ready(){ smc.vmdata.mission_data.hero_num=0 } - // protected update(dt: number): void { - - // } /** 预留:召唤事件扩展入口 */ private zhao_huan(event: string, args: any){ } - /** 召唤请求入口:归一化参数并进入串行队列 */ + /** + * 召唤请求入口: + * 从事件参数中提取 uuid / hero_lv / pool_lv,放入串行队列。 + * + * @param event 事件名 + * @param args { uuid, hero_lv, pool_lv } + */ private async call_hero(event: string, args: any){ const payload = args ?? event; const uuid = Number(payload?.uuid ?? 1001); @@ -91,7 +137,19 @@ export class MissionHeroCompComp extends CCComp { this.summon_queue.push({ uuid, hero_lv, pool_lv }); this.processSummonQueue(); } - /** 添加英雄:固定出生点上方生成,再落至落点 */ + + // ======================== 英雄生成 ======================== + + /** + * 生成一个英雄 ECS 实体: + * - 计算出生点(空中)和落点(地面)。 + * - 调用 hero.load() 初始化并播放掉落动画。 + * + * @param uuid 英雄 UUID + * @param hero_lv 英雄等级 + * @param pool_lv 卡池等级 + * @returns 创建的 Hero 实体 + */ private addHero(uuid:number=1001,hero_lv:number=1, pool_lv:number=1) { console.log("addHero uuid:",uuid) let hero = ecs.getEntity(Hero); @@ -102,6 +160,13 @@ export class MissionHeroCompComp extends CCComp { return hero; } + /** + * 计算英雄落点位置。 + * Y 坐标来自 HeroPos 配置,X 坐标根据英雄类型(近战/远程)决定。 + * + * @param uuid 英雄 UUID + * @returns 落点 Vec3 + */ private resolveHeroLandingPos(uuid: number): Vec3 { const hero_pos = 0; const baseY = HeroPos[hero_pos].pos.y; @@ -109,6 +174,11 @@ export class MissionHeroCompComp extends CCComp { return v3(startX, baseY, 0); } + /** + * 根据英雄类型决定出生 X 坐标。 + * @param uuid 英雄 UUID + * @returns 近战 or 远程的起始 X + */ private resolveSpawnStartX(uuid: number): number { const heroType = HeroInfo[uuid]?.type; return heroType === HType.Melee @@ -116,7 +186,16 @@ export class MissionHeroCompComp extends CCComp { : MissionHeroCompComp.HERO_SPAWN_START_RANGED_X; } - /** 添加合成后的新英雄,并覆盖为聚合后的属性 */ + /** + * 生成合成后的高级英雄,并覆盖为聚合后的属性。 + * + * @param uuid 英雄 UUID + * @param hero_lv 合成后等级 + * @param pool_lv 卡池等级 + * @param ap 聚合后攻击力 + * @param hp_max 聚合后最大生命值 + * @returns 实际生成的英雄等级 + */ private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number): number { const hero = this.addHero(uuid, hero_lv, pool_lv); const model = hero.get(HeroAttrsComp); @@ -128,7 +207,9 @@ export class MissionHeroCompComp extends CCComp { return model.lv; } - /** 获取当前全部存活友方英雄 */ + // ======================== 英雄查询 ======================== + + /** 获取当前全部存活友方英雄 ECS 实体列表 */ private getAliveHeroes(): Hero[] { const heroes: Hero[] = []; ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => { @@ -141,7 +222,15 @@ export class MissionHeroCompComp extends CCComp { return heroes; } - /** 挑选可参与本次合成的英雄组 */ + /** + * 从存活英雄中挑选可参与本次合成的英雄组。 + * + * @param aliveHeroes 存活英雄列表 + * @param uuid 目标英雄 UUID + * @param hero_lv 目标等级 + * @param needCount 合成需要数量 + * @returns 匹配的英雄数组(长度 = needCount 或不足) + */ private pickMergeHeroes(aliveHeroes: Hero[], uuid: number, hero_lv: number, needCount: number = 3): Hero[] { const mergeHeroes: Hero[] = []; for (let i = 0; i < aliveHeroes.length; i++) { @@ -155,7 +244,7 @@ export class MissionHeroCompComp extends CCComp { return mergeHeroes; } - /** 统计满足同 uuid、同等级的可合成英雄数量 */ + /** 统计满足同 UUID 同等级的可合成英雄数量 */ private countMergeHeroes(aliveHeroes: Hero[], uuid: number, hero_lv: number): number { let count = 0; for (let i = 0; i < aliveHeroes.length; i++) { @@ -168,17 +257,32 @@ export class MissionHeroCompComp extends CCComp { return count; } - /** 读取当前合成需要数量,仅支持 2 或 3 */ + // ======================== 合成规则 ======================== + + /** + * 读取合成所需数量(仅支持 2 或 3)。 + * 由 FightSet.MERGE_NEED 配置。 + */ private getMergeNeedCount(): number { return this.merge_need_count === 2 ? 2 : 3; } - /** 判断该等级是否还能继续向上合成 */ + /** + * 判断该等级是否还能继续向上合成。 + * @param hero_lv 当前等级 + * @returns true = 可以合成(未达上限) + */ private canMergeLevel(hero_lv: number): boolean { return hero_lv < Math.max(1, this.merge_max_lv); } - /** 串行消费召唤队列,避免同帧并发触发多次合成导致状态错乱 */ + // ======================== 召唤队列 ======================== + + /** + * 串行消费召唤队列: + * 使用 is_processing_queue 标志防止同帧多次调用。 + * 逐个取出队列中的请求并处理。 + */ private async processSummonQueue() { if (this.is_processing_queue) return; this.is_processing_queue = true; @@ -193,7 +297,16 @@ export class MissionHeroCompComp extends CCComp { } } - /** 处理单次召唤:先生成,再检测是否触发合成,再尝试链式合成 */ + /** + * 处理单次召唤: + * 1. 生成英雄。 + * 2. 检测是否满足合成条件。 + * 3. 满足则执行合成 + 链式合成。 + * + * @param uuid 英雄 UUID + * @param hero_lv 英雄等级 + * @param pool_lv 卡池等级 + */ private async handleSingleSummon(uuid: number, hero_lv: number, pool_lv: number = 1) { this.addHero(uuid, hero_lv, pool_lv); if (!this.canMergeLevel(hero_lv)) return; @@ -210,7 +323,15 @@ export class MissionHeroCompComp extends CCComp { } } - /** 将一组合成素材向出生点汇聚并销毁,全部完成后返回 */ + // ======================== 合成动画 ======================== + + /** + * 将一组合成素材英雄向出生点汇聚并销毁。 + * 所有素材动画完成后 Promise resolve。 + * + * @param mergeHeroes 合成素材英雄数组 + * @param spawnPos 汇聚目标位置 + */ private mergeDestroyAtBirth(mergeHeroes: Hero[], spawnPos: Vec3): Promise { return new Promise((resolve) => { let doneCount = 0; @@ -231,7 +352,12 @@ export class MissionHeroCompComp extends CCComp { }); } - /** 播放合成爆点特效,使用 oneCom 控制特效生命周期 */ + /** + * 播放合成爆点特效(使用 oneCom 控制生命周期)。 + * 延迟 0.4 秒后 resolve。 + * + * @param worldPos 特效播放位置 + */ private playMergeBoomFx(worldPos: Vec3): Promise { return new Promise((resolve) => { const scene = smc.map?.MapView?.scene; @@ -257,8 +383,21 @@ export class MissionHeroCompComp extends CCComp { }); } - /** 执行一次完整合成:聚合属性、销毁素材、播放特效、生成高一级英雄 */ + /** + * 执行一次完整合成流程: + * 1. 聚合素材的 AP 和 HP。 + * 2. 将素材向出生点汇聚并销毁。 + * 3. 播放爆点特效。 + * 4. 生成高一级英雄(属性为聚合值)。 + * + * @param mergeHeroes 合成素材 + * @param uuid 英雄 UUID + * @param hero_lv 素材等级 + * @param pool_lv 卡池等级 + * @returns 合成产物的实际等级 + */ private async mergeGroupHeroes(mergeHeroes: Hero[], uuid: number, hero_lv: number, pool_lv: number): Promise { + // 聚合属性 let sumAp = 0; let sumHpMax = 0; for (let i = 0; i < mergeHeroes.length; i++) { @@ -267,14 +406,23 @@ export class MissionHeroCompComp extends CCComp { sumAp += model.ap; sumHpMax += model.hp_max; } + // 计算出生点 const landingPos = this.resolveHeroLandingPos(uuid); const spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0); + // 汇聚 → 特效 → 生成 await this.mergeDestroyAtBirth(mergeHeroes, spawnPos); await this.playMergeBoomFx(spawnPos); return this.addMergedHero(uuid, Math.min(this.merge_max_lv, hero_lv + 1), pool_lv, sumAp, sumHpMax); } - /** 链式合成:当前等级合成完成后,继续尝试更高等级,直到条件不满足 */ + /** + * 链式合成:合成完成后继续检测更高等级是否也满足条件。 + * 最多循环 20 次作为安全上限。 + * + * @param uuid 英雄 UUID + * @param startLv 起始检测等级 + * @param pool_lv 卡池等级 + */ private async tryChainMerge(uuid: number, startLv: number, pool_lv: number) { let checkLv = Math.max(1, startLv); const needCount = this.getMergeNeedCount(); @@ -300,7 +448,7 @@ export class MissionHeroCompComp extends CCComp { - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件时触发,用于自定义释放逻辑 */ + /** ECS 组件移除时触发(当前不销毁节点,保留引用) */ reset() { // this.node.destroy(); } diff --git a/assets/script/game/map/MissionHomeComp.ts b/assets/script/game/map/MissionHomeComp.ts index 3f7f0e4f..c0e12fe5 100644 --- a/assets/script/game/map/MissionHomeComp.ts +++ b/assets/script/game/map/MissionHomeComp.ts @@ -1,3 +1,22 @@ +/** + * @file MissionHomeComp.ts + * @description 任务主页组件(UI 视图层) + * + * 职责: + * 1. 作为玩家进入游戏后第一个看到的界面(主菜单/大厅)。 + * 2. 提供"开始任务"按钮,触发 GameEvent.MissionStart 进入战斗。 + * 3. 提供"排行榜"按钮,打开 RanksComp 弹窗。 + * 4. 监听 MissionEnd 事件,任务结束后自动切回主页。 + * + * 关键设计: + * - start_mission() 分发 MissionStart 事件并隐藏自身节点。 + * - mission_end() 响应后重新显示主页。 + * - isWxClient() 检测是否运行在微信小游戏环境。 + * + * 依赖: + * - GameEvent.MissionStart / MissionEnd —— 游戏生命周期事件 + * - UIID.Ranks —— 排行榜弹窗 ID + */ import { _decorator, instantiate, Prefab, resources, Sprite, SpriteAtlas, UITransform ,Node} from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; @@ -10,61 +29,88 @@ import { UIID } from "../common/config/GameUIConfig"; const { ccclass, property } = _decorator; -/** 视图层对象 */ +/** + * MissionHomeComp —— 任务主页视图组件 + * + * 游戏大厅界面,提供开始战斗和查看排行的入口。 + */ @ccclass('MissionHomeComp') @ecs.register('MissionHome', false) export class MissionHomeComp extends CCComp { + /** 调试日志开关 */ debugMode: boolean = false; - + /** 主页按钮节点(预留) */ @property(Node) home_btn=null! + /** 英雄图鉴按钮节点(预留) */ @property(Node) hero_btn=null! + /** 排行榜按钮节点 */ @property(Node) rank_btn=null! - + /** 注册任务结束事件 */ protected onLoad(): void { this.on(GameEvent.MissionEnd,this.mission_end,this) } - /** 视图层逻辑代码分离演示 */ + + /** 启动时显示主页 */ start() { this.home_active() } + onEnable(){ } + update(dt:number){ } + /** + * 开始任务: + * 1. 打印日志。 + * 2. 分发 MissionStart 事件,驱动 MissionComp / MissionCardComp 初始化战斗。 + * 3. 隐藏主页节点。 + */ start_mission() { mLogger.log(this.debugMode, 'MissionHomeComp', "start_mission") oops.message.dispatchEvent(GameEvent.MissionStart, {}) this.node.active=false; } + + /** 打开排行榜弹窗 */ openRanks(){ oops.gui.open(UIID.Ranks) } + + /** 任务结束回调:重新显示主页 */ mission_end(){ mLogger.log(this.debugMode, 'MissionHomeComp', "[MissionHomeComp]=>mission_end") this.home_active() } + /** 激活主页显示:刷新数据并显示节点 */ home_active(){ this.uodate_data() this.node.active=true } + + /** 更新主页显示数据(预留) */ uodate_data(){ } + + /** + * 判断是否运行在微信小游戏环境。 + * @returns true = 微信小游戏环境 + */ isWxClient(){ return typeof wx !== 'undefined' && typeof (wx as any).getSystemInfoSync === 'function'; } - - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ + /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy(); } diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index f6bcb86b..733593b3 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -1,3 +1,33 @@ +/** + * @file MissionMonComp.ts + * @description 怪物(Monster)波次刷新管理组件(逻辑层) + * + * 职责: + * 1. 管理每一波怪物的 **生成计划**:根据 WaveSlotConfig 分配怪物到固定槽位。 + * 2. 管理 6 个固定刷怪槽位的占用状态,支持 Boss 占 2 格。 + * 3. 处理特殊插队刷怪请求(MonQueue),优先于常规刷新。 + * 4. 自动推进波次:当前波所有怪物被清除后自动进入下一波。 + * + * 关键设计: + * - 全场固定 6 个槽位(索引 0-5),每个槽位占固定 X 坐标。 + * - Boss 占 2 个连续槽位,只能放在 0、2、4 号位。 + * - slotOccupiedEids 记录每个槽位占用的怪物 ECS 实体 ID。 + * - resetSlotSpawnData(wave) 在每波开始时读取配置,分配并立即生成所有怪物。 + * - refreshSlotOccupancy() 定期检查槽位占用的实体是否仍存活,清除已死亡的占用。 + * - tryAdvanceWave() 在所有怪物死亡后自动推进波次。 + * + * 怪物属性计算公式: + * ap = floor((base_ap + stage × grow_ap) × SpawnPowerBias) + * hp = floor((base_hp + stage × grow_hp) × SpawnPowerBias) + * 其中 stage = currentWave - 1 + * + * 依赖: + * - RogueConfig —— 怪物类型、成长值、波次配置 + * - Monster(hero/Mon.ts)—— 怪物 ECS 实体类 + * - HeroInfo(heroSet)—— 怪物基础属性配置(与英雄共用配置) + * - HeroAttrsComp / MoveComp —— 怪物属性和移动组件 + * - BoxSet.GAME_LINE —— 地面基准 Y 坐标 + */ import { _decorator, v3, Vec3 } from "cc"; import { mLogger } from "../common/Logger"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; @@ -13,34 +43,63 @@ import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { MoveComp } from "../hero/MoveComp"; const { ccclass, property } = _decorator; -/** 视图层对象 */ +/** + * MissionMonCompComp —— 怪物波次刷新管理器 + * + * 每波开始时根据 WaveSlotConfig 配置分配怪物到固定槽位, + * 战斗中监控槽位状态,所有怪物消灭后自动推进到下一波。 + */ @ccclass('MissionMonCompComp') @ecs.register('MissionMonComp', false) export class MissionMonCompComp extends CCComp { + // ======================== 常量 ======================== + + /** Boss 的渲染优先级偏移(确保 Boss 始终渲染在最前) */ private static readonly BOSS_RENDER_PRIORITY = 1000000; + /** 第一个槽位的 X 坐标起点 */ private static readonly MON_SLOT_START_X = 30; + /** 槽位间的 X 间距 */ private static readonly MON_SLOT_X_INTERVAL = 60; + /** 怪物出生掉落高度 */ private static readonly MON_DROP_HEIGHT = 280; + /** 最大槽位数 */ + private static readonly MAX_SLOTS = 6; + + // ======================== 编辑器属性 ======================== + @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = false; - // 刷怪队列(用于插队生成:比如运营活动怪、技能召唤怪、剧情强制怪) - // 约定:队列里的怪会优先于常规刷新处理 + // ======================== 插队刷怪队列 ======================== + + /** + * 刷怪队列(优先于常规配置处理): + * 用于插队生成(如运营活动怪、技能召唤怪、剧情强制怪)。 + */ private MonQueue: Array<{ + /** 怪物 UUID */ uuid: number, + /** 怪物等级 */ level: number, }> = []; - private static readonly MAX_SLOTS = 6; - private slotOccupiedEids: Array = Array(6).fill(null); + // ======================== 运行时状态 ======================== - /** 全局生成顺序计数器,用于层级管理(预留) */ + /** 槽位占用状态:记录每个槽位当前占用的怪物 ECS 实体 ID,null 表示空闲 */ + private slotOccupiedEids: Array = Array(6).fill(null); + /** 全局生成顺序计数器(用于渲染层级排序) */ private globalSpawnOrder: number = 0; /** 插队刷怪处理计时器 */ private queueTimer: number = 0; + /** 当前波数 */ private currentWave: number = 0; + /** 当前波的目标怪物总数 */ private waveTargetCount: number = 0; + /** 当前波已生成的怪物数量 */ private waveSpawnedCount: number = 0; + + // ======================== 生命周期 ======================== + onLoad(){ this.on(GameEvent.FightReady,this.fight_ready,this) this.on("SpawnSpecialMonster", this.onSpawnSpecialMonster, this); @@ -48,8 +107,30 @@ export class MissionMonCompComp extends CCComp { } /** - * 接收特殊刷怪事件并入队 - * 事件数据最小结构:{ uuid, level } + * 帧更新: + * 1. 检查游戏是否运行中。 + * 2. 刷新槽位占用状态(清除已死亡怪物的占用)。 + * 3. 尝试推进波次(所有怪物清除后自动进入下一波)。 + * 4. 处理插队刷怪队列。 + */ + protected update(dt: number): void { + if(!smc.mission.play) return + if(smc.mission.pause) return + if(smc.mission.stop_mon_action) return; + if(!smc.mission.in_fight) return; + this.refreshSlotOccupancy(); + this.tryAdvanceWave(); + if(!smc.mission.in_fight) return; + if(smc.mission.stop_spawn_mon) return; + this.updateSpecialQueue(dt); + } + + // ======================== 事件处理 ======================== + + /** + * 接收特殊刷怪事件并入队。 + * @param event 事件名 + * @param args { uuid: number, level: number } */ private onSpawnSpecialMonster(event: string, args: any) { if (!args) return; @@ -58,13 +139,16 @@ export class MissionMonCompComp extends CCComp { uuid: args.uuid, level: args.level, }); - // 让队列在下一帧附近尽快消费,提升事件响应感 + // 加速队列消费 this.queueTimer = 1.0; } start() { } + /** + * 战斗准备:重置所有运行时状态并开始第一波。 + */ fight_ready(){ smc.vmdata.mission_data.mon_num=0 smc.mission.stop_spawn_mon = false @@ -78,18 +162,14 @@ export class MissionMonCompComp extends CCComp { mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System"); } - protected update(dt: number): void { - if(!smc.mission.play) return - if(smc.mission.pause) return - if(smc.mission.stop_mon_action) return; - if(!smc.mission.in_fight) return; - this.refreshSlotOccupancy(); - this.tryAdvanceWave(); - if(!smc.mission.in_fight) return; - if(smc.mission.stop_spawn_mon) return; - this.updateSpecialQueue(dt); - } + // ======================== 插队刷怪 ======================== + /** + * 处理插队刷怪队列(每 0.15 秒尝试消费一个): + * 1. 判断怪物是否为 Boss(决定占用 1 格还是 2 格)。 + * 2. 在空闲槽位中查找合适位置。 + * 3. 找到后从队列中移除并生成怪物。 + */ private updateSpecialQueue(dt: number) { if (this.MonQueue.length <= 0) return; this.queueTimer += dt; @@ -99,9 +179,10 @@ export class MissionMonCompComp extends CCComp { const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) || MonList[MonType.LongBoss].includes(item.uuid); const slotsPerMon = isBoss ? 2 : 1; + // 查找空闲槽位 let slotIndex = -1; if (slotsPerMon === 2) { - // Boss 只能放在 0, 2, 4 + // Boss 只能放在 0, 2, 4(需要连续 2 格空闲) for (const idx of [0, 2, 4]) { if (!this.slotOccupiedEids[idx] && !this.slotOccupiedEids[idx + 1]) { slotIndex = idx; @@ -109,6 +190,7 @@ export class MissionMonCompComp extends CCComp { } } } else { + // 普通怪找第一个空闲格 for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) { if (!this.slotOccupiedEids[i]) { slotIndex = i; @@ -125,11 +207,20 @@ export class MissionMonCompComp extends CCComp { } } + // ======================== 波次管理 ======================== + + /** + * 开始下一波: + * 1. 波数 +1 并更新全局数据。 + * 2. 重置槽位并根据配置生成本波所有怪物。 + * 3. 分发 NewWave 事件。 + */ private startNextWave() { this.currentWave += 1; smc.vmdata.mission_data.level = this.currentWave; this.resetSlotSpawnData(this.currentWave); + // 检查本波是否有 Boss let hasBoss = false; const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot; for (const slot of config) { @@ -145,6 +236,10 @@ export class MissionMonCompComp extends CCComp { }); } + /** + * 尝试推进波次: + * 条件:队列为空 + 所有槽位无活怪 + 全局怪物数为 0。 + */ private tryAdvanceWave() { if (this.MonQueue.length > 0) return; if (this.hasActiveSlotMonster()) return; @@ -152,16 +247,25 @@ export class MissionMonCompComp extends CCComp { this.startNextWave(); } + /** 获取当前阶段(stage = wave - 1,用于属性成长计算) */ private getCurrentStage(): number { return Math.max(0, this.currentWave - 1); } + // ======================== 随机选取 ======================== + + /** 随机选取一种成长类型 */ private getRandomUpType(): UpType { const keys = Object.keys(StageGrow).map(v => Number(v) as UpType); const index = Math.floor(Math.random() * keys.length); return keys[index] ?? UpType.AP1_HP1; } + /** + * 根据怪物类型从对应池中随机选取 UUID。 + * @param monType 怪物类型(MonType 枚举值) + * @returns 怪物 UUID + */ private getRandomUuidByType(monType: number): number { const pool = (MonList as any)[monType] || MonList[MonType.Melee]; if (!pool || pool.length === 0) return 6001; @@ -169,19 +273,38 @@ export class MissionMonCompComp extends CCComp { return pool[index]; } + /** + * 计算怪物属性成长值对。 + * Boss 在普通成长基础上叠加 StageBossGrow。 + * + * @param upType 成长类型 + * @param isBoss 是否为 Boss + * @returns [AP 成长值, HP 成长值] + */ private resolveGrowPair(upType: UpType, isBoss: boolean): [number, number] { - // 普通怪基础成长:StageGrow const grow = StageGrow[upType] || StageGrow[UpType.AP1_HP1]; if (!isBoss) return [grow[0], grow[1]]; - // Boss 额外成长:StageBossGrow(在普通成长上叠加) const bossGrow = StageBossGrow[upType] || StageBossGrow[UpType.AP1_HP1]; return [grow[0] + bossGrow[0], grow[1] + bossGrow[1]]; } + /** 获取全局刷怪强度系数 */ private getSpawnPowerBias(): number { return SpawnPowerBias; } + // ======================== 槽位管理 ======================== + + /** + * 重置槽位并生成本波所有怪物: + * 1. 读取波次配置(WaveSlotConfig 或 DefaultWaveSlot)。 + * 2. 将 Boss 和普通怪分类。 + * 3. Boss 优先分配到 0, 2, 4 号位(占 2 格)。 + * 4. 普通怪填充剩余空闲格。 + * 5. 立即实例化所有怪物。 + * + * @param wave 当前波数 + */ private resetSlotSpawnData(wave: number = 1) { const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot; this.slotOccupiedEids = Array(MissionMonCompComp.MAX_SLOTS).fill(null); @@ -189,6 +312,7 @@ export class MissionMonCompComp extends CCComp { let bosses: any[] = []; let normals: any[] = []; + // 按类型分类 for (const slot of config) { const slotsPerMon = slot.slotsPerMon || 1; const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss; @@ -207,7 +331,7 @@ export class MissionMonCompComp extends CCComp { this.waveTargetCount = bosses.length + normals.length; this.waveSpawnedCount = 0; - // Boss 只能放在 0, 2, 4 (即 1, 3, 5 号位) + // Boss 优先分配(只能放在 0, 2, 4) let bossAllowedIndices = [0, 2, 4]; let assignedSlots = new Array(MissionMonCompComp.MAX_SLOTS).fill(null); @@ -216,7 +340,7 @@ export class MissionMonCompComp extends CCComp { for (const idx of bossAllowedIndices) { if (!assignedSlots[idx] && !assignedSlots[idx + 1]) { assignedSlots[idx] = boss; - assignedSlots[idx + 1] = "occupied"; // 占位 + assignedSlots[idx + 1] = "occupied"; // 占位标记 placed = true; break; } @@ -226,6 +350,7 @@ export class MissionMonCompComp extends CCComp { } } + // 普通怪填充剩余空位 for (const normal of normals) { let placed = false; for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) { @@ -249,6 +374,7 @@ export class MissionMonCompComp extends CCComp { } } + /** 检查是否有任何槽位仍被活着的怪物占用 */ private hasActiveSlotMonster() { for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) { if (this.slotOccupiedEids[i]) return true; @@ -256,6 +382,11 @@ export class MissionMonCompComp extends CCComp { return false; } + /** + * 刷新所有槽位的占用状态: + * 检查每个占用的 ECS 实体是否仍存在且 HP > 0, + * 已失效的清除占用标记。 + */ private refreshSlotOccupancy() { for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) { const eid = this.slotOccupiedEids[i]; @@ -272,6 +403,23 @@ export class MissionMonCompComp extends CCComp { } } + // ======================== 怪物生成 ======================== + + /** + * 在指定槽位生成一个怪物: + * 1. 计算出生坐标(多格时居中)。 + * 2. 创建 Monster ECS 实体。 + * 3. 标记槽位占用。 + * 4. 设置渲染排序(Boss 优先级更高)。 + * 5. 根据阶段和成长类型计算最终 AP / HP。 + * + * @param slotIndex 槽位索引(0-5) + * @param uuid 怪物 UUID + * @param isBoss 是否为 Boss + * @param upType 属性成长类型 + * @param monLv 怪物等级 + * @param slotsPerMon 占用格数 + */ private addMonsterBySlot( slotIndex: number, uuid: number = 1001, @@ -282,7 +430,7 @@ export class MissionMonCompComp extends CCComp { ) { let mon = ecs.getEntity(Monster); let scale = -1; - // 如果占用了多个格子,出生坐标居中处理 + // 多格占用时居中出生点 const centerXOffset = (slotsPerMon - 1) * MissionMonCompComp.MON_SLOT_X_INTERVAL / 2; const spawnX = MissionMonCompComp.MON_SLOT_START_X + slotIndex * MissionMonCompComp.MON_SLOT_X_INTERVAL + centerXOffset; const landingY = BoxSet.GAME_LINE + (isBoss ? 6 : 0); @@ -291,18 +439,22 @@ export class MissionMonCompComp extends CCComp { mon.load(spawnPos, scale, uuid, isBoss, landingY, monLv); - // 将它占用的所有格子都标记为这个 eid + // 标记槽位占用 for (let j = 0; j < slotsPerMon; j++) { if (slotIndex + j < MissionMonCompComp.MAX_SLOTS) { this.slotOccupiedEids[slotIndex + j] = mon.eid; } } + + // 设置渲染排序 const move = mon.get(MoveComp); if (move) { move.spawnOrder = isBoss ? MissionMonCompComp.BOSS_RENDER_PRIORITY + this.globalSpawnOrder : this.globalSpawnOrder; } + + // 计算最终属性 const model = mon.get(HeroAttrsComp); const base = HeroInfo[uuid]; if (!model || !base) return; @@ -314,7 +466,7 @@ export class MissionMonCompComp extends CCComp { model.hp = model.hp_max; } - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ + /** ECS 组件移除时触发(当前不销毁节点) */ reset() { // this.node.destroy(); } diff --git a/assets/script/game/map/RanksComp.ts b/assets/script/game/map/RanksComp.ts index c65d78ca..381e21e5 100644 --- a/assets/script/game/map/RanksComp.ts +++ b/assets/script/game/map/RanksComp.ts @@ -1,3 +1,20 @@ +/** + * @file RanksComp.ts + * @description 排行榜弹窗组件(UI 视图层) + * + * 职责: + * 1. 展示排行榜界面,包含 Top1~Top3 特殊位和通用列表区域。 + * 2. 提供关闭排行榜弹窗的按钮回调。 + * + * 关键设计: + * - top1_node / top2_node / top3_node 用于展示前三名玩家的特殊样式。 + * - lists_node 为滚动列表的容器节点。 + * - list_prefab / melist_prefab 分别为普通排名项和"我的排名"项的预制体。 + * - 当前 onLoad / onAdded 未实现具体逻辑,预留后期接入排行数据。 + * + * 依赖: + * - UIID.Ranks —— 在 oops.gui 系统中注册的弹窗 ID + */ import { _decorator, Animation, AnimationClip, Button, Event, Label, Node, NodeEventType, Sprite, resources, Prefab } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; @@ -10,37 +27,54 @@ import { mLogger } from "../common/Logger"; const {property, ccclass } = _decorator; -/** 视图层对象 */ +/** + * RanksComp —— 排行榜视图组件 + * + * 通过 oops.gui.open(UIID.Ranks) 打开。 + * 展示 Top3 + 通用列表 + 我的排名。 + */ @ccclass('RanksComp') @ecs.register('RanksComp', false) export class RanksComp extends CCComp { + /** 第 1 名展示节点 */ @property(Node) top1_node=null! + /** 第 2 名展示节点 */ @property(Node) top2_node=null! + /** 第 3 名展示节点 */ @property(Node) top3_node=null! + /** 排名列表容器节点 */ @property(Node) lists_node=null! + /** 普通排名项预制体 */ @property(Prefab) list_prefab=null! + /** "我的排名"项预制体 */ @property(Prefab) melist_prefab=null! + /** 预留:加载排行数据 */ onLoad() { } + + /** 预留:弹窗打开时接收参数 */ onAdded(args: any) { } + onDestroy() { } + + /** 关闭排行榜弹窗 */ closeRanks(){ oops.gui.remove(UIID.Ranks) } - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ + /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy(); diff --git a/assets/script/game/map/RogueConfig.ts b/assets/script/game/map/RogueConfig.ts index 46c5f39a..0ec0d9b2 100644 --- a/assets/script/game/map/RogueConfig.ts +++ b/assets/script/game/map/RogueConfig.ts @@ -1,74 +1,142 @@ +/** + * @file RogueConfig.ts + * @description Roguelike 关卡配置 —— 怪物类型、成长值、波次刷怪方案 + * + * 职责: + * 1. 定义怪物属性成长类型(UpType)和每种类型的 AP / HP 每阶段成长值。 + * 2. 定义怪物分类(MonType)和对应的怪物 UUID 池。 + * 3. 定义每一波(Wave)的怪物占位配置(WaveSlotConfig / DefaultWaveSlot)。 + * 4. 提供全局刷怪强度偏差系数(SpawnPowerBias)。 + * + * 设计说明: + * - 战场固定 6 个占位槽(索引 0-5)。 + * - Boss 占 2 个槽位,只能放在 0、2、4 号位(确保有连续 2 格)。 + * - MissionMonComp 在每波开始时读取本配置,决定刷怪组合。 + * + * 注意: + * - StageGrow / StageBossGrow 的索引 [0] 为 AP 成长,[1] 为 HP 成长。 + * - 实际计算公式:base_stat + stage × grow_value × SpawnPowerBias。 + */ +// ======================== 属性成长类型枚举 ======================== + +/** 怪物属性成长类型 */ export enum UpType { - AP1_HP1 = 0, //平衡 - HP2 = 1, //强hp - AP2 = 2 //强ap + /** 平衡型:AP 和 HP 均匀成长 */ + AP1_HP1 = 0, + /** 强 HP 型:以血量为主成长 */ + HP2 = 1, + /** 强 AP 型:以攻击力为主成长 */ + AP2 = 2 } -// 普通关卡成长值:第一项为攻击力成长,第二项为血量成长 + +// ======================== 普通怪成长配置 ======================== + +/** + * 普通怪每阶段成长值:[AP 成长, HP 成长] + * 每经历一波(stage +1),怪物的 base_ap / base_hp 增加对应值。 + */ export const StageGrow = { - [UpType.AP1_HP1]: [4,10], // 平衡型:攻4 血10 - [UpType.HP2]: [2,20], // 强HP型:攻2 血20 - [UpType.AP2]: [8,0], // 强AP型:攻8 血0 + [UpType.AP1_HP1]: [4,10], // 平衡型:每波攻击+4 血量+10 + [UpType.HP2]: [2,20], // 强HP型:每波攻击+2 血量+20 + [UpType.AP2]: [8,0], // 强AP型:每波攻击+8 血量+0 } -// Boss关卡成长值:同上,数值更高 +// ======================== Boss 额外成长配置 ======================== + +/** + * Boss 在普通怪成长基础上的 **额外** 成长值:[AP 增量, HP 增量] + * 实际 Boss 成长 = StageGrow + StageBossGrow。 + */ export const StageBossGrow = { - [UpType.AP1_HP1]: [3,16], // 平衡型:攻3 血16 - [UpType.HP2]: [1,24], // 强HP型:攻1 血24 - [UpType.AP2]: [10,4], // 强AP型:攻10 血4 + [UpType.AP1_HP1]: [3,16], // 平衡型 Boss:额外攻击+3 血量+16 + [UpType.HP2]: [1,24], // 强HP型 Boss:额外攻击+1 血量+24 + [UpType.AP2]: [10,4], // 强AP型 Boss:额外攻击+10 血量+4 } +// ======================== 怪物类型定义 ======================== + +/** 怪物类型常量(用于 WaveSlotConfig 中引用) */ export const MonType = { - Melee: 0, // 近战高功 - Long: 1, // 高速贴近 - Support: 2, // 支持怪 - MeleeBoss: 3, // boss怪 - LongBoss: 4, // boss怪 + /** 近战普通怪 */ + Melee: 0, + /** 远程普通怪 */ + Long: 1, + /** 辅助怪(支持类) */ + Support: 2, + /** 近战 Boss */ + MeleeBoss: 3, + /** 远程 Boss */ + LongBoss: 4, } + +// ======================== 怪物 UUID 池 ======================== + +/** 各类型对应的怪物 UUID 列表(随机抽取) */ export const MonList = { - [MonType.Melee]: [6001,6002,6003], // 近战怪 - [MonType.Long]: [6004,6005], // 远程怪 - [MonType.Support]: [6005], // 辅助怪 - [MonType.MeleeBoss]:[6006,6015], // 近战boss - [MonType.LongBoss]:[6104], // 远程boss + [MonType.Melee]: [6001,6002,6003], // 近战怪池 + [MonType.Long]: [6004,6005], // 远程怪池 + [MonType.Support]: [6005], // 辅助怪池 + [MonType.MeleeBoss]:[6006,6015], // 近战 Boss 池 + [MonType.LongBoss]:[6104], // 远程 Boss 池 } -/* -*** 全局刷怪强度配置,后期根据玩家强度动态调整 -*/ + +// ======================== 全局刷怪强度系数 ======================== + +/** + * 全局刷怪强度偏差系数。 + * 所有怪物的最终 AP / HP 会乘以此系数。 + * 后期可根据玩家强度动态调整以实现自适应难度。 + */ export const SpawnPowerBias = 1 + +// ======================== 波次占位配置数据结构 ======================== + +/** 单条波次占位配置 */ export interface IWaveSlot { - type: number; // 对应 MonType - count: number; // 占位数量 - slotsPerMon?: number; // 每个怪占用几个位置,默认 1 + /** 怪物类型(参考 MonType) */ + type: number; + /** 该类型的怪物数量 */ + count: number; + /** (可选)每个怪物占用几个槽位,默认 1;大型 Boss 设为 2 */ + slotsPerMon?: number; } // ========================================================================================= // 【每波怪物占位与刷怪配置说明】 -// 1. 字段说明: +// +// 字段说明: // - type: 怪物类型 (参考 MonType,如近战 0,远程 1,Boss 3 等)。 // - count: 该类型的怪在场上同时存在几个。 -// - slotsPerMon: (可选) 单个怪物体积占用几个占位坑,默认为 1。如果是大型 Boss 可设为 2,它会跨占位降落。 -// -// 【注意】: -// 全场固定 6 个槽位(索引 0-5)。 -// Boss 固定占用 2 个位置,且只能出现在 1、3、5 号位(对应索引 0, 2, 4)。 -// 每波怪物总槽位占用不能超过 6。不再支持排队刷怪。 +// - slotsPerMon: (可选) 单个怪物体积占用几个占位坑,默认为 1。 +// 大型 Boss 设为 2,它会跨占位降落。 +// +// 【规则约束】: +// - 全场固定 6 个槽位(索引 0-5)。 +// - Boss 固定占用 2 个位置,且只能出现在 1、3、5 号位(对应索引 0, 2, 4)。 +// - 每波怪物总槽位占用不能超过 6。 // ========================================================================================= + +/** 各波次的怪物占位配置(key = 波次编号) */ export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = { + /** 第 1 波:3 近战 + 3 远程 */ 1: [ { type: MonType.Melee, count: 3 }, { type: MonType.Long, count: 3 } ], + /** 第 2 波:2 近战 + 2 远程 + 2 辅助 */ 2: [ { type: MonType.Melee, count: 2 }, { type: MonType.Long, count: 2 }, { type: MonType.Support, count: 2 } ], + /** 第 3 波:2 近战 + 1 近战Boss(占2格) + 2 远程 */ 3: [ { type: MonType.Melee, count: 2 }, { type: MonType.MeleeBoss, count: 1, slotsPerMon: 2 }, { type: MonType.Long, count: 2 } ], + /** 第 4 波:2 近战 + 2 远程 + 1 远程Boss(占2格) */ 4: [ { type: MonType.Melee, count: 2 }, { type: MonType.Long, count: 2 }, @@ -76,7 +144,11 @@ export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = { ], } -// 默认占位配置 (如果在 WaveSlotConfig 中找不到波次,则使用此配置) +/** + * 默认占位配置: + * 当 WaveSlotConfig 中找不到对应波次时使用此兜底配置。 + * 默认 3 近战 + 3 远程。 + */ export const DefaultWaveSlot: IWaveSlot[] = [ { type: MonType.Melee, count: 3 }, { type: MonType.Long, count: 3 } diff --git a/assets/script/game/map/SIconComp.ts b/assets/script/game/map/SIconComp.ts index 7e273dee..1bdc10b1 100644 --- a/assets/script/game/map/SIconComp.ts +++ b/assets/script/game/map/SIconComp.ts @@ -1,3 +1,18 @@ +/** + * @file SIconComp.ts + * @description 技能图标组件(UI 视图层) + * + * 职责: + * 1. 根据技能 UUID 显示对应的技能图标。 + * 2. 从预加载的资源中获取 SpriteFrame 并赋给子节点 "icon" 的 Sprite。 + * + * 使用方式: + * 挂载在需要显示技能图标的节点上,外部调用 update_data(s_uuid) 传入技能 UUID。 + * + * 依赖: + * - SkillSet(SkillSet.ts)—— 技能静态配置(含 icon 字段) + * - oops.res —— 已预加载的资源管理器 + */ import { _decorator, Sprite, SpriteFrame } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; @@ -6,22 +21,29 @@ import { oops } from "db://oops-framework/core/Oops"; const { ccclass, property } = _decorator; -/** 视图层对象 */ +/** + * SIconCompComp —— 技能图标视图组件 + * + * 从 SkillSet 查询技能配置获取 icon 路径, + * 通过 oops.res.get 获取已预加载的 SpriteFrame 并显示。 + */ @ccclass('SIconCompComp') @ecs.register('SIconComp', false) export class SIconCompComp extends CCComp { - /** 视图层逻辑代码分离演示 */ start() { - // var entity = this.ent as ecs.Entity; // ecs.Entity 可转为当前模块的具体实体对象 - // this.on(ModuleEvent.Cmd, this.onHandler, this); } + /** + * 更新技能图标显示。 + * @param s_uuid 技能的唯一标识 UUID + */ update_data(s_uuid:number){ let skill_data = SkillSet[s_uuid] + // 从预加载的资源中获取对应图标的 SpriteFrame 并赋值 this.node.getChildByName("icon").getComponent(Sprite).spriteFrame = oops.res.get("game/heros/cards/"+skill_data.icon, SpriteFrame) } - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ + /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy(); } diff --git a/assets/script/game/map/SkillBoxComp.ts b/assets/script/game/map/SkillBoxComp.ts index ed286db8..a73a18f5 100644 --- a/assets/script/game/map/SkillBoxComp.ts +++ b/assets/script/game/map/SkillBoxComp.ts @@ -1,3 +1,30 @@ +/** + * @file SkillBoxComp.ts + * @description 单个技能卡效果控制组件(UI 视图层 + 逻辑层) + * + * 职责: + * 1. 表示一张已使用的技能卡在战场上的 **可视化实体**。 + * 2. 管理技能的 **触发逻辑**:即时触发 vs 定时触发(战斗中按间隔触发)。 + * 3. 显示技能图标和剩余触发次数。 + * 4. 触发结束后自动销毁。 + * + * 关键设计: + * - is_instant=true(即时技能):init 时立即触发一次,播放后延迟销毁。 + * - is_instant=false(持续技能):战斗中每隔 trigger_interval 秒触发一次, + * 共触发 trigger_times 次后销毁。 + * - 新一波(NewWave)时如果持续技能的次数已用完则销毁。 + * - 销毁时通过 GameEvent.RemoveSkillBox 通知 MissSkillsComp 回收槽位。 + * + * 触发技能的方式: + * - 通过 GameEvent.TriggerSkill 事件,将技能 UUID、卡牌等级、 + * 触发位置等信息分发给技能系统。 + * + * 依赖: + * - CardPoolList(CardSet)—— 查询技能卡的触发配置(t_times / t_inv / is_inst) + * - SkillSet —— 技能静态配置(icon 字段) + * - GameEvent —— 各类游戏事件 + * - smc.mission —— 游戏运行状态 + */ import { mLogger } from "../common/Logger"; import { _decorator, Node, Prefab, Sprite, Label, Vec3, resources, SpriteAtlas } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; @@ -9,27 +36,53 @@ import { GameEvent } from "../common/config/GameEvent"; import { smc } from "../common/SingletonModuleComp"; const { ccclass, property } = _decorator; -/** 视图层对象 */ +/** + * SkillBoxComp —— 单个技能卡效果视图 + 逻辑组件 + * + * 由 MissSkillsComp.addSkill() 实例化并初始化。 + * 在战场上以图标 + 剩余次数的形式呈现。 + */ @ccclass('SkillBoxComp') @ecs.register('SkillBoxComp', false) export class SkillBoxComp extends CCComp { + /** 调试日志开关 */ private debugMode: boolean = true; + + /** 技能图标节点 */ @property({type: Node}) private icon_node:Node= null; + + /** 剩余次数标签 */ @property(Label) private info_label: Label = null; + // ======================== 技能配置 ======================== + + /** 技能 UUID */ private s_uuid: number = 0; + /** 卡牌等级 */ private card_lv: number = 1; + /** 是否为即时技能(true=使用后立即触发,false=战斗中定时触发) */ private is_instant: boolean = true; + /** 总触发次数 */ private trigger_times: number = 1; + /** 触发间隔(秒,仅持续技能有效) */ private trigger_interval: number = 0; + // ======================== 运行时状态 ======================== + + /** 已触发次数 */ private current_trigger_times: number = 0; + /** 当前计时器(秒) */ private timer: number = 0; + /** 是否处于战斗中(仅战斗中持续技能才计时) */ private in_combat: boolean = false; + /** 是否已初始化 */ private initialized: boolean = false; + // ======================== 生命周期 ======================== + + /** 注册战斗开始、任务结束、新一波等事件 */ onLoad() { oops.message.on(GameEvent.FightStart, this.onFightStart, this); oops.message.on(GameEvent.MissionEnd, this.onMissionEnd, this); @@ -37,19 +90,30 @@ export class SkillBoxComp extends CCComp { oops.message.on(GameEvent.NewWave, this.onNewWaveGlobal, this); } + /** 销毁时移除所有事件监听并通知槽位管理器回收 */ onDestroy() { oops.message.off(GameEvent.FightStart, this.onFightStart, this); oops.message.off(GameEvent.MissionEnd, this.onMissionEnd, this); this.node.off(GameEvent.NewWave, this.onNewWave, this); oops.message.off(GameEvent.NewWave, this.onNewWaveGlobal, this); + // 通知 MissSkillsComp 回收该节点占用的槽位 oops.message.dispatchEvent(GameEvent.RemoveSkillBox, this.node); } + /** + * 初始化技能卡效果: + * 1. 从 CardPoolList 查询技能卡的触发配置。 + * 2. 更新 UI 显示(图标 + 次数)。 + * 3. 即时技能立即触发一次;若次数已满则延迟销毁。 + * + * @param uuid 技能 UUID + * @param card_lv 技能卡等级 + */ init(uuid: number, card_lv: number) { - // this.node.parent=smc.map.MapView.scene.entityLayer!.node! this.s_uuid = uuid; this.card_lv = card_lv; + // 查询触发配置 const config = CardPoolList.find(c => c.uuid === uuid); if (config) { this.is_instant = config.is_inst ?? true; @@ -64,18 +128,25 @@ export class SkillBoxComp extends CCComp { this.updateUI(); if (this.is_instant) { - // 即时起效:立即触发 + // 即时技能:立即触发 this.triggerSkill(); this.current_trigger_times++; if (this.current_trigger_times >= this.trigger_times) { + // 次数已满 → 延迟 1 秒后销毁(保留短暂视觉反馈) this.scheduleOnce(() => { this.node.destroy(); - }, 1.0); // 稍微延迟销毁,保证表现 + }, 1.0); } } } + /** + * 更新 UI: + * - 图标:从 uicons 图集获取。 + * - 剩余次数:持续技能显示剩余数字,即时技能不显示。 + */ updateUI() { + // 加载技能图标 if (this.icon_node) { const iconId = SkillSet[this.s_uuid]?.icon || `${this.s_uuid}`; resources.load("gui/uicons", SpriteAtlas, (err, atlas) => { @@ -88,6 +159,7 @@ export class SkillBoxComp extends CCComp { }); } + // 更新剩余次数标签 if (this.info_label) { if (!this.is_instant) { const remain = Math.max(0, this.trigger_times - this.current_trigger_times); @@ -98,40 +170,56 @@ export class SkillBoxComp extends CCComp { } } + // ======================== 战斗状态事件 ======================== + + /** 战斗开始:标记进入战斗状态,持续技能开始计时 */ private onFightStart() { if (!this.initialized) return; this.in_combat = true; if (!this.is_instant) { - // 战斗开始时,计时归0,重新计时 - this.timer = 0; + this.timer = 0; // 重置计时器 } } + /** 节点级新一波事件处理 */ private onNewWave() { this.handleNewWave(); } + /** 全局级新一波事件处理 */ private onNewWaveGlobal() { this.handleNewWave(); } + /** + * 新一波:退出战斗状态。 + * 持续技能:若总次数已用完则销毁。 + */ private handleNewWave() { if (!this.initialized) return; this.in_combat = false; if (!this.is_instant) { - // 每回合不再重置次数,由全局次数进行控制 if (this.current_trigger_times >= this.trigger_times) { this.node.destroy(); } } } + /** 任务结束:强制销毁 */ private onMissionEnd() { this.node.destroy(); } + // ======================== 帧更新 ======================== + + /** + * 每帧更新(仅对持续技能生效): + * - 累加计时器,达到 trigger_interval 时触发一次技能。 + * - 触发后重置计时器并更新 UI。 + * - 总次数用完后延迟销毁。 + */ update(dt: number) { if (!this.initialized || !this.in_combat || this.is_instant) return; if (!smc.mission.play || smc.mission.pause) return; @@ -139,14 +227,13 @@ export class SkillBoxComp extends CCComp { if (this.current_trigger_times < this.trigger_times) { this.timer += dt; if (this.timer >= this.trigger_interval) { - this.timer = 0; // 触发后重新计时 + this.timer = 0; this.triggerSkill(); this.current_trigger_times++; - this.updateUI(); // 触发后更新界面显示的剩余次数 + this.updateUI(); - // 如果在战斗中就达到触发次数上限,则可以在此回合战斗结束或者立即销毁 + // 次数用完 → 延迟销毁 if (this.current_trigger_times >= this.trigger_times) { - // 可以选择直接销毁,不等到下一回合 this.scheduleOnce(() => { if (this.node.isValid) this.node.destroy(); }, 0.5); @@ -155,10 +242,14 @@ export class SkillBoxComp extends CCComp { } } + // ======================== 技能触发 ======================== + + /** + * 触发技能效果: + * - 计算触发位置(节点局部坐标 + 父节点偏移)。 + * - 通过 GameEvent.TriggerSkill 事件将技能数据分发给技能系统。 + */ private triggerSkill() { - // 获取自身在父节点下的局部坐标 - // UI 的局部坐标在 2D 相机中和实际的游戏逻辑坐标存在偏移关系, - // 可以结合自身局部坐标做一次偏移,此处直接读取自身的 localPosition 加上父节点的偏移 let targetPos = new Vec3(); const localPos = this.node.position; const parentPos = this.node.parent ? this.node.parent.position : new Vec3(0, 0, 0); @@ -166,13 +257,13 @@ export class SkillBoxComp extends CCComp { oops.message.dispatchEvent(GameEvent.TriggerSkill, { s_uuid: this.s_uuid, - isCardSkill: true, + isCardSkill: true, // 标记为卡牌技能(区别于英雄自身技能) card_lv: this.card_lv, targetPos: targetPos }); } - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ + /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy(); } diff --git a/assets/script/game/map/TopComp.ts b/assets/script/game/map/TopComp.ts index c90afe63..dc3b0832 100644 --- a/assets/script/game/map/TopComp.ts +++ b/assets/script/game/map/TopComp.ts @@ -1,3 +1,19 @@ +/** + * @file TopComp.ts + * @description 顶部 UI 栏组件 + * + * 职责: + * 1. 监听 GOLD_UPDATE 事件,当金币数量变化时播放数字弹跳动画。 + * 2. 该组件挂载在顶部 HUD 节点上,管理金币图标和数值的视觉反馈。 + * + * 关键设计: + * - 使用 tween 对金币数字节点执行 scale 弹跳动画(放大 → 回缩), + * 提供视觉提示告知玩家金币变化。 + * + * 依赖: + * - GameEvent.GOLD_UPDATE —— 全局金币更新事件 + * - oops.message —— 全局事件总线 + */ import { _decorator, Component, Label, Node, tween, v3 } from 'cc'; import { GameEvent } from '../common/config/GameEvent'; import { smc } from '../common/SingletonModuleComp'; @@ -5,14 +21,30 @@ import { BoxSet, NumberFormatter } from '../common/config/GameSet'; import { oops } from 'db://oops-framework/core/Oops'; const { ccclass, property } = _decorator; +/** + * topComp —— 顶部 UI 栏 + * + * 监听金币变化事件,对金币数值标签播放弹跳反馈动画。 + */ @ccclass('topComp') export class topComp extends Component { + /** 注册金币更新事件监听 */ protected onLoad(): void { oops.message.on(GameEvent.GOLD_UPDATE,this.onGoldUpdate,this); } + start() { } + + /** + * 金币更新事件回调: + * 对 bar/gold/num 节点播放 scale 弹跳动画, + * 先放大到 1.2 再回缩到 1.0。 + * + * @param event 事件名 + * @param data 事件数据(当前未使用) + */ onGoldUpdate(event:string,data:any){ tween(this.node.getChildByName("bar").getChildByName("gold").getChildByName("num").getComponent(Label).node) .to(0.1,{scale:v3(1.2,1.2,1)}) @@ -20,6 +52,7 @@ export class topComp extends Component { .start() } + /** 销毁时移除事件监听 */ onDestroy(){ oops.message.off(GameEvent.GOLD_UPDATE,this.onGoldUpdate,this); } diff --git a/assets/script/game/map/VictoryComp.ts b/assets/script/game/map/VictoryComp.ts index 3730f8cd..df431752 100644 --- a/assets/script/game/map/VictoryComp.ts +++ b/assets/script/game/map/VictoryComp.ts @@ -1,3 +1,23 @@ +/** + * @file VictoryComp.ts + * @description 战斗结算弹窗组件(UI 视图层) + * + * 职责: + * 1. 在战斗结束时弹出,展示结算信息(得分、奖励)。 + * 2. 根据传入参数判断是否可复活,切换"下一步"或"复活"按钮。 + * 3. 计算单局总分并存储到 smc.vmdata.scores.score。 + * 4. 提供"重新开始"和"退出"两个操作入口。 + * + * 关键设计: + * - onAdded(args) 接收战斗结果参数(victory / rewards / game_data / can_revive)。 + * - calculateTotalScore() 根据 ScoreWeights 配置加权计算各项得分。 + * - restart() 和 victory_end() 通过分发 MissionEnd / MissionStart 事件驱动游戏状态切换。 + * + * 依赖: + * - smc.vmdata.scores —— 全局战斗统计数据 + * - ScoreWeights(ScoreSet)—— 得分权重配置 + * - GameEvent.MissionEnd / MissionStart —— 游戏生命周期事件 + */ import { _decorator, instantiate, Label ,Prefab,Node} from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; @@ -14,31 +34,48 @@ import { mLogger } from "../common/Logger"; const { ccclass, property } = _decorator; -/** 视图层对象 */ +/** + * VictoryComp —— 战斗结算弹窗视图组件 + * + * 通过 oops.gui.open(UIID.Victory, args) 打开。 + * 展示战斗结果,计算总分,并提供重开 / 退出操作。 + */ @ccclass('VictoryComp') @ecs.register('Victory', false) export class VictoryComp extends CCComp { + /** 调试日志开关 */ debugMode: boolean = false; + /** 奖励等级(预留) */ reward_lv:number=1 + /** 奖励数量(预留) */ reward_num:number=2 + /** 掉落奖励列表 */ rewards:any[]=[] + /** 累计游戏数据(经验 / 金币 / 钻石) */ game_data:any={ exp:0, gold:0, diamond:0 } - // 复活相关配置 - private canRevive: boolean = false; // 是否可以复活(由MissionComp传入) - // private reviveCount: number = 0; // 已复活次数 - 移交 MissionComp 管理 + // ======================== 复活相关 ======================== - /** 视图层逻辑代码分离演示 */ + /** 是否可以复活(由 MissionComp 传入,取决于剩余复活次数) */ + private canRevive: boolean = false; + + /** 加载时隐藏 loading 遮罩 */ protected onLoad(): void { this.node.getChildByName("loading").active=false - // this.canRevive = true; - // this.reviveCount = 0; } + /** + * 弹窗打开时的回调:接收战斗结果参数。 + * + * @param args.victory 是否胜利(当前仅用于标识) + * @param args.rewards 掉落奖励列表 + * @param args.game_data 累计数据 { exp, gold, diamond } + * @param args.can_revive 是否可复活 + */ onAdded(args: any) { this.node.getChildByName("loading").active=false mLogger.log(this.debugMode, 'VictoryComp', "[VictoryComp] onAdded",args) @@ -46,25 +83,24 @@ export class VictoryComp extends CCComp { this.game_data=args.game_data } - // // 接收复活参数 - // if (args.can_revive !== undefined) { - // this.canRevive = args.can_revive; - // } else { - // this.canRevive = false; // 默认不可复活 - // } - + // 根据是否可复活决定按钮显示 this.node.getChildByName("btns").getChildByName("next").active=!args.can_revive - // this.node.getChildByName("btns").getChildByName("alive").active=args.can_revive - // 只有在不能复活(彻底结算)时才计算总分 - // if (!this.canRevive) { - - // } + // 计算总分 this.calculateTotalScore(); } + // ======================== 得分计算 ======================== + /** - * 计算单局总分并更新到 smc.scores.score + * 计算单局总分并更新到 smc.vmdata.scores.score。 + * + * 得分维度: + * 1. 战斗行为分 —— 暴击次数、连击触发、闪避、格挡、眩晕、冻结 + * 2. 伤害转化分 —— 总伤害、平均伤害、反伤、暴击伤害 + * 3. 击杀得分 —— 近战击杀、远程击杀、精英击杀、Boss 击杀 + * 4. 生存得分 —— 治疗量、吸血量 + * 5. 资源得分 —— 经验获取、金币获取 */ private calculateTotalScore() { const s = smc.vmdata.scores; @@ -98,31 +134,44 @@ export class VictoryComp extends CCComp { totalScore += s.exp_total * ScoreWeights.EXP_FACTOR; totalScore += s.gold_total * ScoreWeights.GOLD_FACTOR; - // 更新总分(向下取整) + // 取整并存储 s.score = Math.floor(totalScore); mLogger.log(this.debugMode, 'VictoryComp', `[VictoryComp] 结算总分: ${s.score}`); } + // ======================== 操作入口 ======================== + /** 退出战斗:清理数据 → 触发任务结束 → 关闭弹窗 */ victory_end(){ this.clear_data() oops.message.dispatchEvent(GameEvent.MissionEnd) oops.gui.removeByNode(this.node) } + + /** 清理运行时数据:解除暂停标志 */ clear_data(){ smc.mission.pause=false } - //看广告双倍 + + /** 看广告双倍奖励(预留) */ watch_ad(){ return true } + + /** 双倍奖励发放(预留) */ double_reward(){ } + /** + * 重新开始: + * 1. 清理数据。 + * 2. 触发 MissionEnd 事件重置状态。 + * 3. 显示 loading 遮罩,延迟 0.5 秒后触发 MissionStart。 + * 4. 关闭弹窗。 + */ restart(){ this.clear_data() - // 确保游戏结束事件被触发,以便重置状态 oops.message.dispatchEvent(GameEvent.MissionEnd) this.node.getChildByName("loading").active=true this.scheduleOnce(()=>{ @@ -131,6 +180,8 @@ export class VictoryComp extends CCComp { oops.gui.removeByNode(this.node) },0.5) } + + /** 物品展示回调(预留) */ item_show(e:any,val:any){ mLogger.log(this.debugMode, 'VictoryComp', "item_show",val) } @@ -138,7 +189,8 @@ export class VictoryComp extends CCComp { protected onDestroy(): void { mLogger.log(this.debugMode, 'VictoryComp', "释放胜利界面"); } - /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ + + /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy() } diff --git a/assets/script/game/map/move.ts b/assets/script/game/map/move.ts index 29253780..b5bb35b6 100644 --- a/assets/script/game/map/move.ts +++ b/assets/script/game/map/move.ts @@ -1,36 +1,86 @@ +/** + * @file move.ts + * @description 地图背景横向循环滚动组件 + * + * 职责: + * 1. 使挂载的节点在 [minX, maxX] 范围内沿水平方向持续移动。 + * 2. 到达边界后停止并发出事件,等待下一轮触发(配合另一个同类组件实现无缝循环)。 + * 3. 支持动态改变移动方向。 + * + * 关键设计: + * - sc(direction):1 = 从左到右,-1 = 从右到左。 + * - 采用事件驱动的接力机制: + * * 当一个 move 组件到达终点后,发出 MAP_MOVE_END_LEFT 或 MAP_MOVE_END_RIGHT, + * 同方向的另一个 move 组件收到后从起点开始移动,形成无缝衔接。 + * - isMove 控制当前是否处于移动中。 + * + * 使用场景: + * 通常挂载在地图背景层的两个重叠节点上,通过交替移动实现无限滚动效果。 + * + * 依赖: + * - GameEvent.MAP_MOVE_END_LEFT —— 左方向移动结束事件 + * - GameEvent.MAP_MOVE_END_RIGHT —— 右方向移动结束事件 + */ import { _decorator, CCBoolean, CCInteger, Component, Node } from 'cc'; import { oops } from 'db://oops-framework/core/Oops'; import { GameEvent } from '../common/config/GameEvent'; const { ccclass, property } = _decorator; +/** + * move —— 背景循环滚动组件 + * + * 挂载在背景节点上,配合另一个同方向实例实现无缝横向滚动。 + */ @ccclass('move') export class move extends Component { + /** 移动速度(像素/秒) */ @property({ type: CCInteger }) speed: number = 2; + /** 水平移动右边界 */ @property({ type: CCInteger }) maxX: number = 640; + /** 水平移动左边界 */ @property({ type: CCInteger }) minX: number = -640; + /** + * 移动方向: + * 1 = 从左到右 + * -1 = 从右到左 + */ @property({ type: CCInteger }) - sc: number = 1; // 1: 从左到右, -1: 从右到左 + sc: number = 1; + + /** 当前是否正在移动 */ @property isMove:boolean=false + + /** 注册地图移动结束事件 */ protected onLoad(): void { oops.message.on(GameEvent.MAP_MOVE_END_LEFT, this.onMapMoveEndLeft, this); oops.message.on(GameEvent.MAP_MOVE_END_RIGHT, this.onMapMoveEndRight, this); } + start() { - // 根据移动方向设置初始位置 } + + /** + * 收到"左方向移动结束"事件: + * 仅从右到左(sc==-1)的实例响应 → 重置到起点并开始移动。 + */ onMapMoveEndLeft() { if(this.sc==-1){ this.isMove=true this.setInitialPosition() } } + + /** + * 收到"右方向移动结束"事件: + * 仅从左到右(sc==1)的实例响应 → 重置到起点并开始移动。 + */ onMapMoveEndRight() { if(this.sc==1){ this.isMove=true @@ -38,47 +88,53 @@ export class move extends Component { } } + /** 销毁时移除事件监听 */ onDestroy() { oops.message.off(GameEvent.MAP_MOVE_END_LEFT, this.onMapMoveEndLeft, this); oops.message.off(GameEvent.MAP_MOVE_END_RIGHT, this.onMapMoveEndRight, this); } /** - * 根据移动方向设置初始位置 + * 根据移动方向设置初始位置: + * - 从左到右:起点为 minX + * - 从右到左:起点为 maxX */ setInitialPosition() { if (this.sc > 0) { - // 从左到右移动,起点为 minX this.node.setPosition(this.minX, this.node.position.y); } else if (this.sc < 0) { - // 从右到左移动,起点为 maxX this.node.setPosition(this.maxX, this.node.position.y); } } + /** + * 帧更新:按速度和方向更新节点位置。 + * 移动中才执行,到达边界后检查是否需要停止。 + */ update(dt: number) { - // 更新位置 if(this.isMove){ const newX = this.node.position.x + dt * this.speed * this.sc; this.node.setPosition(newX, this.node.position.y); - // 检查边界并重置位置 this.checkBoundaries(); } } /** - * 检查边界并重置位置 + * 检查边界并处理到达终点: + * - 从左到右:到达 maxX → 停止移动,发出 MAP_MOVE_END_LEFT + * - 从右到左:到达 minX → 停止移动,发出 MAP_MOVE_END_RIGHT + * + * 注意:事件名意味着"该方向的移动已结束", + * 接收端的另一个实例会据此开始下一段移动。 */ checkBoundaries() { if (this.sc > 0) { - // 从左到右移动,到达右边界后回到左边界 if (this.node.position.x >= this.maxX) { this.node.setPosition(this.minX, this.node.position.y); this.isMove=false oops.message.dispatchEvent(GameEvent.MAP_MOVE_END_LEFT) } } else if (this.sc < 0) { - // 从右到左移动,到达左边界后回到右边界 if (this.node.position.x <= this.minX) { this.node.setPosition(this.maxX, this.node.position.y); this.isMove=false @@ -88,8 +144,8 @@ export class move extends Component { } /** - * 动态改变移动方向 - * @param direction 1: 从左到右, -1: 从右到左 + * 动态改变移动方向并重置到对应起点。 + * @param direction 1 = 从左到右,-1 = 从右到左 */ changeDirection(direction: number) { this.sc = direction;