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.
 
 
 
 
AdLike2048/assets/script/battle/view/HeroWeaponMount.ts

618 lines
23 KiB

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<string> = [
'',
'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<ITableWeapon>(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 });
}
}