You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

487 lines
16 KiB

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;
}
}