From e17ffeb2fd12021579f9ce1b00026304f5585f9a Mon Sep 17 00:00:00 2001 From: pan Date: Wed, 22 Jul 2026 12:54:29 +0800 Subject: [PATCH] feat(map): add equip card box system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增了场上装备卡槽管理系统,包含以下内容: 1. 新增EBox实体类,负责装备卡节点的创建、初始化和销毁流程 2. 新增EquipBoxComp组件,实现单个装备卡的视图显示和逻辑控制 3. 新增MissEquipComp组件,管理10个装备卡槽位的分配、回收和重排 4. 完整实现了装备卡的触发逻辑,支持即时、定时、事件型等多种技能类型 --- assets/script/game/map/EBox.ts | 52 ++ assets/script/game/map/EBox.ts.meta | 9 + assets/script/game/map/EquipBoxComp.ts | 565 +++++++++++++++++++ assets/script/game/map/EquipBoxComp.ts.meta | 9 + assets/script/game/map/MissEquipComp.ts | 191 +++++++ assets/script/game/map/MissEquipComp.ts.meta | 9 + 6 files changed, 835 insertions(+) create mode 100644 assets/script/game/map/EBox.ts create mode 100644 assets/script/game/map/EBox.ts.meta create mode 100644 assets/script/game/map/EquipBoxComp.ts create mode 100644 assets/script/game/map/EquipBoxComp.ts.meta create mode 100644 assets/script/game/map/MissEquipComp.ts create mode 100644 assets/script/game/map/MissEquipComp.ts.meta diff --git a/assets/script/game/map/EBox.ts b/assets/script/game/map/EBox.ts new file mode 100644 index 00000000..e0f86582 --- /dev/null +++ b/assets/script/game/map/EBox.ts @@ -0,0 +1,52 @@ +import { instantiate, Prefab, Vec3, Node } from "cc"; +import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; +import { smc } from "../common/SingletonModuleComp"; +import { EquipBoxComp } from "./EquipBoxComp"; + +/** EBox 实体:负责技能卡效果节点创建、初始化与销毁流程 */ +@ecs.register(`EBox`) +export class EBox extends ecs.Entity { + /** 技能盒子视图和逻辑组件引用 */ + SkillBox!: EquipBoxComp; + + protected init() { + // 如果有纯逻辑数据组件可以在这里 addComponents + } + + /** 销毁实体并释放视图节点,防止残留 */ + destroy(): void { + const view = this.get(EquipBoxComp); + if (view && view.node && view.node.isValid) { + view.node.destroy(); + } + + this.remove(EquipBoxComp); + super.destroy(); + } + + /** + * 加载并初始化技能盒子 + * 1) 创建节点并挂到 SKILL 层 + * 2) 初始化表现与属性数据 + */ + load(uuid: number, card_lv: number, pos: Vec3, prefab: Prefab): Node { + let node = instantiate(prefab); + let scene = smc.map.MapView.scene; + + // 统一挂到实体显示层 SKILL 节点下 + let parent = scene.entityLayer!.node!.getChildByName("SKILL"); + if (parent) { + node.parent = parent; + } + node.setPosition(pos); + + // 获取并注册组件 + let eboxComp = node.getComponent(EquipBoxComp) || node.addComponent(EquipBoxComp); + this.add(eboxComp); + + // 初始化业务逻辑 + eboxComp.init(uuid, card_lv); + + return node; + } +} diff --git a/assets/script/game/map/EBox.ts.meta b/assets/script/game/map/EBox.ts.meta new file mode 100644 index 00000000..db9d1945 --- /dev/null +++ b/assets/script/game/map/EBox.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "dc85b25e-2b65-464b-8091-07ae23d04bb3", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/game/map/EquipBoxComp.ts b/assets/script/game/map/EquipBoxComp.ts new file mode 100644 index 00000000..326a1b9c --- /dev/null +++ b/assets/script/game/map/EquipBoxComp.ts @@ -0,0 +1,565 @@ +/** + * @file EquipBoxComp.ts + * @description 单个技能卡效果控制组件(UI 视图层 + 逻辑层) + * + * 职责: + * 1. 表示一张已使用的技能卡在战场上的 **可视化实体**。 + * 2. 按 trigger_type 类型化分发触发逻辑(即时 / 定时 / 驻场 / 事件型)。 + * 3. 显示技能图标和剩余触发次数。 + * 4. 触发结束后自动销毁。 + * + * 触发类型(CardTriggerType): + * - Instant (1):init 时立即触发一次(按 t_times 控制次数,跨波次 NewWave 时再次触发) + * - Interval (2):监听 FightStart → update 帧驱动按 t_inv 间隔重复触发(按 t_times 控制每波次数) + * - Field (3):被动生效,不主动施法(实际由 FieldSkillSet 处理) + * - FightStart (4):监听 FightStart 事件,按 trigger_limit 全局累计上限 + * - FightEnd (5):监听 FightEnd 事件(每波结束派发),按 trigger_limit 全局累计上限 + * - HeroDead (6):监听 HeroDead 事件(仅英雄阵营派发,怪物死亡不触发) + * - HeroCall (7):监听 MasterCalled(主角/技能召唤)+ ReviveSuccess(复活) + * + * 关键设计: + * - 事件型(4-7)统一走 onEventTrigger 入口,仅作触发信号,不读取 payload + * - 触发上限:Instant/Interval 按 t_times(每波内),事件型按 trigger_limit(全局) + * - 跨波次:keep_waves 控制存活;事件型 trigger_count 不随波次重置 + * - 销毁时通过 GameEvent.RemoveSkillBox 通知 MissSkillsComp 回收槽位 + * + * 依赖: + * - CardPoolList / CardTriggerType(CardSet)—— 查询技能卡的触发配置 + * - SkillSet —— 技能静态配置(icon 字段) + * - GameEvent —— 各类游戏事件 + * - smc.mission —— 游戏运行状态 + */ +import { mLogger } from "../common/Logger"; +import { _decorator, Node, Prefab, Sprite, Label, Vec3, resources, SpriteAtlas, tween, v3, Tween, NodeEventType } from "cc"; +import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; +import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; +import { CardPoolList, CardTriggerType } from "../common/config/CardSet"; +import { SkillSet, SkillOverrides, FieldSkillSet } from "../common/config/SkillSet"; +import { oops } from "db://oops-framework/core/Oops"; +import { GameEvent } from "../common/config/GameEvent"; +import { smc } from "../common/SingletonModuleComp"; +import { UIID } from "../common/config/GameUIConfig"; +const { ccclass, property } = _decorator; + +/** + * EquipBoxComp —— 单个技能卡效果视图 + 逻辑组件 + * + * 由 MissSkillsComp.addSkill() 实例化并初始化。 + * 在战场上以图标 + 剩余次数的形式呈现。 + */ +@ccclass('EquipBoxComp') +@ecs.register('EquipBoxComp', false) +export class EquipBoxComp extends CCComp { + /** 调试日志开关 */ + private debugMode: boolean = true; + + /** 技能图标节点 */ + @property({ type: Node }) + private icon_node: Node = null; + + /** 技能图标节点:下面有 green blue purple red yellow 5个不同颜色节点*/ + @property({ type: Node }) + private bg_node: Node = null; + + /** 剩余次数标签 */ + @property(Label) + private info_label: Label = null; + + /** cd计时遮罩 */ + @property({ type: Node }) + private cd_mask: Node = 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; + /** 维持的波次数(-1表示直到战斗结束,0表示不跨波次,>0表示维持的具体波次数) */ + private keep_waves: number = 0; + /** 技能覆写参数(自定义伤害、Buff等) */ + private overrides?: SkillOverrides; + /** 驻场技能 UUID 列表 */ + public field: number[] = []; + /** 卡牌自定义图标ID(优先级最高,未定义则按 trigger_type 自动取) */ + private card_icon?: string; + + // ======================== 触发类型化扩展 ======================== + + /** 触发类型(默认即时,保持向后兼容) */ + private trigger_type: CardTriggerType = CardTriggerType.Instant; + /** 事件型触发的全局次数上限(Infinity 表示无上限) */ + private trigger_limit: number = Infinity; + /** 事件型已触发次数 */ + private trigger_count: number = 0; + + // ======================== 运行时状态 ======================== + + /** 已触发次数 */ + private current_trigger_times: number = 0; + /** 当前计时器(秒) */ + private timer: number = 0; + /** 是否处于战斗中(仅战斗中持续技能才计时) */ + private in_combat: boolean = false; + /** 是否已初始化 */ + private initialized: boolean = false; + + // ======================== 生命周期 ======================== + + /** + * 注册全局事件: + * - MissionEnd:所有类型都需要监听,任务结束时强制销毁 + * - NewWave:处理 keep_waves 跨波次逻辑(所有类型统一) + * - 其它触发事件由 registerTrigger 按 trigger_type 动态注册 + */ + onLoad() { + oops.message.on(GameEvent.MissionEnd, this.onMissionEnd, this); + this.node.on(GameEvent.NewWave, this.onNewWave, this); + oops.message.on(GameEvent.NewWave, this.onNewWaveGlobal, this); + this.node.on(NodeEventType.TOUCH_END, this.onNodeClicked, this); + } + + /** 销毁时移除所有事件监听并通知槽位管理器回收 */ + onDestroy() { + super.onDestroy(); + // 统一 off 所有可能订阅的事件(即使未订阅也无副作用) + // 注意:FightStart 可能由两种回调订阅(Interval→onFightStart / FightStart触发型→onEventTrigger),都需要 off + oops.message.off(GameEvent.MissionEnd, this.onMissionEnd, this); + oops.message.off(GameEvent.FightStart, this.onFightStart, this); + oops.message.off(GameEvent.FightStart, this.onEventTrigger, this); + oops.message.off(GameEvent.FightEnd, this.onEventTrigger, this); + oops.message.off(GameEvent.HeroDead, this.onEventTrigger, this); + oops.message.off(GameEvent.MasterCalled, this.onEventTrigger, this); + oops.message.off(GameEvent.ReviveSuccess, this.onEventTrigger, this); + if (this.node && this.node.isValid) { + this.node.off(GameEvent.NewWave, this.onNewWave, this); + this.node.off(NodeEventType.TOUCH_END, this.onNodeClicked, this); + } + oops.message.off(GameEvent.NewWave, this.onNewWaveGlobal, this); + // 通知 MissEquipComp 回收该节点占用的槽位 + oops.message.dispatchEvent(GameEvent.RemoveEquipBox, this.node); + } + + private onNodeClicked() { + if (!this.initialized) return; + oops.audio.playEffect("music/button"); + // 点击时弹出 HInfoComp,传入卡牌 UUID 和等级以启用预览模式 + const config = CardPoolList.find(c => c.uuid === this.s_uuid || c.skill === this.s_uuid); + const cardUuid = config ? config.uuid : this.s_uuid; + + oops.gui.remove(UIID.HInfo); + oops.gui.open(UIID.HInfo, { heroUuid: cardUuid, heroLv: this.card_lv, poolLv: config?.pool_lv ?? 1, isSkillCard: true }); + } + + /** + * 初始化技能卡效果: + * 1. 从 CardPoolList 查询技能卡的触发配置(含 trigger_type)。 + * 2. 更新 UI 显示(图标 + 次数)。 + * 3. 按 trigger_type 注册对应事件监听并执行首次触发。 + * + * @param uuid 卡牌 UUID + * @param card_lv 技能卡等级 + */ + init(uuid: number, card_lv: number) { + this.card_lv = card_lv; + + // 查询触发配置 + const config = CardPoolList.find(c => c.uuid === uuid); + if (config) { + this.s_uuid = config.skill ?? uuid; // 获取实际的技能 UUID + this.is_instant = config.is_inst ?? true; + this.trigger_times = config.t_times ?? 1; + this.trigger_interval = config.t_inv ?? 0; + this.keep_waves = config.keep_waves ?? 0; + this.overrides = config.overrides; + this.field = config.field || []; + this.card_icon = config.icon; // 保存卡牌自定义图标(优先级最高) + // 读取触发类型与上限(兜底默认值,避免 undefined) + this.trigger_type = config.trigger_type ?? CardTriggerType.Instant; + this.trigger_limit = config.trigger_limit ?? Infinity; + } else { + this.s_uuid = uuid; + } + + this.current_trigger_times = 0; + this.trigger_count = 0; + this.timer = 0; + this.initialized = true; + + this.updateUI(); + + // 按 trigger_type 注册事件监听 + 执行首次触发 + this.registerTrigger(); + } + + /** + * 按 trigger_type 注册对应事件监听: + * - Instant: init 时立即触发一次(保持旧行为) + * - Interval: 监听 FightStart,进入战斗后由 update 帧驱动计时 + * - Field: 不主动施法(实际生效由 FieldSkillSet 处理) + * - FightStart: 监听 FightStart 事件 + * - FightEnd: 监听 FightEnd 事件 + * - HeroDead: 监听 HeroDead 事件(已在派发处做阵营过滤) + * - HeroCall: 监听 MasterCalled(主角/技能召唤)+ ReviveSuccess(复活) + * + * 注意:MasterCalled 各派发点 payload 不一致,onEventTrigger 仅作触发信号使用。 + */ + private registerTrigger(): void { + switch (this.trigger_type) { + case CardTriggerType.Instant: + // 即时技能:立即触发一次 + this.onEventTrigger(); + break; + + case CardTriggerType.Interval: + // 定时循环:监听 FightStart 进入战斗后启动计时 + oops.message.on(GameEvent.FightStart, this.onFightStart, this); + break; + + case CardTriggerType.Field: + // 驻场光环:不主动施法,由 FieldSkillSet 处理 + break; + + case CardTriggerType.FightStart: + oops.message.on(GameEvent.FightStart, this.onEventTrigger, this); + break; + + case CardTriggerType.FightEnd: + oops.message.on(GameEvent.FightEnd, this.onEventTrigger, this); + break; + + case CardTriggerType.HeroDead: + oops.message.on(GameEvent.HeroDead, this.onEventTrigger, this); + break; + + case CardTriggerType.HeroCall: + // 同时监听召唤和复活两类英雄上场事件 + oops.message.on(GameEvent.MasterCalled, this.onEventTrigger, this); + oops.message.on(GameEvent.ReviveSuccess, this.onEventTrigger, this); + break; + + default: + mLogger.warn(true, 'EquipBoxComp', `[registerTrigger] unknown trigger_type: ${this.trigger_type}, fallback to Instant`); + this.onEventTrigger(); + break; + } + } + + /** + * 事件型触发的统一入口: + * - Instant 类型:按 trigger_times 上限判定,复用 current_trigger_times 跟踪 + * (保持与原 is_instant 行为一致,且 NewWave 中也用 current_trigger_times) + * - 事件型(FightStart/FightEnd/HeroDead/HeroCall):按 trigger_limit 上限判定,使用 trigger_count 跟踪 + * + * 注意:本方法不读取事件 payload,仅作触发信号使用(避免 MasterCalled 不同 payload 字段引发的兼容问题)。 + */ + private onEventTrigger(): void { + if (!this.initialized) return; + + if (this.trigger_type === CardTriggerType.Instant) { + // 即时触发:上限由 trigger_times 控制(保持旧行为) + if (this.current_trigger_times >= this.trigger_times) { + this.destroySelf(); + return; + } + this.triggerSkill(); + this.current_trigger_times++; + this.updateUI(); + + // 单次触发 + 不跨波次维持 → 延迟销毁(保留短暂视觉反馈) + if (this.keep_waves === 0 && this.current_trigger_times >= this.trigger_times) { + this.scheduleOnce(() => this.destroySelf(), 1.0); + } + return; + } + + // 事件型:上限由 trigger_limit 控制(全局累计,跨波次不重置) + if (this.trigger_count >= this.trigger_limit) { + this.destroySelf(); + return; + } + + this.triggerSkill(); + this.trigger_count++; + this.updateUI(); + } + + /** + * 统一的节点销毁封装: + * 优先通过 ECS 实体销毁;否则直接销毁节点。 + */ + private destroySelf(): void { + if (this.ent) { + (this.ent as ecs.Entity).destroy(); + } else if (this.node && this.node.isValid) { + this.node.destroy(); + } + } + + /** + * 根据技能等级切换 bg_node 下的颜色子节点显示。 + * 等级与颜色对应关系(技能卡 wave 档位): + * card_lv 1 (wave 1) → green + * card_lv 2 (wave 5) → blue + * card_lv 3 (wave 8) → purple + */ + private updateBgNode() { + if (!this.bg_node) return; + const lvToColor: Record = { + 1: "green", + 2: "blue", + 3: "purple", + }; + const targetColor = lvToColor[this.card_lv]; + this.bg_node.children.forEach(child => { + child.active = (child.name === targetColor); + }); + } + + /** + * 更新 UI: + * - 背景:根据技能等级切换对应颜色子节点显示 + * - 图标:从 uicons 图集获取 + * - 剩余次数标签: + * * Interval / 事件型:显示剩余次数(按各自上限计算) + * * Instant / Field:不显示 + * - CD 遮罩:仅 Interval 类型展示冷却进度 + */ + updateUI() { + // 按技能等级切换背景颜色节点(与 getLvColor 等级配色一致) + this.updateBgNode(); + + // 加载技能图标 + if (this.icon_node) { + // 优先使用卡牌自定义 icon;未定义则按 trigger_type 自动取 + let iconId: string | undefined = this.card_icon; + if (!iconId) { + if (this.trigger_type === CardTriggerType.Interval) { + iconId = this.s_uuid ? SkillSet[this.s_uuid]?.icon : undefined; + } else if (this.trigger_type === CardTriggerType.Field) { + const fieldUuid = this.field?.[0]; + iconId = fieldUuid ? FieldSkillSet[fieldUuid]?.icon : undefined; + } + } + if (this.icon_node && this.icon_node.isValid) { + let sprite = this.icon_node.getComponent(Sprite) || this.icon_node.addComponent(Sprite); + if (smc.uiconsAtlas && iconId) { + const frame = smc.uiconsAtlas.getSpriteFrame(iconId); + sprite.spriteFrame = frame || null; // 取不到时清空,避免残留 + } else { + sprite.spriteFrame = null; + } + } + } + + // 是否需要展示剩余次数 + const showRemainCount = + this.trigger_type === CardTriggerType.Interval || + this.trigger_type === CardTriggerType.FightStart || + this.trigger_type === CardTriggerType.FightEnd || + this.trigger_type === CardTriggerType.HeroDead || + this.trigger_type === CardTriggerType.HeroCall; + + if (this.info_label) { + if (showRemainCount) { + // 事件型按 trigger_limit;Interval 按 t_times + const isEvent = + this.trigger_type === CardTriggerType.FightStart || + this.trigger_type === CardTriggerType.FightEnd || + this.trigger_type === CardTriggerType.HeroDead || + this.trigger_type === CardTriggerType.HeroCall; + const used = isEvent ? this.trigger_count : this.current_trigger_times; + const total = isEvent ? this.trigger_limit : this.trigger_times; + if (isEvent && !isFinite(total)) { + // 无上限:显示已触发次数 + this.info_label.string = `${used}`; + } else { + const remain = Math.max(0, Math.floor(total) - used); + this.info_label.string = `${remain}`; + } + } else { + this.info_label.string = ""; + } + } + + // 初始化或重置 CD 遮罩表现(仅 Interval 类型有冷却进度) + if (this.cd_mask && this.cd_mask.isValid) { + let sprite = this.cd_mask.getComponent(Sprite); + if (sprite) { + if (this.trigger_type === CardTriggerType.Interval && this.trigger_interval > 0) { + sprite.fillRange = Math.max(0, 1 - (this.timer / this.trigger_interval)); + } else { + sprite.fillRange = 0; // 非冷却类型直接归 0 + } + } + } + } + + // ======================== 战斗状态事件 ======================== + + /** + * 战斗开始回调: + * - 仅 Interval 类型在 registerTrigger 中订阅此事件 + * - 标记进入战斗状态,启动计时器(实际触发由 update 帧驱动) + * + * 注意:FightStart 触发型(CardTriggerType.FightStart)的事件回调是 onEventTrigger,不是本方法。 + */ + private onFightStart() { + if (!this.initialized) return; + this.in_combat = true; + this.timer = 0; // 重置计时器 + } + + /** 节点级新一波事件处理 */ + private onNewWave() { + this.handleNewWave(); + } + + /** 全局级新一波事件处理 */ + private onNewWaveGlobal() { + this.handleNewWave(); + } + + /** + * 新一波:退出战斗状态。 + * 处理维持波次逻辑:递减剩余波次,或者重置触发次数。 + * + * 各类型在新一波的行为: + * - Instant/Interval/FightStart/FightEnd:按 keep_waves 决定维持/销毁,并在新一波开始时重置本地计数 + * - Field:被动生效,跟随 keep_waves 决定存活 + * - HeroDead/HeroCall:跨波次触发的事件型,trigger_count(全局)不重置,仅 keep_waves 控制存活 + */ + private handleNewWave() { + if (!this.initialized) return; + this.in_combat = false; + + // 事件型触发(HeroDead / HeroCall):trigger_count 全局累计,不随波次重置 + const isGlobalEventType = + this.trigger_type === CardTriggerType.HeroDead || + this.trigger_type === CardTriggerType.HeroCall; + + if (this.keep_waves !== 0) { + if (this.keep_waves > 0) { + this.keep_waves--; + if (this.keep_waves <= 0) { + this.destroySelf(); + return; + } + } + // 跨波次维持:重置本地计数与计时器(事件型 trigger_count 不重置) + if (!isGlobalEventType) { + this.current_trigger_times = 0; + this.trigger_count = 0; + } + this.timer = 0; + + // 即时/事件型触发一次(保持旧行为:Instant 在新一波开始立即触发一次) + if (this.trigger_type === CardTriggerType.Instant) { + this.triggerSkill(); + this.current_trigger_times++; + } + this.updateUI(); + } else { + // 不跨波次维持:达到上限即销毁 + // - Interval / Instant:按 t_times 判定 + // - 事件型:按 trigger_limit 判定 + const reachedLimit = isGlobalEventType + ? this.trigger_count >= this.trigger_limit + : this.current_trigger_times >= this.trigger_times; + + if (reachedLimit) { + this.destroySelf(); + } + } + } + + /** 任务结束:强制销毁 */ + private onMissionEnd() { + this.destroySelf(); + } + + // ======================== 帧更新 ======================== + + /** + * 每帧更新: + * - 仅 Interval 类型走帧驱动计时逻辑(其它类型提前 return) + * - 累加计时器,达到 trigger_interval 时触发一次技能 + * - 触发后重置计时器并更新 UI + * - 总次数用完后延迟销毁 + */ + update(dt: number) { + // 收窄:仅 Interval 类型走帧驱动 + if (this.trigger_type !== CardTriggerType.Interval) return; + if (!this.initialized || !this.in_combat) return; + if (!smc.mission.play || smc.mission.pause) return; + + if (this.current_trigger_times < this.trigger_times) { + this.timer += dt; + + // 更新 CD 遮罩 (fillRange 从 1 降到 0) + if (this.cd_mask && this.cd_mask.isValid && this.trigger_interval > 0) { + let sprite = this.cd_mask.getComponent(Sprite); + if (sprite) { + sprite.fillRange = Math.max(0, 1 - (this.timer / this.trigger_interval)); + } + } + + if (this.timer >= this.trigger_interval) { + this.timer = 0; + this.triggerSkill(); + this.current_trigger_times++; + this.updateUI(); + + // 次数用完且不跨波次维持 → 延迟销毁 + if (this.keep_waves === 0 && this.current_trigger_times >= this.trigger_times) { + this.scheduleOnce(() => this.destroySelf(), 0.5); + } + } + } + } + + // ======================== 技能触发 ======================== + + /** + * 触发技能效果: + * - 计算触发位置(节点局部坐标 + 父节点偏移)。 + * - 通过 GameEvent.TriggerSkill 事件将技能数据分发给技能系统。 + */ + private triggerSkill() { + let targetPos = new Vec3(); + const localPos = this.node.position; + const parentPos = this.node.parent ? this.node.parent.position : new Vec3(0, 0, 0); + targetPos.set(parentPos.x + localPos.x, parentPos.y + localPos.y, 0); + + // 播放技能释放缓动动画:图标放大再缩回 + if (this.node && this.node.isValid) { + // 停止之前的缓动,防止快速重复触发导致缩放异常 + Tween.stopAllByTarget(this.node); + this.node.scale = v3(1, 1, 1); + tween(this.node) + .to(0.1, { scale: v3(1.3, 1.3, 1.3) }) + .to(0.15, { scale: v3(1, 1, 1) }) + .start(); + } + + oops.message.dispatchEvent(GameEvent.TriggerSkill, { + s_uuid: this.s_uuid, + isCardSkill: true, // 标记为卡牌技能(区别于英雄自身技能) + card_lv: this.card_lv, + targetPos: targetPos, + overrides: this.overrides + }); + } + + /** ECS 组件移除时销毁节点 */ + reset() { + if (this.node && this.node.isValid) { + this.node.destroy(); + } + } +} diff --git a/assets/script/game/map/EquipBoxComp.ts.meta b/assets/script/game/map/EquipBoxComp.ts.meta new file mode 100644 index 00000000..b9f8a028 --- /dev/null +++ b/assets/script/game/map/EquipBoxComp.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "f689f41b-0ba2-4d72-ad29-adc5818f7e13", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/game/map/MissEquipComp.ts b/assets/script/game/map/MissEquipComp.ts new file mode 100644 index 00000000..04d4d544 --- /dev/null +++ b/assets/script/game/map/MissEquipComp.ts @@ -0,0 +1,191 @@ + +/** + * @file MissEquipComp.ts + * @description 场上装备卡槽位管理器组件(UI 视图层) + * + * 职责: + * 1. 管理场上已使用的 **装备卡** 的可视化槽位(最多 10 个)。 + * 2. 监听 UseEquipCard 事件,当玩家使用装备卡时实例化 EquipBoxComp 并放入空闲槽位。 + * 3. 监听 RemoveEquipBox 事件,当装备生效完毕后回收槽位并重新排列。 + * + * 关键设计: + * - slots 数组预定义了 10 个固定坐标位(2 行 × 5 列),s + * 每个槽位记录是否占用及对应节点引用。 + * - 当某个 EquipBox 销毁时,触发 rearrangeSlots 将剩余节点 + * 紧凑地重排到前置槽位,避免视觉空洞。 + * - EquipBox 的实例化使用 equip_box Prefab,在编辑器中绑定。 + * + * 依赖: + * - EquipBoxComp(EquipBoxComp.ts)—— 单个装备卡的效果控制组件 + * - GameEvent.UseEquipCard —— 装备卡使用事件 + * - GameEvent.RemoveEquipBox —— 装备卡移除事件 + * - smc.map.MapView.scene.entityLayer —— 装备节点的父容器(EQUIP 节点) + */ +import { mLogger } from "../common/Logger"; +import { _decorator, Node, Prefab, instantiate, 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"; +import { EBox } from "./EBox"; +import { oops } from "db://oops-framework/core/Oops"; +import { GameEvent } from "../common/config/GameEvent"; +const { ccclass, property } = _decorator; + +/** 技能槽位数据结构 */ +interface SkillBoxSlot { + /** 该槽位的固定 X 坐标 */ + x: number; + /** 该槽位的固定 Y 坐标 */ + y: number; + /** 是否已被占用 */ + used: boolean; + /** 占用该槽位的节点引用 */ + node: Node | null; +} + +/** + * MissEquipComp —— 场上装备卡槽位管理器 + * + * 在战斗场景中管理已激活的装备卡显示位置。 + * 2 行 × 5 列 = 10 个槽位,不足时提示已满。 + */ +@ccclass('MissEquipComp') +@ecs.register('MissEquipComp', false) +export class MissEquipComp extends CCComp { + /** 调试日志开关 */ + private debugMode: boolean = true; + + /** 技能卡 Prefab(在编辑器中赋值) */ + @property({ type: Prefab }) + private equip_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: -310, y: 300, used: false, node: null }, + { x: -220, y: 300, used: false, node: null }, + { x: -130, y: 300, used: false, node: null }, + { x: -40, y: 300, used: false, node: null }, + { x: 50, y: 300, used: false, node: null }, + { x: -310, y: 400, used: false, node: null }, + { x: -220, y: 400, used: false, node: null }, + { x: -130, y: 400, used: false, node: null }, + { x: -40, y: 400, used: false, node: null }, + { x: 50, y: 400, used: false, node: null }, + ]; + + /** 注册事件监听 */ + onLoad() { + oops.message.on(GameEvent.UseEquipCard, this.onUseEquipCard, this); + oops.message.on(GameEvent.RemoveEquipBox, this.onRemoveEquipBox, this); + } + + /** 移除事件监听 */ + onDestroy() { + super.onDestroy(); + oops.message.off(GameEvent.UseEquipCard, this.onUseEquipCard, this); + oops.message.off(GameEvent.RemoveEquipBox, this.onRemoveEquipBox, this); + } + + /** + * 处理装备卡移除事件: + * 1. 在 slots 中找到对应节点并释放。 + * 2. 调用 rearrangeSlots 紧凑重排。 + * + * @param event 事件名 + * @param args 要移除的节点引用 + */ + private onRemoveEquipBox(event: string, args: any) { + const node = args as Node; + let removed = false; + for (let i = 0; i < this.slots.length; i++) { + if (this.slots[i].node === node) { + this.slots[i].used = false; + this.slots[i].node = null; + removed = true; + break; + } + } + if (removed) { + this.rearrangeSlots(); + } + } + + /** + * 紧凑重排:将所有有效节点按顺序移到前置槽位。 + * 确保视觉上不会出现中间空洞。 + */ + 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) { + validNodes.push(this.slots[i].node); + } + 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; + this.slots[i].node = validNodes[i]; + validNodes[i].setPosition(new Vec3(this.slots[i].x, this.slots[i].y, 0)); + } + } + } + + /** + * 处理使用装备卡事件:提取 uuid 和 card_lv 后调用 addEquip。 + * @param event 事件名 + * @param args 卡牌数据(含 uuid、card_lv) + */ + private onUseEquipCard(event: string, args: any) { + const payload = args ?? event; + const uuid = Number(payload?.uuid ?? 0); + const card_lv = Math.max(1, Math.floor(Number(payload?.card_lv ?? 1))); + if (!uuid) return; + this.addSkill(uuid, card_lv); + } + + start() { + + } + + /** + * 在场上添加一个装备卡: + * 1. 在 slots 中查找空闲位。 + * 2. 实例化 equip_box Prefab 并放置在空闲位坐标。 + * 3. 获取或添加 EquipBoxComp 并初始化。 + * + * @param uuid 装备 UUID + * @param card_lv 装备卡等级 + */ + addSkill(uuid: number, card_lv: number) { + if (!this.equip_box) { + mLogger.error(this.debugMode, "MissEquipComp", "equip_box prefab not set"); + return; + } + + // 查找空闲槽位 + const emptyIndex = this.slots.findIndex(slot => !slot.used); + if (emptyIndex === -1) { + mLogger.warn(this.debugMode, "MissEquipComp", "equip_box slots are full"); + return; + } + + // 使用 ECS 实体创建装备节点 + let ebox = ecs.getEntity(EBox); + let pos = new Vec3(this.slots[emptyIndex].x, this.slots[emptyIndex].y, 0); + let node = ebox.load(uuid, card_lv, pos, this.equip_box); + + this.slots[emptyIndex].used = true; + this.slots[emptyIndex].node = node; + } + + /** ECS 组件移除时销毁节点 */ + reset() { + this.node.destroy(); + } +} diff --git a/assets/script/game/map/MissEquipComp.ts.meta b/assets/script/game/map/MissEquipComp.ts.meta new file mode 100644 index 00000000..064bdb32 --- /dev/null +++ b/assets/script/game/map/MissEquipComp.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "da70c59d-7481-405a-b029-f32d19ded3fa", + "files": [], + "subMetas": {}, + "userData": {} +}