本次提交为全量的卡牌技能触发系统重构,主要变更包括: 1. 新增CardTriggerType枚举,统一卡牌触发类型定义 2. 补全依赖事件派发:每波战斗结束FightEnd、英雄死亡HeroDead(带阵营过滤)、复活成功ReviveSuccess 3. 重构SkillBoxComp,按触发类型动态注册事件监听,拆分即时/定时/驻场/事件型逻辑 4. 批量迁移所有卡牌配置,为旧技能补充显式触发类型 5. 新增全局触发次数上限机制,区分每波/全局触发计数规则 6. 新增配套设计文档,记录改造背景与方案细节 本次重构彻底解决了原有隐式配置难以维护、无法支持事件型触发的痛点,实现了技能触发逻辑的标准化与可扩展性。
524 lines
21 KiB
TypeScript
524 lines
21 KiB
TypeScript
/**
|
||
* @file SkillBoxComp.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 } 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;
|
||
|
||
/**
|
||
* 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;
|
||
|
||
/** 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[] = [];
|
||
|
||
// ======================== 触发类型化扩展 ========================
|
||
|
||
/** 触发类型(默认即时,保持向后兼容) */
|
||
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);
|
||
// 通知 MissSkillsComp 回收该节点占用的槽位
|
||
oops.message.dispatchEvent(GameEvent.RemoveSkillBox, 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 || [];
|
||
// 读取触发类型与上限(兜底默认值,避免 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, 'SkillBoxComp', `[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();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新 UI:
|
||
* - 图标:从 uicons 图集获取
|
||
* - 剩余次数标签:
|
||
* * Interval / 事件型:显示剩余次数(按各自上限计算)
|
||
* * Instant / Field:不显示
|
||
* - CD 遮罩:仅 Interval 类型展示冷却进度
|
||
*/
|
||
updateUI() {
|
||
// 加载技能图标
|
||
if (this.icon_node) {
|
||
const iconId = SkillSet[this.s_uuid]?.icon || `${this.s_uuid}`;
|
||
if (smc.uiconsAtlas) {
|
||
const frame = smc.uiconsAtlas.getSpriteFrame(iconId);
|
||
if (frame && this.icon_node && this.icon_node.isValid) {
|
||
let sprite = this.icon_node.getComponent(Sprite) || this.icon_node.addComponent(Sprite);
|
||
sprite.spriteFrame = frame;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 是否需要展示剩余次数
|
||
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();
|
||
}
|
||
}
|
||
}
|