修复一些技能

master
humingxing 1 week ago
parent d17893d50b
commit 285bddd6a5
  1. 8
      assets/bundles/first/config/Skill.json
  2. 2
      assets/mx/module/battle/combat/AbilitySystem.ts
  3. 4
      assets/mx/module/battle/ecs/Components.ts
  4. 175
      assets/script/battle/combat/PassiveSkillRuntime.ts
  5. 30
      assets/script/battle/combat/SkillAtkBuffExecutor.ts
  6. 8
      assets/script/battle/config/ChapterLevelBuilder.ts
  7. 7
      assets/script/battle/config/DebuffBuffBuilder.ts
  8. 21
      assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts
  9. 43
      assets/script/battle/presentation/AtkTypePresenters.ts
  10. 2
      assets/script/battle/presentation/SkillPresenterRouter.ts
  11. 26
      assets/script/battle/presentation/SkillVisualChannel.ts
  12. 117
      assets/script/battle/view/CocosBattleCombatPresenter.ts
  13. 2
      assets/script/game/TableAnomaly.ts
  14. 13
      docs/方案-战斗技能逻辑层.md
  15. 1
      docs/方案-战斗表现数据驱动.md

@ -916,7 +916,7 @@
"range": 0,
"atk_type": "10",
"fly": "",
"atk_para": "5,15000",
"atk_para": "5,0,15000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
@ -982,7 +982,7 @@
"range": 0,
"atk_type": "10",
"fly": "",
"atk_para": "6,20000",
"atk_para": "6,0,20000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
@ -1048,7 +1048,7 @@
"range": 0,
"atk_type": "10",
"fly": "",
"atk_para": "7,25000",
"atk_para": "7,0,25000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",
@ -1114,7 +1114,7 @@
"range": 0,
"atk_type": "10",
"fly": "",
"atk_para": "8,30000",
"atk_para": "8,0,30000",
"atk_buff1": "",
"atk_buff2": "",
"skill": "",

@ -223,7 +223,7 @@ export class AbilitySystem implements ISystem {
if (this._services.castBlockPredicate?.(entityId)) {
return false;
}
if (this._services.buffs.hasTag(entityId, 'debuff_8')) {
if (this._services.buffs.hasTag(entityId, 'debuff_8') || this._services.buffs.hasTag(entityId, 'debuff_13')) {
return false;
}
const abilities = this._world.getComponent(entityId, AbilitySetComp);

@ -83,14 +83,14 @@ export class BuffContainerComp extends EcsComponent {
static readonly ecsTypeId = 'mx.BuffContainerComp';
constructor(public buffs: BuffInstance[] = []) { super(); }
/** MovementSystem O(1):是否定身(debuff_4 眩晕 / debuff_bind_net 缚仙网 / debuff_fire_trap 三昧真火,移速归零) */
/** MovementSystem O(1):是否定身(眩晕 / 缚仙网 / 三昧真火 / 冰冻,移速归零) */
hasDebuff4 = false;
/** buff 列表变更后同步定身标记 */
refreshDebuff4Flag(): void {
for (let i = 0; i < this.buffs.length; i++) {
const id = this.buffs[i].buffId;
if (id === 'debuff_4' || id === 'debuff_bind_net' || id === 'debuff_fire_trap') {
if (id === 'debuff_4' || id === 'debuff_bind_net' || id === 'debuff_fire_trap' || id === 'debuff_13') {
this.hasDebuff4 = true;
return;
}

@ -81,6 +81,8 @@ export enum PassiveResultKind {
SwordWave = 11,
/** 闪电链:蔓延范围像素,蔓延数量,伤害系数万分比 */
LightningChain = 12,
/** 分裂冰锥:索敌半径像素,分裂数量,伤害系数万分比 */
IceConeSplit = 13,
/** 爆炸:圆形半径像素,伤害系数万分比 */
Explosion = 14,
}
@ -103,6 +105,10 @@ const SWORD_WAVE_DMG = 'passive_sword_wave';
const LIGHTNING_CHAIN_DMG = 'passive_lightning_chain';
/** 爆炸伤害类型(不回触发 condition) */
const EXPLOSION_DMG = 'passive_explosion';
/** 分裂冰锥伤害类型(不回触发 condition) */
const ICE_CONE_DMG = 'passive_ice_cone';
/** 分裂冰锥飞行像素/秒(与水弹一致) */
const ICE_CONE_SPEED = 1280;
/**
* 是否为被动 result 造成的伤害(不回触发任何 condition)。
@ -122,6 +128,8 @@ export const LINE_DRAGON_EVENT = 'LineDragonApplied';
export const SWORD_WAVE_EVENT = 'SwordWaveApplied';
/** 闪电链表现事件名 */
export const LIGHTNING_CHAIN_EVENT = 'LightningChainApplied';
/** 分裂冰锥表现事件名 */
export const ICE_CONE_SPLIT_EVENT = 'IceConeSplitApplied';
/** 直线火龙预制体宽 100,命中半长(再加半格占地) */
const LINE_DRAGON_HALF_LEN = 50;
/** 直线火龙预制体高 30,命中半宽(再加半格占地) */
@ -341,6 +349,38 @@ interface LineDragonFlight {
hitIds: EntityId[];
}
/** 分裂冰锥一枚(待落地) */
interface IceConePending {
/** 施法者 */
caster: EntityId;
/** 锁定目标 */
target: EntityId;
/** Skill.id */
skillId: number;
/** 伤害倍率 */
scale: number;
/** 距落地(秒) */
wait: number;
/** atk_buff1 */
buff1: string;
/** atk_buff2 */
buff2: string;
}
/** 分裂冰锥表现 cue(一枚) */
export interface IceConeSplitCue {
/** 起点逻辑 X */
fromX: number;
/** 起点逻辑 Y */
fromY: number;
/** 终点逻辑 X */
toX: number;
/** 终点逻辑 Y */
toY: number;
/** 飞行秒 */
duration: number;
}
/** 待按间隔放出的剑气 */
interface SwordWavePending {
/** 施法者 */
@ -423,6 +463,7 @@ export class PassiveSkillHost {
private readonly _wavePending: SwordWavePending[] = [];
private readonly _waves: SwordWaveFlight[] = [];
private readonly _chains: LightningChainRun[] = [];
private readonly _iceCones: IceConePending[] = [];
private _session: BattleSession | null = null;
/** result 结算中(含其产生的伤害/击杀),禁止再判任何 condition */
private _resultGate = 0;
@ -446,6 +487,7 @@ export class PassiveSkillHost {
this._wavePending.length = 0;
this._waves.length = 0;
this._chains.length = 0;
this._iceCones.length = 0;
this._session = null;
this._atkOverride = null;
}
@ -460,6 +502,7 @@ export class PassiveSkillHost {
this._removeZonesByCaster(entityId);
this._removeWavesByCaster(entityId);
this._removeChainsByCaster(entityId);
this._removeIceConesByCaster(entityId);
const idx = this._indexOf(entityId);
if (idx >= 0) this._binds.splice(idx, 1);
}
@ -518,6 +561,7 @@ export class PassiveSkillHost {
this._tickSwordWavePending(dt);
this._tickSwordWaves(dt);
this._tickLightningChains(dt);
this._tickIceCones(dt);
}
/**
@ -781,6 +825,9 @@ export class PassiveSkillHost {
case PassiveResultKind.LightningChain:
this._applyLightningChain(session, caster, target, bind.skillId, p);
return;
case PassiveResultKind.IceConeSplit:
this._applyIceConeSplit(session, caster, target, bind, p);
return;
case PassiveResultKind.Explosion:
this._applyExplosion(session, caster, target, bind, p);
return;
@ -1409,6 +1456,134 @@ export class PassiveSkillHost {
return false;
}
/**
* 分裂冰锥:从触发目标向半径内其它敌发射最多 N 枚,落地伤并施加行上 atk_buff。
* @param session 战斗会话
* @param caster 施法者
* @param target 触发目标
* @param bind 被动绑定
* @param p 索敌半径, 分裂数量, 伤害万分比
*/
private _applyIceConeSplit(session: BattleSession, caster: EntityId, target: EntityId, bind: PassiveSkillBind, p: number[]): void {
const radius = p[0];
const count = Math.floor(p[1]);
const coeffWan = p[2];
if (!(radius > 0) || !(count > 0) || !(coeffWan > 0)) {
console.log(`[PassiveSkill] result 13 参数无效 skill=${bind.skillId}`, p.join(','));
return;
}
const origin = this._logicPos(session, target > 0 ? target : caster)
?? this._logicPos(session, caster);
if (!origin) {
console.log(`[PassiveSkill] result 13 无落点 skill=${bind.skillId}`);
return;
}
const casterTeam = session.world.getComponent(caster, TeamComp);
const cell = session.grid.worldToCell(origin.x, origin.y);
const radiusCells = radius / Math.max(1, session.grid.cellSize);
const ids = session.spatial.queryRadius(cell, radiusCells + 1);
const r2 = radius * radius;
const candidates: { id: EntityId; dist2: number }[] = [];
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
if (id === caster || id === target) continue;
if (!session.world.isAlive(id)) continue;
const hp = session.world.getComponent(id, HealthComp);
if (!hp || !(hp.hp > 0)) continue;
const move = session.world.getComponent(id, MovementComp);
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) continue;
if (!BattleTeamId.isHostile(casterTeam, session.world.getComponent(id, TeamComp))) continue;
const pos = this._logicPos(session, id);
if (!pos) continue;
const dx = pos.x - origin.x;
const dy = pos.y - origin.y;
const dist2 = dx * dx + dy * dy;
if (dist2 > r2) continue;
candidates.push({ id, dist2 });
}
if (candidates.length === 0) return;
this._sortByDist2(candidates);
const scale = coeffWan / 10000;
const buff1 = String(bind.atkBuff1 ?? '').trim();
const buff2 = String(bind.atkBuff2 ?? '').trim();
const n = Math.min(count, candidates.length);
for (let i = 0; i < n; i++) {
const tid = candidates[i].id;
const aim = this._logicPos(session, tid);
if (!aim) continue;
const dx = aim.x - origin.x;
const dy = aim.y - origin.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const duration = Math.max(0.12, dist / ICE_CONE_SPEED);
this._iceCones.push({
caster,
target: tid,
skillId: bind.skillId,
scale,
wait: duration,
buff1,
buff2,
});
const cue: IceConeSplitCue = {
fromX: origin.x,
fromY: origin.y,
toX: aim.x,
toY: aim.y,
duration,
};
session.events.emit(BattleEventFactory.custom(ICE_CONE_SPLIT_EVENT, cue));
}
}
/**
* 推进分裂冰锥落地。
* @param dt 逻辑秒
*/
private _tickIceCones(dt: number): void {
if (!(dt > 0) || this._iceCones.length <= 0) return;
const session = this._session;
if (!session) return;
for (let i = this._iceCones.length - 1; i >= 0; i--) {
const d = this._iceCones[i];
d.wait -= dt;
if (d.wait > 0) continue;
this._iceCones.splice(i, 1);
if (!session.world.isAlive(d.caster) || !session.world.isAlive(d.target)) continue;
const hp = session.world.getComponent(d.target, HealthComp);
if (!hp || !(hp.hp > 0)) continue;
const move = session.world.getComponent(d.target, MovementComp);
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) continue;
this._dealResultDamage(session, d.caster, d.target, d.skillId, d.scale, ICE_CONE_DMG);
if (!d.buff1 && !d.buff2) continue;
if (!session.world.isAlive(d.target)) continue;
const abilityCtx = createAbilityContext(session, d.caster, `passive_${d.skillId}`, [d.target]);
SkillAtkBuffExecutor.applyPair(session, abilityCtx, d.target, d.buff1, d.buff2, { fromPassiveResult: true });
}
}
/** 卸下施法者时清其分裂冰锥 */
private _removeIceConesByCaster(caster: EntityId): void {
for (let i = this._iceCones.length - 1; i >= 0; i--) {
if (this._iceCones[i].caster === caster) this._iceCones.splice(i, 1);
}
}
/**
* 按 dist2 升序(插入排序,避免函数内回调)。
* @param list 候选
*/
private _sortByDist2(list: { id: EntityId; dist2: number }[]): void {
for (let i = 1; i < list.length; i++) {
const cur = list[i];
let j = i - 1;
while (j >= 0 && list[j].dist2 > cur.dist2) {
list[j + 1] = list[j];
j -= 1;
}
list[j + 1] = cur;
}
}
/**
* 爆炸:以触发目标为圆心圈伤;行上 atk_buff 非空时对命中目标施加。
* @param session 战斗会话

@ -101,8 +101,8 @@ export class SkillAtkBuffExecutor {
private static _applyLoadoutRaw(services: BattleServices, entity: EntityId, raw: string): void {
const parts = this._parse(raw);
if (!parts.length) return;
// 11 射程已计入 setup;14 点燃仅命中施加
if (parts[0] === 11 || parts[0] === 14) return;
// 11 射程已计入 setup;13 冰冻 / 14 点燃仅命中施加
if (parts[0] === 11 || parts[0] === 13 || parts[0] === 14) return;
this._applyKind(services, null, entity, parts, {});
}
@ -189,6 +189,9 @@ export class SkillAtkBuffExecutor {
case 11:
this._attackRange(services, target, parts[1] ?? 0, parts[2] ?? -1);
break;
case 13:
this._freeze(services, ctx, target, parts);
break;
case 14:
this._ignite(services, ctx, target, parts, opts);
break;
@ -198,6 +201,29 @@ export class SkillAtkBuffExecutor {
}
}
/**
* 冰冻:定身 + 禁施法(debuff_13)。
* @param services 战斗服务
* @param ctx 施法上下文
* @param target 目标
* @param parts `13,概率,冰冻秒`
*/
private static _freeze(
services: BattleServices,
ctx: AbilityContext | null,
target: EntityId,
parts: number[],
): void {
const duration = Math.max(0, Number(parts[2]) || 0);
if (!(duration > 0)) return;
if (!services.config.getBuff('debuff_13')) {
console.log('[SkillAtkBuff] 缺 buff debuff_13');
return;
}
const caster = ctx?.caster ?? target;
services.buffs.add(target, 'debuff_13', caster, 1, { duration });
}
/**
* 点燃:立即首跳,再按间隔跳至结束。
* @param services 战斗服务

@ -59,8 +59,6 @@ export interface WaveIntervalConfig {
/** 双方基地默认血量(后续养成覆盖) */
export const DEFAULT_BATTLE_WALL_HP = 3;
/** 开战初始桃子(写死,不读 Level.startnumber) */
const START_PEACH = 1000;
/** 单怪出怪间隔默认秒数(常量表无对应行) */
const DEFAULT_MONSTER_SPAWN_INTERVAL_SEC = 1;
/** 对齐 Const 表 id=10(ConstNumType.WaveInterval),避免从 ConfigProjectData 拉环 */
@ -77,7 +75,11 @@ export class ChapterLevelBuilder {
}
const difficultyRow = Math.max(1, options.rankDifficultyBonus ?? (Number(levelRow.diffcult) || 1));
const startGold = START_PEACH;
const startGold = Math.floor(Number(levelRow.startnumber));
if (!(startGold >= 0) || !Number.isFinite(startGold)) {
console.error('[ChapterLevelBuilder] Level.startnumber 无效', options.levelId, levelRow.startnumber);
return null;
}
const spawnInterval = this.resolveMonsterSpawnIntervalSec(options.spawnInterval);
const waveTiming = this.resolveWaveIntervalConfig();
const firstWaveDelay = options.firstWaveDelay ?? waveTiming.firstWaveDelaySec;

@ -66,5 +66,12 @@ export function buildBasicDebuffDefs(): BuffDef[] {
category: 'confusion',
removeGroup: 'debuff_8',
},
{
id: 'debuff_13',
duration: 3,
polarity: 'negative',
category: 'freeze',
removeGroup: 'debuff_13',
},
];
}

@ -593,6 +593,21 @@ export class SkillAbilityBuilder {
params,
};
}
// 19 全屏直线冰锥:atk_para=数量,伤害万分比;按威胁锁最多 N 个全屏敌
if (atkHead === 19) {
const maxCount = Math.max(0, Math.floor(atkParts[0] || 0));
const wan = Number(atkParts[1]);
if (!(maxCount > 0) || !Number.isFinite(wan)) {
console.log('[SkillAbilityBuilder] atk_type=19 参数须为「数量,伤害万分比」', row.id, row.atk_para);
}
return {
team,
selector: 'AllEnemies',
shape: 'Single',
maxCount,
params: { ...baseParams, pickCount: maxCount },
};
}
// 8 / 11 召唤:可无目标
if (atkHead === 8 || atkHead === 11) {
return {
@ -647,9 +662,9 @@ export class SkillAbilityBuilder {
}]];
}
// 12 散射 / 13 连发 / 17 不穿透散射:伤害由 BowAtkHost 弹道结算,无 deferred 段
// 12 散射 / 13 连发 / 17 不穿透散射 / 19 冰锥:伤害由 BowAtkHost 弹道结算,无 deferred 段
// 18 丢出独立持续伤:伤害由 ThrownCircleHost 结算,无 deferred 段
if (atkHead === 12 || atkHead === 13 || atkHead === 17 || atkHead === 18) {
if (atkHead === 12 || atkHead === 13 || atkHead === 17 || atkHead === 18 || atkHead === 19) {
return [];
}
@ -777,6 +792,8 @@ export class SkillAbilityBuilder {
case 12:
case 13:
return this._damageScale(atkParts[2] ?? 10000);
case 19:
return this._damageScale(atkParts[1] ?? 10000);
default:
return this._damageScale(atkParts[atkParts.length - 1] ?? 10000);
}

@ -269,6 +269,49 @@ export class FallbackAtkPresenter implements ISkillPresenter {
}
}
/**
* atk_type=19 全屏直线冰锥:瞬发,对锁定的最多 N 个敌各发一枚水弹,落地结算。
*/
export class IceConeAtkPresenter 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 wan = Number(String(raw[1] ?? '').trim());
if (!(count > 0) || !Number.isFinite(wan) || !(wan > 0)) {
console.log('[IceConeAtkPresenter] 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();
const scale = wan / 10000;
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 < count; i++) {
const tid = targets[i];
if (!(tid > 0)) continue;
if (host.visualChannel.launchIceCone(ctx.caster, ctx.abilityId, tid, scale, buff1, buff2)) {
launched += 1;
}
}
if (launched <= 0) {
console.log('[IceConeAtkPresenter] 无有效发射', ctx.params.skillId);
}
ctx.releasePresentationLock();
host.finishPresentationCast(seq, false);
}
}
/**
* atk_type=12 穿透散射 / 17 不穿透散射:出手时刻扇出直线箭,伤害由 BowAtkHost 结算。
*/

@ -3,6 +3,7 @@ import type { SkillPresentationContext } from '../skill/SkillPresentationTypes';
import type { ISkillPresenter } from './SkillPresenter';
import {
FallbackAtkPresenter,
IceConeAtkPresenter,
InstantAtkPresenter,
LineMultiHitAtkPresenter,
LineRectAtkPresenter,
@ -139,6 +140,7 @@ export class SkillPresenterRouter {
this.registerAtkType(17, new ScatterPierceAtkPresenter());
this.registerAtkType(13, new VolleyArrowAtkPresenter());
this.registerAtkType(18, new ThrownCircleMultiHitAtkPresenter());
this.registerAtkType(19, new IceConeAtkPresenter());
this.registerAtkType(20, new SegmentAnimMultiHitAtkPresenter());
}

@ -1170,6 +1170,25 @@ export class SkillVisualChannel {
* @param aimWorld 瞄准点(目标死后用最后坐标)
* @returns 是否发出
*/
/**
* atk_type=19 冰锥:直线水弹飞向目标,落地结算(不依赖法杖判定)。
* @param caster 施法者
* @param abilityId Ability id
* @param target 锁定目标
* @param scale 伤害倍率
* @param buff1 atk_buff1
* @param buff2 atk_buff2
* @returns 是否发出
*/
launchIceCone(caster: EntityId, abilityId: string, target: EntityId, scale: number, buff1: string, buff2: string): boolean {
const aim = target > 0 ? SkillPresentationEnv.getTargetWorldPos(target) : null;
if (!aim) {
console.log('[SkillVisualChannel] 冰锥无目标坐标', target);
return false;
}
return this._launchWaterBall(caster, abilityId, target, scale, buff1, buff2, aim);
}
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);
@ -1389,7 +1408,10 @@ export class SkillVisualChannel {
private _launchWaterBall(caster: EntityId, abilityId: string, target: EntityId, scale: number, buff1: string, buff2: string, aimWorld: Readonly<Vec3>): boolean {
this._weaponCasting = true;
const fromNode = this._weapon.skinAnim?.node ?? this._weapon.aim ?? this._weapon.root;
if (!fromNode?.isValid) {
const fromWorld = fromNode?.isValid
? fromNode.worldPosition
: SkillPresentationEnv.getCasterGridCenter(caster);
if (!fromWorld) {
console.log('[SkillVisualChannel] 水弹无起点');
return false;
}
@ -1399,7 +1421,7 @@ export class SkillVisualChannel {
console.log('[SkillVisualChannel] 水弹无目标坐标', target);
return false;
}
this._arrowFrom.set(fromNode.worldPosition);
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);

@ -20,16 +20,19 @@ import { BattleSpinePlayback } from '../presentation/BattleSpinePlayback';
import { SkillTransformDriver, computeAimAngleDeg } from '../skill/SkillTransformDriver';
import {
HOMING_DRAGON_EVENT,
ICE_CONE_SPLIT_EVENT,
LINE_DRAGON_EVENT,
SWORD_ARRAY_EVENT,
SWORD_WAVE_EVENT,
LIGHTNING_CHAIN_EVENT,
type HomingDragonCue,
type IceConeSplitCue,
type LineDragonCue,
type SwordArrayCue,
type SwordWaveCue,
type LightningChainCue,
} from '../combat/PassiveSkillRuntime';
import { BattleMapSpec } from '../map/BattleMapSpec';
import { traceCombatDamage } from 'db://assets/mx/tools/CombatDamageTrace';
type MonsterView = IBattleMonsterView & {
@ -63,11 +66,16 @@ const BURN_BUFF_ID = 'debuff_1';
const BLEED_BUFF_ID = 'debuff_2';
const GHOST_BITE_BUFF_ID = 'debuff_6';
const ELEC_BUFF_ID = 'debuff_7';
const FREEZE_BUFF_ID = 'debuff_13';
const BIND_NET_BUFF_ID = 'debuff_bind_net';
const FIRE_TRAP_BUFF_ID = 'debuff_fire_trap';
const KNOCKBACK_APPLIED_EVENT = 'KnockbackApplied';
const SWORD_ARRAY_PREFAB = '剑阵';
const SWORD_ARRAY_PREFAB_PATH = 'prefab/skill/';
/** 分裂冰锥表现用「水弹」 */
const ICE_CONE_PREFAB = '水弹';
/** 水弹预制体原图宽约 322,缩到约一格 */
const ICE_CONE_SRC_W = 322;
/** 剑阵根节点参考边长(预制体 UITransform 200,直径=边长) */
const SWORD_ARRAY_REF_SIDE = 200;
const HOMING_DRAGON_PREFAB = '追踪火龙';
@ -106,8 +114,11 @@ export class CocosBattleCombatPresenter {
private readonly _lineDragonFx: Node[] = [];
private readonly _swordWaveFx: Node[] = [];
private readonly _lightningFx: Node[] = [];
private readonly _iceConeFx: Node[] = [];
private readonly _dragonFrom = new Vec3();
private readonly _dragonTo = new Vec3();
private readonly _iceConeFrom = new Vec3();
private readonly _iceConeTo = new Vec3();
bind(battle: ProjectBattleHandle, monsterPresenter: CocosBattleMonsterPresenter, displayRoot: Node): void {
this.unbind();
@ -126,6 +137,7 @@ export class CocosBattleCombatPresenter {
this._recycleAllLineDragons();
this._recycleAllSwordWaves();
this._recycleAllLightning();
this._recycleAllIceCones();
if (this._battle && this._listener) {
this._battle.off(BATTLE_EVENT_ALL, this._listener);
}
@ -173,6 +185,8 @@ export class CocosBattleCombatPresenter {
this._onStunApplied(event.target, event.duration);
} else if (event.buffId === SLOW_BUFF_ID) {
this._onSlowApplied(event.target, event.duration);
} else if (event.buffId === FREEZE_BUFF_ID) {
this._onFreezeApplied(event.target, event.duration);
} else if (event.buffId === BURN_BUFF_ID) {
this._onBurnApplied(event.target, event.duration);
} else if (event.buffId === BLEED_BUFF_ID) {
@ -192,6 +206,8 @@ export class CocosBattleCombatPresenter {
this._onStunRemoved(event.target);
} else if (event.buffId === SLOW_BUFF_ID) {
this._onSlowRemoved(event.target);
} else if (event.buffId === FREEZE_BUFF_ID) {
this._onFreezeRemoved(event.target);
} else if (event.buffId === BURN_BUFF_ID) {
this._onBurnRemoved(event.target);
} else if (event.buffId === BLEED_BUFF_ID) {
@ -205,6 +221,7 @@ export class CocosBattleCombatPresenter {
case BattleEventType.EntityKilled:
this._onStunRemoved(event.victim);
this._onSlowRemoved(event.victim);
this._onFreezeRemoved(event.victim);
this._onBurnRemoved(event.victim);
this._onBleedRemoved(event.victim);
this._onGhostBiteRemoved(event.victim);
@ -213,6 +230,7 @@ export class CocosBattleCombatPresenter {
case BattleEventType.UnitDespawned:
this._onStunRemoved(event.entity);
this._onSlowRemoved(event.entity);
this._onFreezeRemoved(event.entity);
this._onBurnRemoved(event.entity);
this._onBleedRemoved(event.entity);
this._onGhostBiteRemoved(event.entity);
@ -234,6 +252,8 @@ export class CocosBattleCombatPresenter {
this._playSwordWave(event.payload as SwordWaveCue | undefined);
} else if (event.name === LIGHTNING_CHAIN_EVENT) {
this._playLightningChain(event.payload as LightningChainCue | undefined);
} else if (event.name === ICE_CONE_SPLIT_EVENT) {
this._playIceConeSplit(event.payload as IceConeSplitCue | undefined);
}
break;
}
@ -261,6 +281,18 @@ export class CocosBattleCombatPresenter {
this._getMonsterView(target)?.clearSlowFx?.();
}
/** 冰冻:复用怪_冰表现并飘「冰冻」 */
private _onFreezeApplied(target: EntityId, duration: number): void {
const monster = this._getMonsterView(target);
if (!monster || !Number.isFinite(duration) || duration <= 0) return;
monster.showSlowFx?.(Math.max(0.05, duration));
this._playStatusText(target, '冰冻');
}
private _onFreezeRemoved(target: EntityId): void {
this._getMonsterView(target)?.clearSlowFx?.();
}
private _onBurnApplied(target: EntityId, duration: number): void {
const monster = this._getMonsterView(target);
if (!monster || !Number.isFinite(duration) || duration <= 0) return;
@ -873,6 +905,91 @@ export class CocosBattleCombatPresenter {
}
}
/**
* 分裂冰锥:水弹从触发点飞向目标。
* @param cue 逻辑层 cue
*/
private _playIceConeSplit(cue: IceConeSplitCue | undefined): void {
if (!cue || !(cue.duration > 0)) return;
const parent = SkillPresentationEnv.flyLayer ?? this._displayRoot;
if (!parent?.isValid) {
console.log('[CombatPresenter] 冰锥无父节点');
return;
}
this._iceConeFrom.set(cue.fromX, cue.fromY, 0);
this._iceConeTo.set(cue.toX, cue.toY, 0);
const angleDeg = computeAimAngleDeg(this._iceConeFrom, this._iceConeTo);
if (angleDeg == null) {
console.log('[CombatPresenter] 冰锥朝向为零');
return;
}
let node: Node | null = null;
try {
node = gg.res.getNode(SWORD_ARRAY_PREFAB_PATH, ICE_CONE_PREFAB, GBundle.BattleHero);
} catch (err) {
console.log('[CombatPresenter] 冰锥 getNode 失败', String(err));
return;
}
if (!node?.isValid) {
console.log('[CombatPresenter] 冰锥预制体未加载', ICE_CONE_PREFAB);
return;
}
parent.active = true;
const flyUt = parent.getComponent(UITransform);
const scale = BattleMapSpec.CELL_SIZE / ICE_CONE_SRC_W;
node.active = true;
node.layer = parent.layer;
node.setScale(scale, scale, 1);
node.parent = parent;
const startLocal = flyUt
? flyUt.convertToNodeSpaceAR(this._iceConeFrom)
: this._iceConeFrom.clone();
const endLocal = flyUt
? flyUt.convertToNodeSpaceAR(this._iceConeTo)
: this._iceConeTo.clone();
node.setPosition(startLocal);
node.angle = angleDeg;
node.setSiblingIndex(parent.children.length - 1);
this._iceConeFx.push(node);
SkillTransformDriver.cancelNode(node);
SkillTransformDriver.animate({
node,
duration: cue.duration,
fromPos: startLocal,
toPos: endLocal,
fromAngle: angleDeg,
toAngle: angleDeg,
easing: SkillTransformDriver.easeLinear,
onComplete: this._onIceConeArrived,
});
}
private readonly _onIceConeArrived = (node?: Node): void => {
if (node) this._recycleIceCone(node);
};
/** 回收全部分裂冰锥表现 */
private _recycleAllIceCones(): void {
for (let i = this._iceConeFx.length - 1; i >= 0; i--) {
this._recycleIceCone(this._iceConeFx[i]);
}
}
/**
* 停飞并回池冰锥表现。
* @param node 水弹节点
*/
private _recycleIceCone(node: Node): void {
for (let i = this._iceConeFx.length - 1; i >= 0; i--) {
if (this._iceConeFx[i] === node) this._iceConeFx.splice(i, 1);
}
if (!node?.isValid) return;
SkillTransformDriver.cancelNode(node);
node.angle = 0;
node.setScale(1, 1, 1);
gg.res.putNode(node);
}
/**
* 停飞并回池。
* @param node 刀光节点

@ -9,7 +9,7 @@ export interface TableRowSource {
}
/** 索敌已实现的 atk_type 头 */
const ATK_LOGIC: readonly number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20];
const ATK_LOGIC: readonly number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
/** Skill.skill 额外效果已落地的头 */
const EXTRA_SKILL: readonly number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
/** Weapon.type(1刀 2枪 3棍 4弓 5燃烧 6增幅) */

@ -131,10 +131,22 @@ Hero(合成等级 level)
参数与 18 相同。开战占施法锁;**每段伤害前重播 `Skill.animation`**(如暴风雪),段间隔从段起点起跳;命中走 deferred `DealDamage` + `atk_buff`。
### atk_type 19 全屏直线冰锥
`atk_para`=`数量,伤害万分比`。全屏按威胁锁最多 N 个敌;瞬发不占普攻,对每个目标发直线「水弹」冰锥,落地结算伤害与 `atk_buff`。
### atk_buff 13 冰冻
参数:`概率,冰冻秒`。挂 `debuff_13`(定身 + 禁施法),表现复用 `怪_冰` 并飘「冰冻」。部署 loadout 不施加。
### atk_buff 14 点燃
参数:`间隔秒,伤害系数万分比,持续秒`。挂 `debuff_1`,立即首跳再按间隔跳伤(`burn_dot`)。被动 result 附带施加时用 `passive_burn` / `fromPassiveResult`,不回触发 condition。部署 loadout 不施加。
### result 13 分裂冰锥
参数:`索敌半径像素,分裂数量,伤害万分比`。从触发目标向半径内其它敌(按距排序)发最多 N 枚冰锥,落地伤;行上 `atk_buff1/2` 非空时对落地目标 `applyPair`。
### result 14 爆炸
参数:`爆炸圆形半径,伤害系数万分比`。以触发目标为圆心圈伤(`fromPassiveResult`)。**仅当行上 `atk_buff1/2` 非空**时对命中目标 `applyPair`(不为 desc 补效果)。
@ -178,3 +190,4 @@ Hero(合成等级 level)
| 2026-09-11 | target_stage=2:maxCount 仅为上限,每人一次,不循环补位(悟净治疗术/悟能疾行术) |
| 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 补间隔段 |

@ -142,4 +142,5 @@ M| 1 / 3 / 6 / 7 / 15 | 通道播动画 → drain hit(15:逻辑已锁主目
| 2026-09-30 | atk_type=17 不穿透散射箭:散射扇出同 12,首中即结算并当场回收表现箭(400002/400003) |
| 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-09-30 | 棍 200005 环形刀光:GunRingAtkPresenter,攻效播完从字位置按 atk_para(起始角,角度间隔,伤害系数)逆时针发射龙影(prefab/skill/棍),体盒弹道首中即消耗;atk_type 改 17(锁单体、伤害全在弹道) |

Loading…
Cancel
Save