|
|
|
|
@ -28,6 +28,14 @@ const STAFF_PROJECTILE_RELEASE_AT = 0.6333; |
|
|
|
|
const STAFF_PROJECTILE_MIN_AT = 0.3; |
|
|
|
|
/** 法杖出手特效骨(水弹/冰锥/暴风雪共用「挥砍」) */ |
|
|
|
|
const STAFF_RELEASE_BONE = '挥砍'; |
|
|
|
|
/** 法杖头部发光骨(水弹/冰锥发射起点) */ |
|
|
|
|
const STAFF_MUZZLE_BONE = '聚集光能'; |
|
|
|
|
/** |
|
|
|
|
* 相对发光骨的本地微调(Spine 节点空间像素,正 X 右、正 Y 上)。 |
|
|
|
|
* 用于对齐杖头水晶发光中心;骨骼缺失时作为相对 skin 节点的发射偏移。 |
|
|
|
|
*/ |
|
|
|
|
const STAFF_MUZZLE_OFFSET_X = 0; |
|
|
|
|
const STAFF_MUZZLE_OFFSET_Y = 0; |
|
|
|
|
/** |
|
|
|
|
* weaponType4 / 弓类武器+专属技能:「攻击」「攻击2」「攻击3」槽位「箭矢*」附件清空帧。 |
|
|
|
|
* 整段动画 1.5s,卸箭=拉完放箭时刻(非动画结束)。 |
|
|
|
|
@ -84,10 +92,9 @@ const WATER_BALL_PREFAB = '水弹'; |
|
|
|
|
/** battleHero/prefab/skill/冰锥(水属性冰锥技能发射物) */ |
|
|
|
|
const ICE_CONE_PREFAB = '冰锥'; |
|
|
|
|
/** |
|
|
|
|
* 水弹/冰锥「飞行水弹」原图约 322×122;缩到约一格可视大小。 |
|
|
|
|
* 缩放比 = CELL_SIZE / 原图像素宽。 |
|
|
|
|
* 缩放比
|
|
|
|
|
*/ |
|
|
|
|
const WATER_BALL_SCALE = BattleMapSpec.CELL_SIZE / 322; |
|
|
|
|
const WATER_BALL_SCALE =1; |
|
|
|
|
/** 水弹飞行像素/秒(与弓箭同量级) */ |
|
|
|
|
const WATER_BALL_SPEED = BOW_ATK_ARROW_SPEED; |
|
|
|
|
/** battleHero/prefab/skill/飞剑(武器主动) */ |
|
|
|
|
@ -208,15 +215,17 @@ export interface CastAnimParams { |
|
|
|
|
followWeapon: boolean; |
|
|
|
|
/** 主目标(瞄准) */ |
|
|
|
|
primaryTarget: EntityId | null; |
|
|
|
|
/** Ability 槽:normal 跟攻速;hero/weapon/monster 固定 1 */ |
|
|
|
|
/** Ability 槽:normal 按攻击间隔;hero/weapon/monster 按技能 CD */ |
|
|
|
|
skillKind?: string; |
|
|
|
|
/** 主动技能配表 CD(秒);缺省且非普攻时不加速 */ |
|
|
|
|
skillCdSec?: number; |
|
|
|
|
/** 跳过默认单箭(atk_type 12/13 自行发射) */ |
|
|
|
|
skipBowArrow?: boolean; |
|
|
|
|
/** atk_type=4 非弓:出手立刻飞出玉帝飞剑 */ |
|
|
|
|
throwWeapon?: boolean; |
|
|
|
|
/** 弓对准目标连线,无抛物线偏角(散射) */ |
|
|
|
|
aimDirect?: boolean; |
|
|
|
|
/** 覆盖攻速:动画与瞄准倍速 */ |
|
|
|
|
/** 显式倍速覆盖(箭雨等已算好的);未传则按动画时长与间隔/CD 对齐 */ |
|
|
|
|
animTimeScale?: number; |
|
|
|
|
/** 播完不收招待机(连发由外部收招) */ |
|
|
|
|
holdCasting?: boolean; |
|
|
|
|
@ -275,6 +284,8 @@ export class SkillVisualChannel { |
|
|
|
|
private readonly _arrowFrom = new Vec3(); |
|
|
|
|
private readonly _arrowTo = new Vec3(); |
|
|
|
|
private readonly _arrowCtrl = new Vec3(); |
|
|
|
|
/** 法杖发射点本地缓冲(骨坐标 + 偏移) */ |
|
|
|
|
private readonly _muzzleLocal = new Vec3(); |
|
|
|
|
private readonly _scatterDegs: number[] = []; |
|
|
|
|
/** 本段释放弓对准目标连线 */ |
|
|
|
|
private _aimDirect = false; |
|
|
|
|
@ -382,16 +393,45 @@ export class SkillVisualChannel { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 释放动画倍速:hero/weapon/monster 固定 1;普攻跟攻速;显式 override 优先。 |
|
|
|
|
* @param caster 施法实体 |
|
|
|
|
* @param skillKind Ability 槽 |
|
|
|
|
* @param override 显式倍速 |
|
|
|
|
* 间隔/CD 与动画时长对齐:仅当间隔短于动画时加速,从不减速。 |
|
|
|
|
* @param animDurationSec 动画原始时长;无效则 1 |
|
|
|
|
* @param intervalSec 攻击间隔或技能 CD;≤0 则 1 |
|
|
|
|
*/ |
|
|
|
|
static fitAnimTimeScale(animDurationSec: number | null | undefined, intervalSec: number): number { |
|
|
|
|
const dur = Number(animDurationSec); |
|
|
|
|
if (!(dur > 0)) return 1; |
|
|
|
|
const gap = Number(intervalSec); |
|
|
|
|
if (!(gap > 0)) return 1; |
|
|
|
|
return Math.max(1, dur / gap); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 释放动画倍速:普攻用 1/攻速;主动用最终 CD;仅加速不减速。 |
|
|
|
|
* @param opts.caster 施法实体 |
|
|
|
|
* @param opts.skillKind Ability 槽 |
|
|
|
|
* @param opts.animDurationSec 动画时长 |
|
|
|
|
* @param opts.skillCdSec 主动技能配表 CD |
|
|
|
|
* @param opts.override 显式倍速 |
|
|
|
|
*/ |
|
|
|
|
static resolveCastAnimTimeScale(caster: EntityId, skillKind?: string, override?: number): number { |
|
|
|
|
if (override != null && override > 0) return override; |
|
|
|
|
const kind = String(skillKind ?? ''); |
|
|
|
|
if (kind === 'hero' || kind === 'weapon' || kind === 'monster') return 1; |
|
|
|
|
return Math.max(0.01, SkillPresentationEnv.getFinalAttackSpeed(caster)); |
|
|
|
|
static resolveCastAnimTimeScale(opts: { |
|
|
|
|
caster: EntityId; |
|
|
|
|
skillKind?: string; |
|
|
|
|
animDurationSec?: number | null; |
|
|
|
|
skillCdSec?: number | null; |
|
|
|
|
override?: number; |
|
|
|
|
}): number { |
|
|
|
|
if (opts.override != null && opts.override > 0) return opts.override; |
|
|
|
|
const kind = String(opts.skillKind ?? ''); |
|
|
|
|
let intervalSec: number; |
|
|
|
|
if (kind === 'hero' || kind === 'weapon' || kind === 'monster') { |
|
|
|
|
const baseCd = Math.max(0, Number(opts.skillCdSec) || 0); |
|
|
|
|
if (!(baseCd > 0)) return 1; |
|
|
|
|
intervalSec = SkillPresentationEnv.getFinalCooldown(opts.caster, baseCd); |
|
|
|
|
} else { |
|
|
|
|
const aspd = Math.max(0.01, SkillPresentationEnv.getFinalAttackSpeed(opts.caster)); |
|
|
|
|
intervalSec = 1 / aspd; |
|
|
|
|
} |
|
|
|
|
return SkillVisualChannel.fitAnimTimeScale(opts.animDurationSec, intervalSec); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 绑定施法实体(用于首字格定位) */ |
|
|
|
|
@ -471,6 +511,29 @@ export class SkillVisualChannel { |
|
|
|
|
return !!this._weapon.root?.active; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 读本次释放动画原始时长(跟武器优先,否则字形「攻击」)。 |
|
|
|
|
* @param weaponAnim Skill.animation |
|
|
|
|
* @param followWeapon 是否跟播武器 |
|
|
|
|
*/ |
|
|
|
|
private _peekCastAnimDuration(weaponAnim: string, followWeapon: boolean): number | null { |
|
|
|
|
if (followWeapon) { |
|
|
|
|
const sk = this._weapon.drawAnim ?? this._weapon.skinAnim; |
|
|
|
|
if (sk && weaponAnim) { |
|
|
|
|
const dur = BattleSpinePlayback.getAnimDuration(sk, weaponAnim); |
|
|
|
|
if (dur != null) return dur; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if (!this._attackOnlyWeapon) { |
|
|
|
|
const glyph = this._skin?.anim ?? this._glyphAnim; |
|
|
|
|
if (glyph) { |
|
|
|
|
const dur = BattleSpinePlayback.getAnimDuration(glyph, GLYPH_CAST_ANIM); |
|
|
|
|
if (dur != null) return dur; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 取消字形/武器上未触发的释放完成回调 */ |
|
|
|
|
clearCastCompletes(): void { |
|
|
|
|
BattleSpinePlayback.clearPendingComplete(this._weapon.skinAnim); |
|
|
|
|
@ -520,15 +583,9 @@ export class SkillVisualChannel { |
|
|
|
|
* 释放表现:组合字形播「攻击」;五行数字不播字形攻击。 |
|
|
|
|
* 有武器时武器播 Skill.animation(缺省「攻击」)。跟随武器时等字形与武器都播完再收招。 |
|
|
|
|
* 玉帝飞剑:不播原武器攻击,直接隐藏。 |
|
|
|
|
* @returns 本段释放动画倍速(供命中延迟换算;主动技能为 1) |
|
|
|
|
* @returns 本段释放动画倍速(供命中延迟换算;≥1,仅加速) |
|
|
|
|
*/ |
|
|
|
|
playCastAnim(params: CastAnimParams): number { |
|
|
|
|
const animScale = SkillVisualChannel.resolveCastAnimTimeScale( |
|
|
|
|
params.caster, |
|
|
|
|
params.skillKind, |
|
|
|
|
params.animTimeScale, |
|
|
|
|
); |
|
|
|
|
params.animTimeScale = animScale; |
|
|
|
|
this._caster = params.caster; |
|
|
|
|
this._aimDirect = params.aimDirect === true; |
|
|
|
|
this._castAnimParams = params; |
|
|
|
|
@ -538,7 +595,17 @@ export class SkillVisualChannel { |
|
|
|
|
if (!weaponAnim) { |
|
|
|
|
console.warn('[SkillVisualChannel] Skill.animation 为空', params.caster); |
|
|
|
|
} |
|
|
|
|
const follow = params.followWeapon && this._useWeapon && this.ensureWeapon(); |
|
|
|
|
const followWanted = !!(params.followWeapon && this._useWeapon); |
|
|
|
|
if (followWanted) this.ensureWeapon(); |
|
|
|
|
const animScale = SkillVisualChannel.resolveCastAnimTimeScale({ |
|
|
|
|
caster: params.caster, |
|
|
|
|
skillKind: params.skillKind, |
|
|
|
|
animDurationSec: this._peekCastAnimDuration(weaponAnim, followWanted), |
|
|
|
|
skillCdSec: params.skillCdSec, |
|
|
|
|
override: params.animTimeScale, |
|
|
|
|
}); |
|
|
|
|
params.animTimeScale = animScale; |
|
|
|
|
const follow = followWanted && this.ensureWeapon(); |
|
|
|
|
const playGlyph = !this._attackOnlyWeapon; |
|
|
|
|
if (follow) { |
|
|
|
|
this._cancelRestHold(); |
|
|
|
|
@ -806,8 +873,7 @@ export class SkillVisualChannel { |
|
|
|
|
if (!params) return; |
|
|
|
|
if (!params.holdCasting) { |
|
|
|
|
this._weaponCasting = false; |
|
|
|
|
const aspd = Math.max(0.01, SkillPresentationEnv.getFinalAttackSpeed(params.caster)); |
|
|
|
|
const restScale = params.animTimeScale != null && params.animTimeScale > 0 ? params.animTimeScale : aspd; |
|
|
|
|
const restScale = params.animTimeScale != null && params.animTimeScale > 0 ? params.animTimeScale : 1; |
|
|
|
|
if (this._isBowWeapon() && this._weapon.bowAtkShowing) { |
|
|
|
|
this._weapon.freezeBowDraw(); |
|
|
|
|
// 弓与枪相同:连击窗保持当前角,超时再回待机
|
|
|
|
|
@ -1002,6 +1068,41 @@ export class SkillVisualChannel { |
|
|
|
|
return Math.max(STAFF_PROJECTILE_MIN_AT, releaseAt); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 水弹/冰锥发射世界坐标:法杖「聚集光能」骨 + 本地偏移;无骨则 skin/aim 原点 + 同偏移。 |
|
|
|
|
* @param out 输出世界坐标 |
|
|
|
|
* @param caster 施法者(无武器节点时回落格心) |
|
|
|
|
*/ |
|
|
|
|
private _fillStaffProjectileFromWorld(out: Vec3, caster: EntityId): boolean { |
|
|
|
|
const sk = this._weapon.skinAnim; |
|
|
|
|
if (sk?.isValid) { |
|
|
|
|
const bone = typeof sk.findBone === 'function' ? sk.findBone(STAFF_MUZZLE_BONE) : null; |
|
|
|
|
const ui = sk.node?.getComponent(UITransform); |
|
|
|
|
if (bone && ui?.isValid) { |
|
|
|
|
this._muzzleLocal.set(bone.worldX + STAFF_MUZZLE_OFFSET_X, bone.worldY + STAFF_MUZZLE_OFFSET_Y, 0); |
|
|
|
|
ui.convertToWorldSpaceAR(this._muzzleLocal, out); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
const fromNode = sk?.node ?? this._weapon.aim ?? this._weapon.root; |
|
|
|
|
if (fromNode?.isValid) { |
|
|
|
|
const ui = fromNode.getComponent(UITransform); |
|
|
|
|
if (ui?.isValid) { |
|
|
|
|
this._muzzleLocal.set(STAFF_MUZZLE_OFFSET_X, STAFF_MUZZLE_OFFSET_Y, 0); |
|
|
|
|
ui.convertToWorldSpaceAR(this._muzzleLocal, out); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
out.set(fromNode.worldPosition); |
|
|
|
|
out.x += STAFF_MUZZLE_OFFSET_X; |
|
|
|
|
out.y += STAFF_MUZZLE_OFFSET_Y; |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
const grid = SkillPresentationEnv.getCasterGridCenter(caster); |
|
|
|
|
if (!grid) return false; |
|
|
|
|
out.set(grid); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 法杖水弹/冰锥首次出手延迟(逻辑秒):动画关键帧 / 攻速倍速。 |
|
|
|
|
* @param animScale playCastAnim 返回的倍速 |
|
|
|
|
@ -1219,11 +1320,7 @@ export class SkillVisualChannel { |
|
|
|
|
} |
|
|
|
|
this._weaponCasting = true; |
|
|
|
|
this._weapon.setVisible(true); |
|
|
|
|
const fromNode = this._weapon.skinAnim?.node ?? this._weapon.aim ?? this._weapon.root; |
|
|
|
|
const fromWorld = fromNode?.isValid |
|
|
|
|
? fromNode.worldPosition |
|
|
|
|
: SkillPresentationEnv.getCasterGridCenter(caster); |
|
|
|
|
if (!fromWorld) { |
|
|
|
|
if (!this._fillStaffProjectileFromWorld(this._arrowFrom, caster)) { |
|
|
|
|
console.log('[SkillVisualChannel] 冰锥无起点'); |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
@ -1233,7 +1330,6 @@ export class SkillVisualChannel { |
|
|
|
|
console.log('[SkillVisualChannel] 冰锥无目标坐标', target); |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
this._arrowFrom.set(fromWorld); |
|
|
|
|
this._arrowTo.set(to); |
|
|
|
|
const dist = Vec3.distance(this._arrowFrom, this._arrowTo); |
|
|
|
|
const duration = Math.max(0.12, dist / WATER_BALL_SPEED); |
|
|
|
|
@ -1537,11 +1633,7 @@ export class SkillVisualChannel { |
|
|
|
|
this._weaponCasting = true; |
|
|
|
|
// 出手瞬间再确保法杖可见(避免在飞弹道占 _weaponFlies 时藏武器)
|
|
|
|
|
this._weapon.setVisible(true); |
|
|
|
|
const fromNode = this._weapon.skinAnim?.node ?? this._weapon.aim ?? this._weapon.root; |
|
|
|
|
const fromWorld = fromNode?.isValid |
|
|
|
|
? fromNode.worldPosition |
|
|
|
|
: SkillPresentationEnv.getCasterGridCenter(caster); |
|
|
|
|
if (!fromWorld) { |
|
|
|
|
if (!this._fillStaffProjectileFromWorld(this._arrowFrom, caster)) { |
|
|
|
|
console.log('[SkillVisualChannel] 水弹无起点'); |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
@ -1551,7 +1643,6 @@ export class SkillVisualChannel { |
|
|
|
|
console.log('[SkillVisualChannel] 水弹无目标坐标', target); |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
this._arrowFrom.set(fromWorld); |
|
|
|
|
this._arrowTo.set(to); |
|
|
|
|
const dist = Vec3.distance(this._arrowFrom, this._arrowTo); |
|
|
|
|
const duration = Math.max(0.12, dist / WATER_BALL_SPEED); |
|
|
|
|
|