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