humingxing 1 week ago
parent 32d9e005d1
commit eaf613c5e8
  1. 10
      assets/bundles/first/config/Skill.json
  2. 5
      assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts
  3. 14
      assets/script/battle/config/weapon1/Weapon1BattleIds.ts
  4. 137
      assets/script/battle/presentation/FlySwordAtkPresenter.ts
  5. 9
      assets/script/battle/presentation/FlySwordAtkPresenter.ts.meta
  6. 96
      assets/script/battle/presentation/SkillVisualChannel.ts
  7. 6
      assets/script/battle/presentation/SpecialSkillPresenters.ts
  8. 4
      assets/script/battle/runtime/BattleRuntime.ts
  9. 6
      docs/方案-战斗技能逻辑层.md
  10. 1
      docs/方案-战斗表现数据驱动.md

@ -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": "",

@ -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 [];
}

@ -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}`;

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

@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "aa83d9ec-834f-4235-8777-652362698e71",
"files": [],
"subMetas": {},
"userData": {}
}

@ -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<Vec3>): 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<Vec3>): 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 起点

@ -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 自身投掷物,其余通用

@ -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();

@ -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 仍为目标点区域持续跳伤 |

@ -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(锁单体、伤害全在弹道) |

Loading…
Cancel
Save