import { Node, Vec3, sp } from 'cc'; import type { BattleSession, Cell, EntityId } from 'db://assets/mx/module/battle'; import { BattleTeamId } from 'db://assets/mx/module/battle'; import { HealthComp, MovementComp, TeamComp, isBajiaoBlowRetreating, isYanSoulClaimed, } from 'db://assets/mx/module/battle/ecs/Components'; import { GBundle, GPath } from '../../game/ConfigRes'; import { BattleMapSpec } from '../map/BattleMapSpec'; import type { MapManager } from '../map/legacy/MapManager'; import { HeroActorHost } from '../heros/HeroActorHost'; import { HeroSkin } from '../view/HeroSkin'; import type { Skill } from '../skill/Skill'; import type { SkillPresentationContext } from '../skill/SkillPresentationTypes'; import { SkillPresentationEnv } from '../skill/SkillPresentationEnv'; import { computeAimAngleDeg, setNodeWorldPos, shortestAngleDeltaDeg, } from '../skill/SkillTransformDriver'; import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch'; import { applyWeaponSpineSkin } from '../view/GlyphSpineSkin'; import { BattleSpinePlayback } from './BattleSpinePlayback'; import { getEarthChargeHost } from './EarthChargePresenter'; import { isEarthChargeSkillId } from '../config/weapon1/Weapon1BattleIds'; import type { ISkillPresenter } from './SkillPresenter'; /** battleHero/prefab/skill/哪吒冲锋 */ const CHARGE_PREFAB = '哪吒冲锋'; /** 预制体枪节点(默认动画 + 朝冲锋方向转角) */ const SPEAR_NODE = '冲锋枪'; /** 枪头转向速度(度/秒) */ const SPEAR_TURN_DEG = 540; /** 沿路径前瞻(像素),用于算朝向 */ const SPEAR_LOOKAHEAD = 28; /** 预制体哪吒身体 */ const BODY_NODE = '默认'; /** 组合字:飞出冲锋的字 */ const ZHA_NAME = '吒'; /** 冲锋期间英雄待机(只留哪) */ const IDLE_SINGLE = '待机单字'; /** 冲锋结束回普通待机 */ const IDLE_ANIM = '待机'; /** 英雄预制体阴影根 */ const SHADOW_ROOT = '阴影'; /** 冲锋期间须隐藏的阴影子节点(吒脚下影;按名取,勿用 children 下标) */ const SHADOW_SECOND = 'ebg-001'; /** 起跳蓄力(秒) */ const JUMP_ANTICIP = 0.08; /** 滞空(秒) */ const JUMP_AIR = 0.3; /** 落地回正(秒) */ const JUMP_SETTLE = 0.07; /** 跳出总时长 */ const JUMP_TOTAL = JUMP_ANTICIP + JUMP_AIR + JUMP_SETTLE; /** 跳弧高度(像素) */ const JUMP_H = 96; /** 沿途命中半径(像素,对齐冲锋枪半长) */ const HIT_RADIUS = 100; /** 冲锋直伤类型 */ const DMG_TYPE = 'nezha_charge'; /** Skill.skill type=1 解析结果 */ export interface NezhaChargeParams { /** 冲锋速度(像素/秒) */ speed: number; /** 持续秒 */ duration: number; /** 伤害万分比 → DealDamage scale */ scale: number; } /** 一次冲锋运行态 */ interface NezhaChargeRun { /** 施法者 */ caster: EntityId; /** 表现上下文 */ ctx: SkillPresentationContext; /** Skill 宿主 */ host: Skill; /** 播控序号 */ seq: number; /** 冲锋根节点 */ node: Node; /** 冲锋枪节点 */ spear: Node; /** 路径世界坐标(含吸附点) */ waypoints: Vec3[]; /** 像素/秒 */ speed: number; /** 持续秒 */ duration: number; /** 伤害系数 */ scale: number; /** 冲锋已过秒(不含跳出) */ elapsed: number; /** 跳出已过秒 */ jumpElapsed: number; /** 吒字世界 X */ jumpFromX: number; /** 吒字世界 Y */ jumpFromY: number; /** 吸附格世界 X */ jumpToX: number; /** 吸附格世界 Y */ jumpToY: number; /** 已走路程(像素) */ traveled: number; /** 枪当前角(度,0=右) */ spearAngle: number; /** 已命中实体 */ hitIds: EntityId[]; /** 英雄根(结束回待机) */ heroNode: Node | null; /** 阴影第二节点(吒影,结束恢复) */ shadow2: Node | null; /** 上一帧世界坐标 */ lastX: number; /** 上一帧世界坐标 */ lastY: number; } let _host: NezhaChargeHost | null = null; /** 哪吒冲锋运行时(表现+沿途伤) */ export function getNezhaChargeHost(): NezhaChargeHost { if (!_host) _host = new NezhaChargeHost(); return _host; } /** 哪吒冲锋:英雄待机单字、吒位跳出、路径反冲、沿途伤 */ export class NezhaChargeHost { private readonly _runs: NezhaChargeRun[] = []; private readonly _tmp = new Vec3(); private readonly _aim = new Vec3(); private readonly _from = new Vec3(); /** 当前命中扫描的冲锋 */ private _hitRun: NezhaChargeRun | null = null; /** * 解析 `1,速度,时长,伤害万分比`。 * @param raw Skill.skill */ static parse(raw: string): NezhaChargeParams | null { const parts = String(raw ?? '').split(',').map((s) => Number(String(s).trim())); if (parts[0] !== 1) return null; const speed = parts[1]; const duration = parts[2]; const wan = parts[3]; if (!(speed > 0) || !(duration > 0) || !(wan > 0)) { console.log('[NezhaCharge] skill 参数无效'); return null; } return { speed, duration, scale: wan / 10000 }; } /** * 开战冲锋。 * @param ctx 表现上下文 * @param host Skill 宿主 */ start(ctx: SkillPresentationContext, host: Skill): void { this._abortCaster(ctx.caster); const raw = String(ctx.params.extraSkill ?? ctx.params.skillRaw ?? ''); const params = NezhaChargeHost.parse(raw); if (!params) { host.drainLogicPresentation(ctx); return; } const session = SkillPresentationEnv.getBattleSession(); const map = SkillPresentationEnv.getMap(); if (!session || !map) { console.log('[NezhaCharge] 无 session/map'); host.drainLogicPresentation(ctx); return; } const zhaIdx = SkillPresentationEnv.getCompositionGridIndexByWeapon2Name(ctx.caster, ZHA_NAME); const zhaWorld = zhaIdx != null ? SkillPresentationEnv.getGridWorldCenter(zhaIdx) : SkillPresentationEnv.getCasterGridCenter(ctx.caster); if (!zhaWorld) { console.log('[NezhaCharge] 无吒字落点', ctx.caster); host.drainLogicPresentation(ctx); return; } const fromCell = this._zhaCell(zhaIdx, zhaWorld, map); if (!fromCell) { console.log('[NezhaCharge] 吒字格无效'); host.drainLogicPresentation(ctx); return; } const snap = this._snapApproachCell(session, ctx.caster, fromCell); if (!snap) { console.log('[NezhaCharge] 进路上无吸附格', fromCell); host.drainLogicPresentation(ctx); return; } const waypoints = this.buildRetreatWorlds(session, ctx.caster, snap, params); if (waypoints.length < 1) { console.log('[NezhaCharge] 反向路径为空'); host.drainLogicPresentation(ctx); return; } const parent = SkillPresentationEnv.flyLayer ?? map.ensureHeroParent(); if (!parent?.isValid) { console.log('[NezhaCharge] 无父节点'); host.drainLogicPresentation(ctx); return; } let node: Node | null = null; try { node = gg.res.getNode(GPath.HeroSkillPrefab(), CHARGE_PREFAB, GBundle.BattleHero); } catch (err) { console.log('[NezhaCharge] getNode 失败', String(err)); host.drainLogicPresentation(ctx); return; } if (!node?.isValid) { console.log('[NezhaCharge] 预制体未加载', CHARGE_PREFAB); host.drainLogicPresentation(ctx); return; } const spear = this._findNamed(node, SPEAR_NODE); if (!spear?.isValid) { console.log('[NezhaCharge] 缺少节点', SPEAR_NODE); gg.res.putNode(node); host.drainLogicPresentation(ctx); return; } const seq = host.beginPresentationCast(ctx); host.disarmAnimComplete(); host.visualChannel.clearCastCompletes(); parent.active = true; node.active = true; node.layer = parent.layer; node.parent = parent; node.setSiblingIndex(parent.children.length - 1); node.setScale(1, 1, 1); const spearAngle = this._chargeAimDeg(zhaWorld.x, zhaWorld.y, waypoints, 0) ?? 0; spear.angle = spearAngle; this._applySpearSkin(spear, host); setNodeWorldPos(node, zhaWorld); this._replayPrefabAnims(node); const heroLook = this._beginHeroChargeIdle(ctx.caster, host); this._runs.push({ caster: ctx.caster, ctx, host, seq, node, spear, waypoints, speed: params.speed, duration: params.duration, scale: params.scale, elapsed: 0, jumpElapsed: 0, jumpFromX: zhaWorld.x, jumpFromY: zhaWorld.y, jumpToX: waypoints[0].x, jumpToY: waypoints[0].y, traveled: 0, spearAngle, hitIds: [], heroNode: heroLook.heroNode, shadow2: heroLook.shadow2, lastX: zhaWorld.x, lastY: zhaWorld.y, }); } /** * 推进冲锋。 * @param dt 逻辑秒(含倍速) */ tick(dt: number): void { if (!(dt > 0)) return; for (let i = this._runs.length - 1; i >= 0; i--) { if (!this._step(this._runs[i], dt)) { this._finish(this._runs[i], true); this._runs.splice(i, 1); } } } /** 结束全部冲锋并恢复显隐 */ abortAll(): void { for (let i = this._runs.length - 1; i >= 0; i--) { this._finish(this._runs[i], false); } this._runs.length = 0; } /** * 结束该施法者未完成的冲锋。 * @param caster 施法者 */ private _abortCaster(caster: EntityId): void { for (let i = this._runs.length - 1; i >= 0; i--) { if (this._runs[i].caster !== caster) continue; this._finish(this._runs[i], false); this._runs.splice(i, 1); } } /** * 单帧:跳出、沿路位移、命中。返回 false 表示结束。 * @param run 运行态 * @param dt 逻辑秒 */ private _step(run: NezhaChargeRun, dt: number): boolean { if (!run.ctx.isAlive || !run.host.isPresentationCast(run.seq) || !run.node.isValid) { if (run.jumpElapsed < JUMP_TOTAL) { console.log( '[NezhaCharge] 跳出被打断', 'alive=', run.ctx.isAlive, 'cast=', run.host.isPresentationCast(run.seq), 'node=', run.node.isValid, ); } return false; } if (run.jumpElapsed < JUMP_TOTAL) { run.jumpElapsed += dt; this._applyJump(run); this._turnSpear(run, dt, this._chargeAimDeg(run.lastX, run.lastY, run.waypoints, 0)); this._hitNear(run); return true; } run.elapsed += dt; const cap = run.speed * Math.min(run.duration, run.elapsed); this._pointAlong(run.waypoints, cap, this._tmp); setNodeWorldPos(run.node, this._tmp); run.lastX = this._tmp.x; run.lastY = this._tmp.y; run.traveled = cap; this._turnSpear(run, dt, this._chargeAimDeg(run.lastX, run.lastY, run.waypoints, cap)); this._hitNear(run); const pathLen = this._pathLen(run.waypoints); if (run.elapsed >= run.duration || cap >= pathLen - 0.5) return false; return true; } /** * 冲锋体附近敌对怪各伤一次。 * @param run 运行态 */ private _hitNear(run: NezhaChargeRun): void { const session = SkillPresentationEnv.getBattleSession(); if (!session || !session.world.isAlive(run.caster)) return; this._hitRun = run; session.world.view(HealthComp).forEach(this._hitOne); this._hitRun = null; } /** * 单体命中判定(forEach 回调)。 * @param id 实体 * @param hp 生命 */ private readonly _hitOne = (id: EntityId, hp: HealthComp): void => { const run = this._hitRun; const session = SkillPresentationEnv.getBattleSession(); if (!run || !session) return; if (id === run.caster) return; if (this._alreadyHit(run, id)) return; if (!session.world.isAlive(id) || !(hp.hp > 0)) return; const move = session.world.getComponent(id, MovementComp); if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) return; const casterTeam = session.world.getComponent(run.caster, TeamComp); if (!BattleTeamId.isHostile(casterTeam, session.world.getComponent(id, TeamComp))) return; const wp = SkillPresentationEnv.getTargetWorldPos(id); if (!wp) return; const dx = wp.x - run.lastX; const dy = wp.y - run.lastY; if (dx * dx + dy * dy > HIT_RADIUS * HIT_RADIUS) return; run.hitIds.push(id); run.ctx.dealDamage(id, { scale: run.scale, damageType: DMG_TYPE }); }; /** * 是否已命中。 * @param run 运行态 * @param id 实体 */ private _alreadyHit(run: NezhaChargeRun, id: EntityId): boolean { const ids = run.hitIds; for (let i = 0; i < ids.length; i++) { if (ids[i] === id) return true; } return false; } /** * 回收并恢复显隐;可选 finish 施法锁。 * @param run 运行态 * @param releaseLock 是否 finish ctx */ private _finish(run: NezhaChargeRun, releaseLock: boolean): void { this._restoreHeroChargeIdle(run); if (run.host.isValid) run.host.visualChannel.setCaster(run.caster); if (run.node?.isValid) { this._clearSpine(run.node); run.node.angle = 0; run.node.setScale(1, 1, 1); if (run.spear.isValid) { run.spear.angle = 0; this._resetSpearSkin(run.spear); } gg.res.putNode(run.node); } if (releaseLock && run.host.isPresentationCast(run.seq)) { run.host.finishPresentationCast(run.seq, false); } else if (!releaseLock && run.ctx.isAlive) { run.ctx.cancel('aborted'); } } /** * 冲锋期间:英雄保持可见,播「待机单字」,藏阴影第二节点与武器。 * @param caster 施法者 * @param host Skill */ private _beginHeroChargeIdle(caster: EntityId, host: Skill): { heroNode: Node | null; shadow2: Node | null } { const actor = HeroActorHost.instance.getActor(caster); const node = actor?.node ?? host.node; if (!node?.isValid) return { heroNode: null, shadow2: null }; host.visualChannel.hideWeapon(); const skin = node.getComponent(HeroSkin); const anim = skin?.anim; if (!anim?.isValid || !BattleSpinePlayback.play(anim, IDLE_SINGLE, { loop: true })) { console.warn('[NezhaCharge] 缺少动画', IDLE_SINGLE, caster); } // 只藏「吒」脚下影 ebg-001;「哪」的 ebg 须保持显示 const shadow2 = this._findShadowSecond(skin?.displayNode ?? node); if (shadow2) { shadow2.active = false; } else { console.warn('[NezhaCharge] 阴影第二节点缺失', caster); } return { heroNode: node, shadow2 }; } /** 按名取阴影第二节点,避免 children 顺序变化藏错 */ private _findShadowSecond(display: Node | null): Node | null { const shadowRoot = display?.getChildByName(SHADOW_ROOT); if (!shadowRoot?.isValid) return null; const byName = shadowRoot.getChildByName(SHADOW_SECOND); if (byName?.isValid) return byName; return shadowRoot.children[1]?.isValid ? shadowRoot.children[1] : null; } /** * 冲锋结束:回「待机」,恢复阴影第二节点。 * @param run 运行态 */ private _restoreHeroChargeIdle(run: NezhaChargeRun): void { // 引用可能已随换皮失效:优先用存的节点,否则从当前展示重找 let shadow2 = run.shadow2?.isValid ? run.shadow2 : null; if (!shadow2 && run.heroNode?.isValid) { const skin = run.heroNode.getComponent(HeroSkin); shadow2 = this._findShadowSecond(skin?.displayNode ?? run.heroNode); } if (shadow2) shadow2.active = true; if (!run.heroNode?.isValid) return; const anim = run.heroNode.getComponent(HeroSkin)?.anim; if (anim?.isValid) BattleSpinePlayback.play(anim, IDLE_ANIM, { loop: true, optional: true }); } /** * 吒字格;无 index 时用世界坐标反查。 * @param zhaIdx 地图格 * @param zhaWorld 吒世界坐标 * @param map 地图 */ private _zhaCell(zhaIdx: number | null, zhaWorld: Readonly, map: MapManager): Cell | null { if (zhaIdx != null) { const rc = SkillPresentationEnv.getMapGridRowCol(zhaIdx); if (rc) return { row: rc.row, col: rc.col }; } const grid = map.getMapGridByWorldPos(zhaWorld as Vec3); if (!grid) return null; return { row: grid.Row, col: grid.Col }; } /** * 攻我方进路上离吒最近的格(按格距,不经任意可走格二次吸附)。 * @param session 会话 * @param caster 施法者 * @param zhaCell 吒字格 */ private _snapApproachCell(session: BattleSession, caster: EntityId, zhaCell: Cell): { Row: number; Col: number } | null { const enemyPort = this._enemyPort(session, caster); const spawn = session.topology.getSpawnCell(enemyPort); if (!spawn) { console.log('[NezhaCharge] 敌方出怪口缺失', enemyPort); return null; } const cell = session.pathfinder.nearestApproachCell(zhaCell, spawn, enemyPort); if (!cell) return null; return { Row: cell.row, Col: cell.col }; } /** * 攻向施法者阵营的出怪口。 * @param session 会话 * @param caster 施法者 */ private _enemyPort(session: BattleSession, caster: EntityId): number { const team = session.world.getComponent(caster, TeamComp); const owner = team?.ownerId ?? team?.team ?? 0; return owner === 0 ? 1 : 2; } /** * 从吸附格沿攻我方的怪物进路退向敌方出怪口。 * @param session 会话 * @param caster 施法者 * @param snap 吸附格 * @param params 配表 */ private buildRetreatWorlds(session: BattleSession, caster: EntityId, snap: { Row: number; Col: number }, params: NezhaChargeParams): Vec3[] { const enemyPort = this._enemyPort(session, caster); const spawn = session.topology.getSpawnCell(enemyPort); if (!spawn) { console.log('[NezhaCharge] 敌方出怪口缺失', enemyPort); return []; } const cellSize = Math.max(1, BattleMapSpec.CELL_SIZE); const maxSteps = Math.ceil(params.speed * params.duration / cellSize) + 8; const from: Cell = { row: snap.Row, col: snap.Col }; const cells = session.pathfinder.buildRetreatPath(from, maxSteps, spawn, enemyPort); const out: Vec3[] = []; for (let i = 0; i < cells.length; i++) { const w = SkillPresentationEnv.getGridWorldCenterByRowCol(cells[i].row, cells[i].col); if (w) out.push(w.clone()); } return out; } /** * 沿折线取点。 * @param points 路点 * @param dist 路程 * @param out 输出 */ private _pointAlong(points: readonly Vec3[], dist: number, out: Vec3): void { if (points.length === 0) { out.set(0, 0, 0); return; } if (points.length === 1 || dist <= 0) { out.set(points[0].x, points[0].y, points[0].z); return; } let left = dist; for (let i = 0; i < points.length - 1; i++) { const a = points[i]; const b = points[i + 1]; const dx = b.x - a.x; const dy = b.y - a.y; const seg = Math.sqrt(dx * dx + dy * dy); if (seg <= 1e-4) continue; if (left <= seg) { const t = left / seg; out.set(a.x + dx * t, a.y + dy * t, a.z); return; } left -= seg; } const last = points[points.length - 1]; out.set(last.x, last.y, last.z); } /** * 折线总长。 * @param points 路点 */ private _pathLen(points: readonly Vec3[]): number { let len = 0; for (let i = 0; i < points.length - 1; i++) { const dx = points[i + 1].x - points[i].x; const dy = points[i + 1].y - points[i].y; len += Math.sqrt(dx * dx + dy * dy); } return len; } /** * 深度查找子节点。 * @param root 根 * @param name 名 */ private _findNamed(root: Node, name: string): Node | null { const stack: Node[] = [root]; while (stack.length > 0) { const n = stack.pop()!; if (n.name === name) return n; const ch = n.children; for (let i = 0; i < ch.length; i++) stack.push(ch[i]); } return null; } /** * 冲锋枪切到当前 Weapon.skin。 * @param spear 冲锋枪节点 * @param host Skill 宿主 */ private _applySpearSkin(spear: Node, host: Skill): void { const sk = spear.getComponent(sp.Skeleton) ?? spear.getComponentInChildren(sp.Skeleton); if (!sk?.isValid) { console.log('[NezhaCharge] 冲锋枪缺 Spine'); return; } const skin = String(host.visualChannel.weapon.skinName ?? '').trim(); if (!skin) { console.log('[NezhaCharge] 当前武器皮肤为空', host.visualChannel.weapon.weaponId); return; } applyWeaponSpineSkin(sk, skin, '[NezhaCharge]'); } /** * 回池前把冲锋枪皮还原为预制体 defaultSkin。 * @param spear 冲锋枪节点 */ private _resetSpearSkin(spear: Node): void { const sk = spear.getComponent(sp.Skeleton) ?? spear.getComponentInChildren(sp.Skeleton); if (!sk?.isValid) return; const def = String((sk as sp.Skeleton & { defaultSkin?: string }).defaultSkin ?? '').trim(); if (!def) return; applyWeaponSpineSkin(sk, def, '[NezhaCharge]'); } /** * 各 Spine 只重播自己的预制体默认动画,不改冲锋枪朝向。 * @param root 冲锋根 */ private _replayPrefabAnims(root: Node): void { const body = this._findNamed(root, BODY_NODE); if (body) this._replayOwnDefault(body); const spear = this._findNamed(root, SPEAR_NODE); if (spear) this._replayOwnDefault(spear); } /** * 重播该节点自身 defaultAnimation。 * @param node 身体或冲锋枪 */ private _replayOwnDefault(node: Node): void { const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); if (!sk?.isValid) return; if (sk.skeletonData) ensureSpineSkeletonDataRuntime(sk.skeletonData); const typed = sk as sp.Skeleton & { defaultAnimation?: string }; const name = String(typed.defaultAnimation || '').trim(); if (!name) return; BattleSpinePlayback.play(sk, name, { loop: true, optional: true }); } /** * 取消根下全部 Spine 完成回调。 * @param root 冲锋根 */ private _clearSpine(root: Node): void { const list = root.getComponentsInChildren(sp.Skeleton); for (let i = 0; i < list.length; i++) { BattleSpinePlayback.clearPendingComplete(list[i]); } } /** * 枪口对准冲锋前方(最短弧插值)。 * @param run 运行态 * @param dt 逻辑秒 * @param desired 目标角;空则不动 */ private _turnSpear(run: NezhaChargeRun, dt: number, desired: number | null): void { if (desired == null || !run.spear.isValid) return; const delta = shortestAngleDeltaDeg(run.spearAngle, desired); const maxStep = SPEAR_TURN_DEG * dt; const step = delta > maxStep ? maxStep : (delta < -maxStep ? -maxStep : delta); run.spearAngle += step; run.spear.angle = run.spearAngle; } /** * 沿路径前瞻的冲锋朝向(度,0=右)。 * @param fromX 当前 X * @param fromY 当前 Y * @param waypoints 路点 * @param traveled 已走路程 */ private _chargeAimDeg(fromX: number, fromY: number, waypoints: readonly Vec3[], traveled: number): number | null { this._from.set(fromX, fromY, 0); this._pointAlong(waypoints, traveled + SPEAR_LOOKAHEAD, this._aim); return computeAimAngleDeg(this._from, this._aim); } /** * 从吒字位跳到吸附格:先蹲再弹,抛物线插值。 * @param run 运行态 */ private _applyJump(run: NezhaChargeRun): void { const t = run.jumpElapsed; if (t <= JUMP_ANTICIP) { const k = this._easeOutQuad(Math.min(1, t / JUMP_ANTICIP)); run.node.setScale(1 + 0.3 * k, 1 - 0.4 * k, 1); this._tmp.set(run.jumpFromX, run.jumpFromY, 0); setNodeWorldPos(run.node, this._tmp); run.lastX = run.jumpFromX; run.lastY = run.jumpFromY; return; } const airT = Math.min(1, (t - JUMP_ANTICIP) / JUMP_AIR); const u = this._easeOutCubic(airT); const lift = 4 * airT * (1 - airT) * JUMP_H; const x = run.jumpFromX + (run.jumpToX - run.jumpFromX) * u; const y = run.jumpFromY + (run.jumpToY - run.jumpFromY) * u + lift; this._tmp.set(x, y, 0); setNodeWorldPos(run.node, this._tmp); run.lastX = x; run.lastY = y; if (airT < 1) { run.node.setScale(0.82 + 0.36 * airT, 1.28 - 0.5 * airT, 1); return; } const settle = this._easeOutQuad(Math.min(1, (t - JUMP_ANTICIP - JUMP_AIR) / JUMP_SETTLE)); run.node.setScale(1.18 - 0.18 * settle, 0.78 + 0.22 * settle, 1); if (settle >= 1) run.node.setScale(1, 1, 1); } /** * 二次缓出。 * @param t 0~1 */ private _easeOutQuad(t: number): number { return 1 - (1 - t) * (1 - t); } /** * 三次缓出(起跳猛)。 * @param t 0~1 */ private _easeOutCubic(t: number): number { const r = 1 - t; return 1 - r * r * r; } } /** Skill.skill type=1 表现入口 */ export class NezhaChargePresenter implements ISkillPresenter { /** * 播放哪吒冲锋。 * @param ctx 表现上下文 * @param host Skill 宿主 */ present(ctx: SkillPresentationContext, host: Skill): void { if (!ctx.isAlive) { ctx.finish(); return; } const skillId = Math.floor(Number(ctx.params.skillId) || 0); if (isEarthChargeSkillId(skillId)) { getEarthChargeHost().start(ctx, host); return; } getNezhaChargeHost().start(ctx, host); } }