import { Node, sp } from 'cc'; import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch'; import { GBundle } from '../../game/ConfigRes'; import { TableNames } from '../../game/ConfigTableData'; import { BattleSpinePlayback } from '../presentation/BattleSpinePlayback'; import { BattleMapSpec } from '../map/BattleMapSpec'; import { applyWeaponSpineSkin, spineSkinName } from './GlyphSpineSkin'; /** 武器类型 → 预制体名(Hero.herotype / Weapon.type:1–5) */ const WEAPON_TYPE_PREFAB: ReadonlyArray = [ '', 'weaponType1', 'weaponType2', 'weaponType3', 'weaponType4', 'weaponType5', ]; const WEAPON_PREFAB_PATH = 'prefab/weapon/'; /** battleHero/prefab/skill/武器弓-攻击 */ const BOW_ATK_PREFAB = '武器弓-攻击'; const BOW_ATK_PREFAB_PATH = 'prefab/skill/'; const NODE_DRAW = '拉弓动画'; const NODE_PROJECTILE = '投掷物'; const NODE_PROJECTILE_ANIM = 'anim'; /** weapon 节点:刀左右翻转,其余旋转朝向 */ const AIM_NODE = 'weapon'; const SKIN_NODE = 'skin'; /** 技能动画节点(优先于 skin) */ const ANIM_NODE = 'anim'; /** 弓投掷物节点(weaponType4 / weapon 下) */ const ARROW_NODE = '箭'; /** Weapon.type / herotype 刀、金 */ const WEAPON_TYPE_SWORD = 1; /** herotype 木:与刀相同只左右翻转 */ const WEAPON_TYPE_WOOD = 2; /** herotype 水:法杖不旋转 */ const WEAPON_TYPE_STAFF = 3; /** Weapon.type / herotype 弓、火 */ const WEAPON_TYPE_BOW = 4; const SPINE_ATTACK_DEFAULT = '攻击'; const SPINE_IDLE = '待机'; /** weaponType* 的 weapon 节点待机角(预制体 euler.z) */ export const WEAPON_REST_ANGLE = 180; /** 相对格心再向左(约 0.35 格,与老项目组合武器一致) */ export const WEAPON_LEFT_OFFSET_PX = BattleMapSpec.CELL_SIZE * 0.35; /** 「武器弓-攻击」根节点待机角(预制体 euler.z,拉弓动画本地角不动) */ export const BOW_ATK_REST_ANGLE = 0; /** 万箭齐发:武器弓-攻击朝上 */ export const BOW_RAIN_AIM_DEG = 90; /** * 战斗武器挂载:Weapon.type 或 Hero.herotype 对应 weaponType*。 * 弓攻击藏 weaponType4,改显「武器弓-攻击」;朝向只转该根节点,不改拉弓动画本地角。 */ export class HeroWeaponMount { private _root: Node | null = null; private _aim: Node | null = null; private _skinAnim: sp.Skeleton | null = null; private _arrow: Node | null = null; private _arrowAnim: sp.Skeleton | null = null; private _atkRoot: Node | null = null; private _drawAnim: sp.Skeleton | null = null; private _projectile: Node | null = null; private _projectileAnim: sp.Skeleton | null = null; private _weaponId = 0; private _type = 0; private _skinName = ''; private _bowAtkShowing = false; /** 武器根节点(只做位移,不转角) */ get root(): Node | null { return this._root?.isValid ? this._root : null; } /** 朝向节点(weapon) */ get aim(): Node | null { return this._aim?.isValid ? this._aim : null; } /** 武器 skin 节点 Spine */ get skinAnim(): sp.Skeleton | null { return this._skinAnim?.isValid ? this._skinAnim : null; } /** 武器弓-攻击 / 拉弓动画 Spine */ get drawAnim(): sp.Skeleton | null { return this._drawAnim?.isValid ? this._drawAnim : null; } /** 武器弓-攻击根 */ get bowAtkRoot(): Node | null { return this._atkRoot?.isValid ? this._atkRoot : null; } /** 是否正在显示武器弓-攻击 */ get bowAtkShowing(): boolean { return this._bowAtkShowing && !!this._atkRoot?.isValid; } /** 当前武器 id */ get weaponId(): number { return this._weaponId; } /** 当前武器类型(1–5);无武器表行时为 herotype */ get weaponType(): number { return this._type; } /** Weapon 表类型;五行挂载(无 weaponId)为 0,避免当成刀/弓 */ get tableWeaponType(): number { return this._weaponId > 0 ? this._type : 0; } /** 当前 Weapon.skin */ get skinName(): string { return this._skinName; } /** 弓投掷物模板:优先「武器弓-攻击/投掷物」,否则 weaponType4「箭」 */ get arrowTemplate(): Node | null { if (this._projectile?.isValid) return this._projectile; return this._arrow?.isValid ? this._arrow : null; } /** * 确保挂载指定武器(id 变化时换预制体 / 同类型仅切肤)。 * @param parent 挂到英雄 actor 节点下 * @param weaponId Weapon.id */ ensure(parent: Node, weaponId: number): boolean { if (!parent?.isValid) { console.error('[HeroWeaponMount] parent 无效'); return false; } const wid = Math.floor(Number(weaponId) || 0); if (wid <= 0) { console.log('[HeroWeaponMount] weaponId 无效', weaponId); return false; } const row = gg.data.table.getTableData(TableNames.Weapon, wid); if (!row) { console.error('[HeroWeaponMount] Weapon 表缺失', wid); return false; } const type = Math.floor(Number(row.type) || 0); const prefabName = WEAPON_TYPE_PREFAB[type] ?? ''; if (!prefabName) { console.error('[HeroWeaponMount] 不支持的武器类型', type, 'weaponId=', wid); return false; } if (this._root?.isValid && this._type === type) { this._weaponId = wid; if (type === WEAPON_TYPE_BOW && !this._arrow?.isValid) this._bindArrow(); if (type === WEAPON_TYPE_BOW) this._ensureBowAtkPrefab(); this._applySkin(row.skin); return !!this._skinAnim?.isValid; } this.release(); const root = gg.res.getNode(WEAPON_PREFAB_PATH, prefabName, GBundle.BattleHero); if (!root?.isValid) { console.error('[HeroWeaponMount] 预制体未加载', prefabName); return false; } root.parent = parent; root.setPosition(0, 0, 0); root.active = false; this._root = root; this._type = type; this._weaponId = wid; this._aim = root.getChildByName(AIM_NODE); if (!this._aim?.isValid) { console.error('[HeroWeaponMount] 缺少 weapon 朝向节点', prefabName); this.release(); return false; } this._skinAnim = this._resolveAnimSpine(root); if (!this._skinAnim?.isValid) { console.error('[HeroWeaponMount] 缺少 anim/skin Spine', prefabName); this.release(); return false; } this._bindArrow(); if (type === WEAPON_TYPE_BOW) this._ensureBowAtkPrefab(); this.resetAim(); this.setArrowVisible(false); this._applySkin(row.skin); return true; } /** * 按 Hero.herotype 挂 weaponType*(五行数字,不查 Weapon 表、不套装备皮)。 * @param parent 挂点 * @param herotype Hero.herotype(1–5) */ ensureByHeroType(parent: Node, herotype: number): boolean { if (!parent?.isValid) { console.error('[HeroWeaponMount] parent 无效'); return false; } const type = Math.floor(Number(herotype) || 0); const prefabName = WEAPON_TYPE_PREFAB[type] ?? ''; if (!prefabName) { console.error('[HeroWeaponMount] 不支持的 herotype', herotype); return false; } if (this._root?.isValid && this._type === type && this._weaponId <= 0) { if (this._root.parent !== parent) this._root.parent = parent; if (type === WEAPON_TYPE_BOW && !this._arrow?.isValid) this._bindArrow(); return !!this._skinAnim?.isValid; } this.release(); const root = gg.res.getNode(WEAPON_PREFAB_PATH, prefabName, GBundle.BattleHero); if (!root?.isValid) { console.error('[HeroWeaponMount] 预制体未加载', prefabName); return false; } root.parent = parent; root.setPosition(0, 0, 0); root.active = false; this._root = root; this._type = type; this._weaponId = 0; this._aim = root.getChildByName(AIM_NODE); if (!this._aim?.isValid) { console.error('[HeroWeaponMount] 缺少 weapon 朝向节点', prefabName); this.release(); return false; } this._skinAnim = this._resolveAnimSpine(root); if (!this._skinAnim?.isValid) { console.error('[HeroWeaponMount] 缺少 anim/skin Spine', prefabName); this.release(); return false; } const sd = this._skinAnim.skeletonData; if (sd?.isValid) ensureSpineSkeletonDataRuntime(sd); this._bindArrow(); if (type !== WEAPON_TYPE_STAFF) this.resetAim(); return true; } /** 朝向节点转回待机角;刀/木复位左右缩放。水弹法杖不改预制体角 */ resetAim(): void { if (!this._aim?.isValid) return; if (this._type === WEAPON_TYPE_STAFF) return; this._aim.angle = WEAPON_REST_ANGLE; if (this._isFlipType()) this._setAimScaleX(1); } /** * 朝向节点对准世界点(度,Cocos 2D)。刀类型不要调用,应走 aimByTargetX。 * @param deg 角度 */ setAimAngle(deg: number): void { if (this._aim?.isValid) this._aim.angle = deg; } /** * 刀/木:保持待机角,按目标左右翻转 weapon.scale.x(左=1,右=-1)。 * @param targetWorldX 目标世界 X */ aimByTargetX(targetWorldX: number): void { if (!this._aim?.isValid) return; if (!this._isFlipType()) { console.log('[HeroWeaponMount] aimByTargetX 仅刀/木', this._type, 'weaponId=', this._weaponId); return; } this._aim.angle = WEAPON_REST_ANGLE; const dx = targetWorldX - this._aim.worldPosition.x; if (!(Math.abs(dx) > 0)) return; this._setAimScaleX(dx < 0 ? 1 : -1); } /** 显示/隐藏待机武器(weaponType*);弓攻效显隐走 showBowAtk / hideBowAtk */ setVisible(visible: boolean): void { if (!visible) this.hideBowAtk(false); if (this._root?.isValid) this._root.active = visible && !this._bowAtkShowing; } /** * 播放武器释放动画(Skill.animation;缺省「攻击」)。组合弓走 playBowDraw;五行弓走本方法。 * @param animation Spine 动画名 * @returns 是否成功起播 */ playAttack(timeScale: number, onComplete?: () => void, animation: string = SPINE_ATTACK_DEFAULT): boolean { const name = String(animation ?? '').trim() || SPINE_ATTACK_DEFAULT; return this._playNamed(name, false, timeScale, onComplete); } /** * 循环播放武器释放动画(暴风雪等持续技;由外部 endHoldCast 停)。 * @param timeScale 倍速 * @param animation Spine 动画名 */ playAttackLoop(timeScale: number, animation: string = SPINE_ATTACK_DEFAULT): boolean { const name = String(animation ?? '').trim() || SPINE_ATTACK_DEFAULT; return this._playNamed(name, true, timeScale); } /** 播放武器待机(无「待机」则播预制体 defaultAnimation) */ playIdle(timeScale: number = 1): void { this.hideBowAtk(true); this.setArrowVisible(false); const anim = this._skinAnim; if (!anim?.isValid) return; if (BattleSpinePlayback.play(anim, SPINE_IDLE, { loop: true, timeScale, optional: true })) return; const fallback = String(anim.animation ?? '').trim(); if (fallback && BattleSpinePlayback.play(anim, fallback, { loop: true, timeScale, optional: true })) { if (fallback !== SPINE_IDLE) { console.warn('[HeroWeaponMount] 无待机,改播预制体默认动画', fallback, 'type=', this._type); } return; } console.warn('[HeroWeaponMount] 待机未起播', this._type, this._weaponId, fallback); } /** * 显示/隐藏弓上的箭模板。 * @param visible 是否显示 */ setArrowVisible(visible: boolean): void { if (this._arrow?.isValid) this._arrow.active = visible; if (!visible) this.setProjectileVisible(false); } /** * 显示/隐藏「武器弓-攻击」投掷物模板(发射时克隆,模板保持隐藏)。 * @param visible 是否显示 */ setProjectileVisible(visible: boolean): void { if (this._projectile?.isValid) this._projectile.active = visible; } /** * 藏 weaponType4,显示「武器弓-攻击」,皮肤与当前武器一致。 * @returns 是否显示成功 */ showBowAtk(): boolean { if (this._type !== WEAPON_TYPE_BOW) return false; this._ensureBowAtkPrefab(); if (!this._atkRoot?.isValid || !this._drawAnim?.isValid) { console.log('[HeroWeaponMount] 武器弓-攻击未就绪', this._weaponId); return false; } this._applyBowAtkSkin(); this.setProjectileVisible(false); this.syncBowAtkPivot(); if (!this._bowAtkShowing && this._atkRoot?.isValid) { this._atkRoot.angle = BOW_ATK_REST_ANGLE; } if (this._root?.isValid) this._root.active = false; this._atkRoot.active = true; this._bowAtkShowing = true; this._holdDrawSetupPose(); return true; } /** * 藏「武器弓-攻击」。 * @param showIdleWeapon 是否重新显示 weaponType4 */ hideBowAtk(showIdleWeapon: boolean): void { this._restoreDrawTimeScale(); if (this._atkRoot?.isValid && this._bowAtkShowing) { this._atkRoot.active = false; this._atkRoot.angle = BOW_ATK_REST_ANGLE; } this._bowAtkShowing = false; this.setProjectileVisible(false); if (showIdleWeapon && this._root?.isValid) this._root.active = true; } /** * 播拉弓(默认「攻击」)。 * @param timeScale 相对倍速 * @param onComplete 播完 * @param animation 动画名 * @returns 是否起播 */ playBowDraw(timeScale: number, onComplete?: () => void, animation: string = SPINE_ATTACK_DEFAULT): boolean { const anim = this._drawAnim; if (!anim?.isValid) { console.log('[HeroWeaponMount] 缺少拉弓动画', this._weaponId); onComplete?.(); return false; } const name = String(animation ?? '').trim() || SPINE_ATTACK_DEFAULT; BattleSpinePlayback.resetTracks(anim); anim.setToSetupPose(); return BattleSpinePlayback.play(anim, name, { loop: false, timeScale, onComplete }); } /** 攻击段定格无箭末帧 */ freezeBowDraw(): void { if (!this._drawAnim?.isValid) return; BattleSpinePlayback.clearPendingComplete(this._drawAnim); this._drawAnim.timeScale = 0; } /** * 只改「武器弓-攻击」根节点角度,不改拉弓动画本地角。 * @param deg Cocos 2D 角(0 右 / 90 上 / 180 左) */ setBowAtkAim(deg: number): void { if (this._atkRoot?.isValid) this._atkRoot.angle = deg; } /** 攻效位置跟随待机武器根 */ syncBowAtkPivot(): void { if (!this._atkRoot?.isValid || !this._root?.isValid) return; this._atkRoot.setPosition(this._root.position); if (this._atkRoot.parent !== this._root.parent && this._root.parent?.isValid) { this._atkRoot.parent = this._root.parent; } } /** 回收武器节点 */ release(): void { if (this._skinAnim?.isValid) BattleSpinePlayback.clearPendingComplete(this._skinAnim); if (this._arrowAnim?.isValid) BattleSpinePlayback.clearPendingComplete(this._arrowAnim); if (this._drawAnim?.isValid) BattleSpinePlayback.clearPendingComplete(this._drawAnim); if (this._projectileAnim?.isValid) BattleSpinePlayback.clearPendingComplete(this._projectileAnim); this.resetAim(); this._disposeBowAtk(); if (this._root?.isValid) gg.res.putNode(this._root); this._root = null; this._aim = null; this._skinAnim = null; this._arrow = null; this._arrowAnim = null; this._weaponId = 0; this._type = 0; this._skinName = ''; this._bowAtkShowing = false; } /** 绑定弓「箭」模板 */ private _bindArrow(): void { this._arrow = null; this._arrowAnim = null; if (this._type !== WEAPON_TYPE_BOW || !this._aim?.isValid) return; const arrow = this._aim.getChildByName(ARROW_NODE); if (!arrow?.isValid) { console.log('[HeroWeaponMount] 弓缺少节点', ARROW_NODE, 'weaponId=', this._weaponId); return; } const anim = arrow.getComponent(sp.Skeleton) ?? arrow.getComponentInChildren(sp.Skeleton); if (!anim?.isValid) { console.log('[HeroWeaponMount] 箭缺少 Spine', 'weaponId=', this._weaponId); return; } this._arrow = arrow; this._arrowAnim = anim; } /** * 给 Spine 套 Weapon.skin。 * @param anim 目标骨架 * @param skinName 皮肤名 * @param label 告警前缀 */ private _setSpineSkin(anim: sp.Skeleton, skinName: string, label: string): void { if (anim.skeletonData) ensureSpineSkeletonDataRuntime(anim.skeletonData); const resolved = applyWeaponSpineSkin(anim, skinName, `[HeroWeaponMount] ${label}`); if (!resolved) { console.log('[HeroWeaponMount]', label, '无此皮肤', skinName, 'weaponId=', this._weaponId); } } private _applySkin(skinRaw: unknown): void { const skinName = spineSkinName(skinRaw).trim(); if (!skinName) { console.log('[HeroWeaponMount] Weapon.skin 为空', this._weaponId); return; } this._skinName = skinName; if (this._skinAnim?.isValid) this._setSpineSkin(this._skinAnim, skinName, '武器'); if (this._arrowAnim?.isValid) this._setSpineSkin(this._arrowAnim, skinName, '箭'); this._applyBowAtkSkin(); } /** 拉弓动画与投掷物/anim 套当前 Weapon.skin */ private _applyBowAtkSkin(): void { const skinName = this._skinName; if (!skinName) return; if (this._drawAnim?.isValid) this._setSpineSkin(this._drawAnim, skinName, '拉弓动画'); if (this._projectileAnim?.isValid) this._setSpineSkin(this._projectileAnim, skinName, '投掷物'); } /** 加载「武器弓-攻击」并挂到待机武器同级 */ private _ensureBowAtkPrefab(): void { if (this._type !== WEAPON_TYPE_BOW) return; if (this._atkRoot?.isValid) { this._bindBowAtkNodes(); this._attachBowAtk(); return; } const atk = gg.res.getNode(BOW_ATK_PREFAB_PATH, BOW_ATK_PREFAB, GBundle.BattleHero); if (!atk?.isValid) { console.log('[HeroWeaponMount] 预制体未加载', BOW_ATK_PREFAB, 'weaponId=', this._weaponId); this._atkRoot = null; this._drawAnim = null; this._projectile = null; this._projectileAnim = null; return; } this._atkRoot = atk; this._bindBowAtkNodes(); this._attachBowAtk(); atk.active = false; this.setProjectileVisible(false); if (!this._drawAnim?.isValid) { console.log('[HeroWeaponMount] 攻效缺少节点', NODE_DRAW, 'weaponId=', this._weaponId); } if (!this._projectile?.isValid) { console.log('[HeroWeaponMount] 攻效缺少节点', NODE_PROJECTILE, 'weaponId=', this._weaponId); } else if (!this._projectileAnim?.isValid) { console.log('[HeroWeaponMount] 投掷物缺少节点', NODE_PROJECTILE_ANIM, 'weaponId=', this._weaponId); } } /** 绑定拉弓 / 投掷物 */ private _bindBowAtkNodes(): void { const atk = this._atkRoot; if (!atk?.isValid) { this._drawAnim = null; this._projectile = null; this._projectileAnim = null; return; } const draw = atk.getChildByName(NODE_DRAW); this._drawAnim = draw?.getComponent(sp.Skeleton) ?? null; this._projectile = atk.getChildByName(NODE_PROJECTILE); this._projectileAnim = this._projectile?.getChildByName(NODE_PROJECTILE_ANIM)?.getComponent(sp.Skeleton) ?? this._projectile?.getComponentInChildren(sp.Skeleton) ?? null; } /** 攻效与 weaponType4 同父、同位 */ private _attachBowAtk(): void { const atk = this._atkRoot; const root = this._root; if (!atk?.isValid || !root?.isValid) return; const parent = root.parent; if (parent?.isValid && atk.parent !== parent) atk.parent = parent; atk.setPosition(root.position); } /** 瞄准前停在 setup,避免预制体默认循环播攻击 */ private _holdDrawSetupPose(): void { if (!this._drawAnim?.isValid) return; BattleSpinePlayback.resetTracks(this._drawAnim); this._drawAnim.setToSetupPose(); this._drawAnim.timeScale = 0; } /** 解除拉弓定格 */ private _restoreDrawTimeScale(): void { if (!this._drawAnim?.isValid) return; BattleSpinePlayback.clearPendingComplete(this._drawAnim); this._drawAnim.timeScale = 1; } /** 回池攻效,避免嵌进武器节点池 */ private _disposeBowAtk(): void { this._restoreDrawTimeScale(); if (this._atkRoot?.isValid) gg.res.putNode(this._atkRoot); this._atkRoot = null; this._drawAnim = null; this._projectile = null; this._projectileAnim = null; this._bowAtkShowing = false; } /** * 设置 weapon 节点 scale.x,保留 y/z。 * @param x scale.x */ private _setAimScaleX(x: number): void { if (!this._aim?.isValid) return; this._aim.setScale(x, this._aim.scale.y, this._aim.scale.z); } /** 刀、木:只左右翻转,不旋转 */ private _isFlipType(): boolean { return this._type === WEAPON_TYPE_SWORD || this._type === WEAPON_TYPE_WOOD; } /** * 技能动画 Spine:优先 weapon/anim 或根 anim,否则 weapon/skin。 * @param root 武器预制体根 */ private _resolveAnimSpine(root: Node): sp.Skeleton | null { const aim = this._aim; const node = (aim?.isValid ? aim.getChildByName(ANIM_NODE) : null) ?? root.getChildByName(ANIM_NODE) ?? (aim?.isValid ? aim.getChildByName(SKIN_NODE) : null); return node?.getComponent(sp.Skeleton) ?? null; } private _playNamed( name: string, loop: boolean, timeScale: number, onComplete?: () => void, ): boolean { const anim = this._skinAnim; if (!anim?.isValid) { onComplete?.(); return false; } return BattleSpinePlayback.play(anim, name, { loop, timeScale, onComplete }); } }