import { Node } from 'cc'; import { AttrId, AttributeComp, type BattleSession, } from 'db://assets/mx/module/battle'; import type { EntityId } from 'db://assets/mx/module/ecs'; import type { Skill } from '../skill/Skill'; import { SkillPresentationEnv } from '../skill/SkillPresentationEnv'; import type { SkillPresentationContext } from '../skill/SkillPresentationTypes'; import { SkillTransformDriver } from '../skill/SkillTransformDriver'; import { HeroSkin } from '../view/HeroSkin'; /** 攻击 modifier 分组 */ const ATK_MOD = 'faxiang_atk'; /** 缩放插值秒 */ const SCALE_ANIM_SEC = 0.4; /** 放大阶段 */ const PHASE_UP = 0; /** 缩小阶段 */ const PHASE_DOWN = 1; /** 恢复默认缩放 */ const PHASE_RESTORE = 2; /** 英雄预制体动画节点 */ const HERO_ANIM = '动画'; /** 英雄预制体阴影节点 */ const HERO_SHADOW = '阴影'; /** Skill.skill 类型 6 解析结果 */ interface FaXiangParams { /** 武器放大目标倍率 */ upScale: number; /** 加攻万分比 */ upWan: number; /** 加攻持续秒 */ upSec: number; /** 武器/动画/阴影缩小目标倍率 */ downScale: number; /** 减攻万分比 */ downWan: number; /** 减攻持续秒 */ downSec: number; /** 配表是否带缩放段 */ hasVisual: boolean; } /** 单个施法者的法相运行态 */ interface FaXiangRun { /** 施法者 */ caster: EntityId; /** 当前阶段 */ phase: number; /** 本阶段剩余秒 */ wait: number; /** 解析参数 */ params: FaXiangParams; /** 施法者 Skill(每帧回查当前武器根,避免出手重建后缩错节点) */ host: Skill; /** 英雄可缩放节点(动画、阴影;不含品质框) */ heroNodes: Node[]; /** 与 heroNodes 对齐的默认缩放 */ heroBases: number[]; /** 本段动画/阴影起点缩放 */ heroFrom: number[]; /** 本段动画/阴影目标缩放 */ heroTo: number[]; /** 武器根节点 */ weaponNode: Node | null; /** 武器默认缩放(首次绑定,不随当前缩放改写) */ weaponBase: number; /** 本段武器起点缩放 */ weaponFrom: number; /** 本段武器目标缩放 */ weaponTo: number; /** 本段缩放已进行秒 */ lerpElapsed: number; } let _host: FaXiangHost | null = null; /** Skill.skill 类型 6:法相天地 */ export function getFaXiangHost(): FaXiangHost { if (!_host) _host = new FaXiangHost(); return _host; } /** 法相天地:先加攻放大武器,再减攻缩小动画与阴影;品质框不缩放;英雄放大不超过 1 倍 */ export class FaXiangHost { private readonly _runs: FaXiangRun[] = []; /** * 英雄/武器主动释放时启动。 * @param ctx 表现上下文 * @param host 施法者 Skill(取皮肤/武器节点) */ onSkillCast(ctx: SkillPresentationContext, host: Skill): void { if (!ctx.isAlive) return; const kind = String(ctx.params.skillKind ?? ''); if (kind !== 'hero' && kind !== 'weapon') return; const cue = String(ctx.cueId ?? '').toLowerCase(); if (cue !== 'skill' && cue !== 'weaponskill') return; const raw = String(ctx.params.extraSkill ?? ctx.params.skillRaw ?? ''); const parts = raw.split(',').map((s) => Number(String(s).trim())); if ((parts[0] ?? 0) !== 6) return; const params = this._parse(parts); if (!params) return; this._start(ctx.caster, params, host); } /** * 推进阶段计时。 * @param dt 逻辑秒 */ tick(dt: number): void { if (!(dt > 0)) return; const session = SkillPresentationEnv.getBattleSession(); for (let i = this._runs.length - 1; i >= 0; i--) { const run = this._runs[i]; if (session && !session.world.isAlive(run.caster)) { this._abortAt(i); continue; } run.lerpElapsed += dt; this._applyScale(run); run.wait -= dt; if (run.wait > 0) continue; if (run.phase === PHASE_UP) { this._enterDown(run, session); continue; } if (run.phase === PHASE_DOWN) { this._enterRestore(run, session); continue; } this._abortAt(i); } } /** 离场:立刻还原缩放并清攻击加成 */ abortAll(): void { for (let i = this._runs.length - 1; i >= 0; i--) { this._abortAt(i); } } /** * 解析 `6,放大,加攻万分比,加攻秒,缩小,减攻万分比,减攻秒`;缺缩放段仍结算攻击。 * @param parts skill 分段 */ private _parse(parts: number[]): FaXiangParams | null { if (parts.length >= 7) { const params: FaXiangParams = { upScale: parts[1], upWan: parts[2], upSec: parts[3], downScale: parts[4], downWan: parts[5], downSec: parts[6], hasVisual: parts[1] > 0 && parts[4] > 0, }; if (!(params.upSec > 0) || !(params.downSec > 0)) { console.log('[FaXiang] skill 类型6时长无效', parts.join(',')); return null; } if (!params.hasVisual) { console.log('[FaXiang] skill 类型6缩放无效', parts.join(',')); } return params; } if (parts.length >= 5) { console.log('[FaXiang] skill 类型6缺缩放比例', parts.join(',')); const params: FaXiangParams = { upScale: 1, upWan: parts[1], upSec: parts[2], downScale: 1, downWan: parts[3], downSec: parts[4], hasVisual: false, }; if (!(params.upSec > 0) || !(params.downSec > 0)) { console.log('[FaXiang] skill 类型6时长无效', parts.join(',')); return null; } return params; } console.log('[FaXiang] skill 类型6参数不足', parts.join(',')); return null; } /** * 启动或刷新该施法者的法相。 * @param caster 施法者 * @param params 解析结果 * @param host 施法者 Skill */ private _start(caster: EntityId, params: FaXiangParams, host: Skill): void { const session = SkillPresentationEnv.getBattleSession(); if (!session) { console.log('[FaXiang] 无 session'); return; } const idx = this._findIndex(caster); const bound = this._bindVisual(host); let heroBases = bound.heroBases; let weaponBase = bound.weaponBase; if (idx >= 0) { const prev = this._runs[idx]; if (prev.heroBases.length === bound.heroNodes.length) heroBases = prev.heroBases; weaponBase = prev.weaponBase; this._clearAtk(session, caster); this._runs.splice(idx, 1); } const run: FaXiangRun = { caster, phase: PHASE_UP, wait: params.upSec, params, host, heroNodes: bound.heroNodes, heroBases, heroFrom: [], heroTo: [], weaponNode: bound.weaponNode, weaponBase, weaponFrom: weaponBase, weaponTo: weaponBase, lerpElapsed: 0, }; this._runs.push(run); if (idx < 0) this._snapRun(run); this._applyAtk(session, caster, params.upWan, params.upSec); this._beginScale(run, params.upScale, 1); } /** * 进入减攻缩小阶段。 * @param run 运行态 * @param session 会话 */ private _enterDown(run: FaXiangRun, session: BattleSession | null): void { run.phase = PHASE_DOWN; run.wait = run.params.downSec; if (session) this._applyAtk(session, run.caster, run.params.downWan, run.params.downSec); this._beginScale(run, run.params.downScale, run.params.downScale); } /** * 清攻击并插值恢复默认缩放。 * @param run 运行态 * @param session 会话 */ private _enterRestore(run: FaXiangRun, session: BattleSession | null): void { run.phase = PHASE_RESTORE; run.wait = SCALE_ANIM_SEC; if (session) this._clearAtk(session, run.caster); this._beginScale(run, 1, 1); } /** * 绑定动画、阴影与武器;默认缩放优先出池基准,避免把进行中的缩放当成 1 倍。 * @param host 施法者 Skill */ private _bindVisual(host: Skill): { heroNodes: Node[]; heroBases: number[]; weaponNode: Node | null; weaponBase: number } { const empty = { heroNodes: [] as Node[], heroBases: [] as number[], weaponNode: null as Node | null, weaponBase: 1 }; if (!host?.isValid) { console.log('[FaXiang] Skill 无效'); return empty; } const heroNodes: Node[] = []; const heroBases: number[] = []; const skin = host.node.getComponent(HeroSkin); const display = skin?.displayNode; if (display?.isValid) { this._pushNamed(display, HERO_ANIM, heroNodes, heroBases); this._pushNamed(display, HERO_SHADOW, heroNodes, heroBases); } if (heroNodes.length === 0) { const animNode = skin?.anim?.node; if (animNode?.isValid && animNode !== display) this._pushNode(animNode, heroNodes, heroBases); } if (heroNodes.length === 0) { console.log('[FaXiang] 无动画/阴影节点', host.node.name); } const weaponNode = host.visualChannel.weapon.root; if (!weaponNode?.isValid) { console.log('[FaXiang] 无武器节点', host.node.name); } return { heroNodes, heroBases, weaponNode: weaponNode?.isValid ? weaponNode : null, weaponBase: weaponNode?.isValid ? this._readRest(weaponNode) : 1, }; } /** * 收集指定子节点及其默认缩放。 * @param parent 英雄展示根 * @param name 子节点名 * @param nodes 输出节点 * @param bases 输出默认缩放 */ private _pushNamed(parent: Node, name: string, nodes: Node[], bases: number[]): void { const child = parent.getChildByName(name); if (child?.isValid) this._pushNode(child, nodes, bases); } /** * 记录可缩放节点。 * @param node 节点 * @param nodes 输出节点 * @param bases 输出默认缩放 */ private _pushNode(node: Node, nodes: Node[], bases: number[]): void { nodes.push(node); bases.push(this._readRest(node)); } /** * 读取节点默认缩放:武器根优先出池基准,避免叠加脏缩放。 * @param node 节点 */ private _readRest(node: Node): number { const marked = (node as Node & { poolBaseScaleX?: number }).poolBaseScaleX; if (typeof marked === 'number' && Number.isFinite(marked) && marked !== 0) { return Math.abs(marked); } const s = Math.abs(node.scale.x); if (!(s > 0) || !Number.isFinite(s)) { console.log('[FaXiang] 节点缩放无效', node.name, node.scale.x); return 1; } return s; } /** * 开启一段缩放到「默认 × 倍率」;英雄与武器同一帧起算,避免武器缓动被出手 cancelNode 掐掉。 * @param run 运行态 * @param weaponFactor 武器相对默认的倍率 * @param heroFactor 动画/阴影相对默认的倍率 */ private _beginScale(run: FaXiangRun, weaponFactor: number, heroFactor: number): void { this._refreshWeapon(run); run.lerpElapsed = 0; run.weaponTo = run.weaponBase * weaponFactor; run.weaponFrom = this._liveScale(run.weaponNode, run.weaponTo); run.heroFrom.length = 0; run.heroTo.length = 0; for (let i = 0; i < run.heroNodes.length; i++) { const to = run.heroBases[i] * heroFactor; run.heroTo.push(to); run.heroFrom.push(this._liveScale(run.heroNodes[i], to)); } this._applyScale(run); } /** * 按战斗 tick 写入当前缩放,与阶段计时同一时钟。 * @param run 运行态 */ private _applyScale(run: FaXiangRun): void { if (!run.params.hasVisual) return; this._refreshWeapon(run); const u = SCALE_ANIM_SEC > 0 ? Math.min(1, Math.max(0, run.lerpElapsed) / SCALE_ANIM_SEC) : 1; const t = u >= 1 ? 1 : SkillTransformDriver.easeSineInOut(u); this._snapScale(run.weaponNode, this._lerp(run.weaponFrom, run.weaponTo, t)); for (let i = 0; i < run.heroNodes.length; i++) { this._snapScale(run.heroNodes[i], this._lerp(run.heroFrom[i], run.heroTo[i], t)); } } /** * 回查当前武器根;出手过程若重建挂载,继续缩新节点。 * @param run 运行态 */ private _refreshWeapon(run: FaXiangRun): void { const host = run.host; if (!host?.isValid) return; const live = host.visualChannel.weapon.root; run.weaponNode = live?.isValid ? live : null; } /** * 读取节点当前 xy 缩放;节点无效则用目标值。 * @param node 节点 * @param fallback 回退 */ private _liveScale(node: Node | null, fallback: number): number { if (!node?.isValid) return fallback; const s = Math.abs(node.scale.x); if (!(s > 0) || !Number.isFinite(s)) return fallback; return s; } /** * 线性插值。 * @param from 起点 * @param to 终点 * @param t 0~1 */ private _lerp(from: number, to: number, t: number): number { return from + (to - from) * t; } /** * 写入限时攻击 Mul:×(1+万分比/10000)。 * @param session 会话 * @param caster 施法者 * @param wan 万分比 * @param duration 秒 */ private _applyAtk(session: BattleSession, caster: EntityId, wan: number, duration: number): void { const mul = 1 + wan / 10000; if (!(mul > 0) || !Number.isFinite(mul)) { console.log('[FaXiang] 攻击倍率无效', wan); this._clearAtk(session, caster); return; } const world = session.world; if (!world.getComponent(caster, AttributeComp)) { world.addComponent(caster, new AttributeComp()); } session.attributes.addModifier(caster, { attr: AttrId.Attack, op: 'Mul', value: mul, source: ATK_MOD, group: ATK_MOD, remain: duration, }); } /** * 移除法相攻击 modifier。 * @param session 会话 * @param caster 施法者 */ private _clearAtk(session: BattleSession, caster: EntityId): void { session.attributes.removeModifiersByGroup(caster, ATK_MOD); } /** * 结束一条运行态并打回默认缩放,避免下次把残留缩放当成基准叠加。 * @param index 下标 */ private _abortAt(index: number): void { const run = this._runs[index]; this._runs.splice(index, 1); const session = SkillPresentationEnv.getBattleSession(); if (session) this._clearAtk(session, run.caster); this._snapRun(run); } /** * 立刻还原默认缩放。 * @param run 运行态 */ private _snapRun(run: FaXiangRun): void { this._refreshWeapon(run); this._snapScale(run.weaponNode, run.weaponBase); for (let i = 0; i < run.heroNodes.length; i++) { this._snapScale(run.heroNodes[i], run.heroBases[i]); } } /** * 立刻设回缩放。 * @param node 节点 * @param scale 目标 */ private _snapScale(node: Node | null, scale: number): void { if (!node?.isValid) return; node.setScale(scale, scale, node.scale.z); } /** * 查找施法者运行态。 * @param caster 施法者 */ private _findIndex(caster: EntityId): number { for (let i = 0; i < this._runs.length; i++) { if (this._runs[i].caster === caster) return i; } return -1; } }