From eaf613c5e88e414cb7a106da21048def7743d608 Mon Sep 17 00:00:00 2001 From: humingxing <930621236@qq.com> Date: Thu, 8 Oct 2026 16:01:20 +0800 Subject: [PATCH] 11 --- assets/bundles/first/config/Skill.json | 10 +- .../config/baseAtk/SkillAbilityBuilder.ts | 5 +- .../battle/config/weapon1/Weapon1BattleIds.ts | 14 ++ .../presentation/FlySwordAtkPresenter.ts | 137 ++++++++++++++++++ .../presentation/FlySwordAtkPresenter.ts.meta | 9 ++ .../battle/presentation/SkillVisualChannel.ts | 96 ++++++++++++ .../presentation/SpecialSkillPresenters.ts | 6 + assets/script/battle/runtime/BattleRuntime.ts | 4 + docs/方案-战斗技能逻辑层.md | 6 +- docs/方案-战斗表现数据驱动.md | 1 + 10 files changed, 281 insertions(+), 7 deletions(-) create mode 100644 assets/script/battle/presentation/FlySwordAtkPresenter.ts create mode 100644 assets/script/battle/presentation/FlySwordAtkPresenter.ts.meta diff --git a/assets/bundles/first/config/Skill.json b/assets/bundles/first/config/Skill.json index f38c4d3..8877cc8 100644 --- a/assets/bundles/first/config/Skill.json +++ b/assets/bundles/first/config/Skill.json @@ -563,7 +563,7 @@ "target_stage": 1, "range": 0, "atk_type": "10", - "fly": "", + "fly": "飞剑", "atk_para": "1,0,10000", "atk_buff1": "", "atk_buff2": "", @@ -585,7 +585,7 @@ "target_stage": 1, "range": 0, "atk_type": "10", - "fly": "", + "fly": "飞剑", "atk_para": "1,0,10000", "atk_buff1": "", "atk_buff2": "", @@ -651,7 +651,7 @@ "target_stage": 1, "range": 0, "atk_type": "10", - "fly": "", + "fly": "飞剑", "atk_para": "2,0.3,10000", "atk_buff1": "", "atk_buff2": "", @@ -717,7 +717,7 @@ "target_stage": 1, "range": 0, "atk_type": "10", - "fly": "", + "fly": "飞剑", "atk_para": "3,0.22,10000", "atk_buff1": "", "atk_buff2": "", @@ -783,7 +783,7 @@ "target_stage": 1, "range": 0, "atk_type": "10", - "fly": "", + "fly": "飞剑", "atk_para": "4,0.17,10000", "atk_buff1": "", "atk_buff2": "", diff --git a/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts b/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts index 9ba91f5..6cce576 100644 --- a/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts +++ b/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts @@ -6,6 +6,7 @@ import { entityHeroAbilityId, entityMonsterAbilityId, entityWeaponAbilityId, + isFlySwordSkillId, normalAbilityId, } from '../weapon1/Weapon1BattleIds'; @@ -664,7 +665,9 @@ export class SkillAbilityBuilder { // 12 散射 / 13 连发 / 17 不穿透散射 / 19 冰锥:伤害由 BowAtkHost 弹道结算,无 deferred 段 // 18 丢出独立持续伤:伤害由 ThrownCircleHost 结算,无 deferred 段 - if (atkHead === 12 || atkHead === 13 || atkHead === 17 || atkHead === 18 || atkHead === 19) { + // 飞剑主动(atk_type=10 子集):落地结算,无 deferred 段 + if (atkHead === 12 || atkHead === 13 || atkHead === 17 || atkHead === 18 || atkHead === 19 + || isFlySwordSkillId(row.id)) { return []; } diff --git a/assets/script/battle/config/weapon1/Weapon1BattleIds.ts b/assets/script/battle/config/weapon1/Weapon1BattleIds.ts index ac5135b..3afde86 100644 --- a/assets/script/battle/config/weapon1/Weapon1BattleIds.ts +++ b/assets/script/battle/config/weapon1/Weapon1BattleIds.ts @@ -75,6 +75,20 @@ export function isMonsterAbilityId(abilityId: string): boolean { return /^ability_monster_\d+_e\d+$/.test(abilityId); } +/** 飞剑武器主动 Skill.id(atk_type=10 走飞剑预制体,非连砸) */ +export const FLY_SWORD_SKILL_IDS: readonly number[] = [210101, 220101, 230101, 240101, 250101]; + +/** + * 是否为飞剑武器主动。 + * @param skillId Skill.id + */ +export function isFlySwordSkillId(skillId: number): boolean { + for (let i = 0; i < FLY_SWORD_SKILL_IDS.length; i++) { + if (FLY_SWORD_SKILL_IDS[i] === skillId) return true; + } + return false; +} + /** 塔实体 Hero 标签 */ export function weapon1Tag(heroId: number): string { return `weapon1_${heroId}`; diff --git a/assets/script/battle/presentation/FlySwordAtkPresenter.ts b/assets/script/battle/presentation/FlySwordAtkPresenter.ts new file mode 100644 index 0000000..6a9ba60 --- /dev/null +++ b/assets/script/battle/presentation/FlySwordAtkPresenter.ts @@ -0,0 +1,137 @@ +import type { EntityId } from 'db://assets/mx/module/ecs'; +import type { Skill } from '../skill/Skill'; +import type { SkillPresentationContext } from '../skill/SkillPresentationTypes'; +import type { SkillVisualChannel } from './SkillVisualChannel'; +import type { ISkillPresenter } from './SkillPresenter'; + +/** 待发射飞剑 */ +interface FlySwordPending { + /** 施法者 */ + caster: EntityId; + /** Ability id */ + abilityId: string; + /** 目标 */ + target: EntityId; + /** 伤害倍率 */ + scale: number; + /** atk_buff1 */ + buff1: string; + /** atk_buff2 */ + buff2: string; + /** 距发射(秒) */ + wait: number; + /** 表现通道 */ + channel: SkillVisualChannel; +} + +let _host: FlySwordHost | null = null; + +/** 飞剑交错发射宿主 */ +export function getFlySwordHost(): FlySwordHost { + if (!_host) _host = new FlySwordHost(); + return _host; +} + +/** + * atk_type=10 飞剑线:按间隔依次发射,落地伤由 BowAtkHost 结算。 + */ +export class FlySwordHost { + private readonly _pending: FlySwordPending[] = []; + + /** + * 排队发射。 + * @param channel 表现通道 + * @param ctx 表现上下文 + * @param scale 伤害倍率 + * @param intervalSec 邻剑间隔 + * @param maxCount 最多发射数 + */ + begin(channel: SkillVisualChannel, ctx: SkillPresentationContext, scale: number, intervalSec: number, maxCount: number): void { + const buff1 = String(ctx.params.buff1 ?? ''); + const buff2 = String(ctx.params.buff2 ?? ''); + const targets = ctx.targets; + let launched = 0; + for (let i = 0; i < targets.length && launched < maxCount; i++) { + const tid = targets[i]; + if (!(tid > 0)) continue; + this._pending.push({ + caster: ctx.caster, + abilityId: ctx.abilityId, + target: tid, + scale, + buff1, + buff2, + wait: launched * Math.max(0, intervalSec), + channel, + }); + launched += 1; + } + if (launched <= 0) { + console.log('[FlySword] 无有效目标', ctx.params.skillId); + return; + } + this.tick(0); + } + + /** + * 推进排队发射。 + * @param dt 逻辑秒 + */ + tick(dt: number): void { + if (this._pending.length <= 0) return; + for (let i = this._pending.length - 1; i >= 0; i--) { + const p = this._pending[i]; + p.wait -= dt; + if (p.wait > 0) continue; + this._pending.splice(i, 1); + if (!p.channel.launchFlySword(p.caster, p.abilityId, p.target, p.scale, p.buff1, p.buff2)) { + console.log('[FlySword] 发射失败', p.target); + } + } + } + + /** + * 卸下施法者排队。 + * @param caster 塔实体 + */ + cancelCaster(caster: EntityId): void { + for (let i = this._pending.length - 1; i >= 0; i--) { + if (this._pending[i].caster === caster) this._pending.splice(i, 1); + } + } + + /** 清空全部排队 */ + abortAll(): void { + this._pending.length = 0; + } +} + +/** + * 飞剑武器主动:瞬发,实例化飞剑.prefab 飞向目标,落地结算。 + */ +export class FlySwordAtkPresenter implements ISkillPresenter { + present(ctx: SkillPresentationContext, host: Skill): void { + if (!ctx.isAlive) { + ctx.finish(); + return; + } + const raw = String(ctx.params.atkPara ?? '').split(','); + const count = Math.floor(Number(String(raw[0] ?? '').trim()) || 0); + const intervalSec = Number(String(raw[1] ?? '').trim()); + const wan = Number(String(raw[2] ?? '').trim()); + if (!(count > 0) || !Number.isFinite(intervalSec) || intervalSec < 0 || !Number.isFinite(wan) || !(wan > 0)) { + console.log('[FlySwordAtkPresenter] atk_para 无效', ctx.params.skillId, ctx.params.atkPara); + ctx.finish(); + return; + } + const skillKind = String(ctx.params.skillKind ?? ''); + const followWeapon = skillKind === 'weapon' || host.visualChannel.useWeapon; + if (followWeapon) host.visualChannel.ensureWeapon(); + const seq = host.beginPresentationCast(ctx); + if (!(seq > 0)) return; + host.disarmAnimComplete(); + getFlySwordHost().begin(host.visualChannel, ctx, wan / 10000, intervalSec, count); + ctx.releasePresentationLock(); + host.finishPresentationCast(seq, false); + } +} diff --git a/assets/script/battle/presentation/FlySwordAtkPresenter.ts.meta b/assets/script/battle/presentation/FlySwordAtkPresenter.ts.meta new file mode 100644 index 0000000..a976162 --- /dev/null +++ b/assets/script/battle/presentation/FlySwordAtkPresenter.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "aa83d9ec-834f-4235-8777-652362698e71", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/battle/presentation/SkillVisualChannel.ts b/assets/script/battle/presentation/SkillVisualChannel.ts index 1b54020..77bfa82 100644 --- a/assets/script/battle/presentation/SkillVisualChannel.ts +++ b/assets/script/battle/presentation/SkillVisualChannel.ts @@ -79,6 +79,10 @@ const WATER_BALL_PREFAB = '水弹'; const WATER_BALL_SCALE = BattleMapSpec.CELL_SIZE / 322; /** 水弹飞行像素/秒(与弓箭同量级) */ const WATER_BALL_SPEED = BOW_ATK_ARROW_SPEED; +/** battleHero/prefab/skill/飞剑(武器主动) */ +const FLY_SWORD_PREFAB = '飞剑'; +/** 飞剑默认 Spine 动画 */ +const FLY_SWORD_ANIM = '飞剑'; /** battleHero/prefab/skill/玉帝飞剑 */ const YUDI_FLY_SWORD_PREFAB = '玉帝飞剑'; /** 飞剑预制体默认动画(带光效) */ @@ -1189,6 +1193,60 @@ export class SkillVisualChannel { return this._launchWaterBall(caster, abilityId, target, scale, buff1, buff2, aim); } + /** + * 飞剑武器主动:实例化「飞剑」直线飞向目标,落地结算。 + * @param caster 施法者 + * @param abilityId Ability id + * @param target 锁定目标 + * @param scale 伤害倍率 + * @param buff1 atk_buff1 + * @param buff2 atk_buff2 + * @returns 是否发出 + */ + launchFlySword(caster: EntityId, abilityId: string, target: EntityId, scale: number, buff1: string, buff2: string): boolean { + const fromNode = this._weapon.skinAnim?.node ?? this._weapon.aim ?? this._weapon.root; + const fromWorld = fromNode?.isValid + ? fromNode.worldPosition + : SkillPresentationEnv.getCasterGridCenter(caster); + if (!fromWorld) { + console.log('[SkillVisualChannel] 飞剑无起点'); + return false; + } + const to = target > 0 ? SkillPresentationEnv.getTargetWorldPos(target) : null; + if (!to) { + 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); + const node = this._spawnFlySwordPrefab(this._arrowFrom); + if (!node) return false; + SkillTransformDriver.animateParabola({ + node, + duration, + fromWorld: this._arrowFrom, + toWorld: this._arrowTo, + arcHeight: 0, + easing: SkillTransformDriver.easeLinear, + faceTangent: true, + }); + SkillTransformDriver.stepNow(); + const land = WaterBallRecycle.obtain(this, node); + getBowAtkHost().spawnVolley({ + caster, + target, + abilityId, + wait: duration, + scale, + buff1, + buff2, + onLand: land.onLand, + }); + return true; + } + launchVolleyShot(caster: EntityId, abilityId: string, target: EntityId, scale: number, buff1: string, buff2: string, shotIndex: number, aimWorld: Readonly): boolean { if (this._isStaffWeapon()) { return this._launchWaterBall(caster, abilityId, target, scale, buff1, buff2, aimWorld); @@ -1451,6 +1509,44 @@ export class SkillVisualChannel { return true; } + /** + * 取出「飞剑」挂到飞行层并播默认动画。 + * @param fromWorld 起点 + */ + private _spawnFlySwordPrefab(fromWorld: Readonly): Node | null { + const flyLayer = SkillPresentationEnv.flyLayer; + if (!flyLayer?.isValid) { + console.log('[SkillVisualChannel] 飞剑无飞行层'); + return null; + } + let node: Node | null = null; + try { + node = gg.res.getNode(GPath.HeroSkillPrefab(), FLY_SWORD_PREFAB, GBundle.BattleHero); + } catch (e) { + console.log('[SkillVisualChannel] 飞剑加载异常', String(e)); + return null; + } + if (!node?.isValid) { + console.log('[SkillVisualChannel] 飞剑预制体未加载'); + return null; + } + flyLayer.active = true; + node.parent = flyLayer; + node.active = true; + node.layer = flyLayer.layer; + node.setScale(1, 1, 1); + const flyUt = flyLayer.getComponent(UITransform); + if (flyUt) node.setPosition(flyUt.convertToNodeSpaceAR(fromWorld)); + else node.setWorldPosition(fromWorld); + node.setSiblingIndex(flyLayer.children.length - 1); + const sk = node.getComponentInChildren(sp.Skeleton); + if (sk?.isValid) { + BattleSpinePlayback.play(sk, FLY_SWORD_ANIM, { loop: true }); + } + this._weaponFlies.push(node); + return node; + } + /** * 取出「水弹」挂到飞行层。 * @param fromWorld 起点 diff --git a/assets/script/battle/presentation/SpecialSkillPresenters.ts b/assets/script/battle/presentation/SpecialSkillPresenters.ts index 9971952..051932b 100644 --- a/assets/script/battle/presentation/SpecialSkillPresenters.ts +++ b/assets/script/battle/presentation/SpecialSkillPresenters.ts @@ -9,6 +9,8 @@ import { ExtraStrikePresenter } from './ExtraStrikePresenter'; import { RingProjectilesPresenter } from './RingProjectilesPresenter'; import { SpearTossPresenter } from './SpearTossPresenter'; import { PagodaSmashPresenter } from './PagodaSmashPresenter'; +import { FlySwordAtkPresenter } from './FlySwordAtkPresenter'; +import { FLY_SWORD_SKILL_IDS } from '../config/weapon1/Weapon1BattleIds'; import { Gong } from '../heros/Gong'; import { Gun } from '../heros/Gun'; import { Qiang } from '../heros/Qiang'; @@ -125,6 +127,10 @@ export function ensureSpecialSkillPresenters(): void { } router.registerSkillId(PAGODA_SKILL_ID, new PagodaSmashPresenter()); router.registerSkillId(GUN_RING_SKILL_ID, new GunRingAtkPresenter()); + const flySword = new FlySwordAtkPresenter(); + for (let i = 0; i < FLY_SWORD_SKILL_IDS.length; i++) { + router.registerSkillId(FLY_SWORD_SKILL_IDS[i], flySword); + } // 覆盖内置 atk_type=16:棍宿主播攻效,其余走通用多段 router.registerAtkType(16, new FanMultiHitAtkPresenter()); // 覆盖内置 atk_type 12/17:弓宿主散射走 Gong 自身投掷物,其余通用 diff --git a/assets/script/battle/runtime/BattleRuntime.ts b/assets/script/battle/runtime/BattleRuntime.ts index 34a731d..ad4bbe9 100644 --- a/assets/script/battle/runtime/BattleRuntime.ts +++ b/assets/script/battle/runtime/BattleRuntime.ts @@ -39,6 +39,7 @@ import { getArrowRainHost } from '../presentation/ArrowRainPresenter'; import { getExtraRingHost } from '../presentation/RingProjectilesPresenter'; import { getExtraStrikeHost } from '../presentation/ExtraStrikePresenter'; import { getThrownCircleHost } from '../presentation/ThrownCircleAtkPresenter'; +import { getFlySwordHost } from '../presentation/FlySwordAtkPresenter'; import { getSpearTossHost } from '../presentation/SpearTossPresenter'; import { getPagodaSmashHost } from '../presentation/PagodaSmashPresenter'; import { getStaffSmashHost } from '../presentation/StaffSmashPresenter'; @@ -589,6 +590,7 @@ export class BattleRuntime implements IBattleRuntimeHandle { getTowerExpHost().forget(e.entity); getTowerLoadoutService().removeEntity(e.entity); getBowAtkHost().cancelCaster(e.entity); + getFlySwordHost().cancelCaster(e.entity); getBattleGameGlobalAttackSpeedHost().forgetEntity(e.entity); getBattleGameGlobalAttackRangeHost().forgetEntity(e.entity); } @@ -924,6 +926,7 @@ export class BattleRuntime implements IBattleRuntimeHandle { getExtraRingHost().tick(scaledDt); getExtraStrikeHost().tick(scaledDt); getThrownCircleHost().tick(scaledDt); + getFlySwordHost().tick(scaledDt); return; default: return; @@ -1166,6 +1169,7 @@ export class BattleRuntime implements IBattleRuntimeHandle { getExtraRingHost().abortAll(); getExtraStrikeHost().abortAll(); getThrownCircleHost().abortAll(); + getFlySwordHost().abortAll(); getSpearTossHost().abortAll(); getPagodaSmashHost().abortAll(); getStaffSmashHost().abortAll(); diff --git a/docs/方案-战斗技能逻辑层.md b/docs/方案-战斗技能逻辑层.md index 16a88e5..1d2f23d 100644 --- a/docs/方案-战斗技能逻辑层.md +++ b/docs/方案-战斗技能逻辑层.md @@ -113,7 +113,10 @@ Hero(合成等级 level) ### atk_type 10 连续多个单体 -`atk_para`=`次数,间隔秒,伤害万分比`(例如如意金箍棒 20501 `8,0.2,25000` = 连砸 8 次、间隔 0.2s、每次 250%)。`Skill.range` 内按威胁锁最多 N 个单体;按间隔砸满 N 次,每次只打一个(不足则循环仍存活目标)。目标全死则提前结束伤害,已生成的砸击实例播完再收招。每次砸是独立结算与独立武器实例(克隆 `weaponType*`,播 `Skill.animation`,互不打断)。从原武器处朝向目标,按砸出帧 `击中光` 到目标的距离沿 X 拉伸。首次命中对齐砸出帧 0.8667s,其后按间隔 `hit()`。全部实例播完才收招。 +`atk_para`=`次数,间隔秒,伤害万分比`。`Skill.range` 内按威胁锁最多 N 个单体。 + +- **飞剑武器主动**(Skill.id `210101`~`250101`,`fly=飞剑`):瞬发;按间隔依次实例化 `飞剑.prefab` 飞向目标,**落地**才结算伤害(无 deferred 段);飞棍连砸不走此路径。 +- **其余(如飞棍狠击 / 金箍棒连砸)**:按间隔砸满 N 次,每次独立武器实例(克隆 `weaponType*`),砸出帧对齐命中。 ### atk_type 15 目标圆形 @@ -191,3 +194,4 @@ Hero(合成等级 level) | 2026-09-30 | atk_type=16:扇形多段(段数,间隔秒,角度,伤害万分比);atk_buff1/2 命中效果保持不变(100001~100005) | | 2026-10-04 | atk_type=18:丢出独立圈伤(瞬发不占普攻);atk_type=20:占锁且每段播 animation;result 14 爆炸;atk_buff 14 点燃 | | 2026-10-08 | atk_type=19 全屏直线冰锥;atk_buff 13 冰冻;result 13 分裂冰锥;飞棍 320301~350301 atk_para 补间隔段 | +| 2026-10-08 | 飞剑主动 210101~250101:飞剑.prefab 直线落地伤;剑阵 result1 仍为目标点区域持续跳伤 | diff --git a/docs/方案-战斗表现数据驱动.md b/docs/方案-战斗表现数据驱动.md index fff8faa..f48c20b 100644 --- a/docs/方案-战斗表现数据驱动.md +++ b/docs/方案-战斗表现数据驱动.md @@ -143,4 +143,5 @@ M| 1 / 3 / 6 / 7 / 15 | 通道播动画 → drain hit(15:逻辑已锁主目 | 2026-09-30 | 修复弓字形塔散射不发射:Gong 宿主 12/17 委托自身「英雄弓-攻击/投掷物」扇出(BowScatterAtkPresenter),result 10 覆盖普攻形态同步分流 | | 2026-09-30 | 补注册 atk_type=11 召唤(10051~10055)→ SummonAtkPresenter;表内 1~17 表现已全覆盖,仅 9(将武器形 30406)按兜底结算待策划定义攻效 | | 2026-10-08 | atk_type=19 冰锥 → IceConeAtkPresenter(水弹直线落地);18/20 已有独立 Presenter | +| 2026-10-08 | 飞剑主动 skillId 210101~250101 → FlySwordAtkPresenter(飞剑.prefab 落地伤);同 atk_type=10 的飞棍仍 StaffSmash | | 2026-09-30 | 棍 200005 环形刀光:GunRingAtkPresenter,攻效播完从字位置按 atk_para(起始角,角度间隔,伤害系数)逆时针发射龙影(prefab/skill/棍),体盒弹道首中即消耗;atk_type 改 17(锁单体、伤害全在弹道) |