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