import type { EntityId } from 'db://assets/mx/module/ecs'; import { BattleEventType, BattlePhase } from 'db://assets/mx/module/battle'; import type { BattleEvent } from 'db://assets/mx/module/battle'; import type { IBattleMechanism, MechanismContext } from '../IBattleMechanism'; import { calcMiddleRowRange } from '../MechanismSpawnArea'; import { WITCH_CAST_ANIM, WITCH_IDLE_ANIM } from '../MechanismConstants'; import { BATTLE_MONSTER_CCCLASS } from '../../view/IBattleMonsterView'; import { ChapterNewPlayTypeEnum, IWitchProperty } from '../../../game/ChapterPlayTypes'; import { BattleAudio } from '../../audio/BattleAudio'; import { calcWitchMechanismMaxHp, despawnMechanismUnit, healRandomMonsters, isCombatPhase, pickRandomSpawnGrid, spawnWitchMechanismUnit } from '../MechanismServices'; type WitchPhase = 'waiting' | 'active' | 'cooldown'; /** 白骨精机制(Playmode 17 / Monster 1009) */ export class WitchMechanism implements IBattleMechanism { readonly typeId = ChapterNewPlayTypeEnum.Witch; private _ctx: MechanismContext | null = null; private _config: IWitchProperty | null = null; private _phase: WitchPhase = 'waiting'; /** 下次可刷新的 battleTime(与旧 Schedule 一致:按全局战斗时间轴) */ private _nextSpawnAt = 0; private _existTimer = 0; private _healTimer = 0; private _witchEntity: EntityId | null = null; private _killListener: ((e: BattleEvent) => void) | null = null; init(ctx: MechanismContext, config: unknown): void { this._ctx = ctx; this._config = config as IWitchProperty; this._nextSpawnAt = this._config.startTime; this._phase = 'waiting'; this._killListener = (e) => { if (e.type !== BattleEventType.EntityKilled) return; if (this._witchEntity != null && e.victim === this._witchEntity) { this._clearWitch('killed'); } }; ctx.session.events.on(BattleEventType.EntityKilled, this._killListener); } onPhaseChanged(_phase: BattlePhase): void { } tick(dt: number): void { if (!this._ctx || !this._config || !isCombatPhase(this._ctx)) return; if (this._phase === 'waiting' || this._phase === 'cooldown') { if (this._ctx.battleTime >= this._nextSpawnAt) { this._trySpawn(); } return; } if (this._phase === 'active' && this._witchEntity != null) { if (!this._ctx.session.world.isAlive(this._witchEntity)) { this._clearWitch('killed'); return; } this._existTimer -= dt; if (this._existTimer <= 0) { despawnMechanismUnit(this._ctx, this._witchEntity); this._clearWitch('timeout'); return; } this._healTimer -= dt; if (this._healTimer <= 0) { this._castHeal(); } } } destroy(): void { if (this._ctx && this._killListener) { this._ctx.session.events.off(BattleEventType.EntityKilled, this._killListener); } if (this._ctx && this._witchEntity != null && this._ctx.session.world.isAlive(this._witchEntity)) { despawnMechanismUnit(this._ctx, this._witchEntity); } this._witchEntity = null; this._killListener = null; this._ctx = null; this._config = null; } private _trySpawn(): void { const ctx = this._ctx; const cfg = this._config; if (!ctx || !cfg) return; const map = ctx.runtime.Map; if (!map) return; // 可通行格落点;优先地图中部行 const rowRange = calcMiddleRowRange(map.mapRows); const grid = pickRandomSpawnGrid(ctx, rowRange) ?? pickRandomSpawnGrid(ctx); if (!grid) { console.log( '[WitchMechanism] 无可通行落点,稍后重试 battleTime=', ctx.battleTime, 'rowRange=', rowRange, ); return; } const maxHp = calcWitchMechanismMaxHp(ctx, cfg.addHpCoefficient); const entity = spawnWitchMechanismUnit(ctx, grid, maxHp); if (entity == null) { console.error('[WitchMechanism] 生成失败 battleTime=', ctx.battleTime, 'hp=', maxHp); return; } this._witchEntity = entity; this._phase = 'active'; this._existTimer = cfg.existTime; this._scheduleNextHeal(); BattleAudio.playWitchCast(); } private _castHeal(): void { const ctx = this._ctx; const cfg = this._config; if (!ctx || !cfg || this._witchEntity == null) return; const presenter = ctx.runtime.getMonsterPresenter(); const node = presenter?.getEntityNodeMap().get(this._witchEntity); const view = node?.getComponent(BATTLE_MONSTER_CCCLASS) as { playSpineAnim?: (name: string, loop: boolean, onComplete?: () => void) => void; } | null; view?.playSpineAnim?.(WITCH_CAST_ANIM, false, () => { view?.playSpineAnim?.(WITCH_IDLE_ANIM, true); }); BattleAudio.playWitchCast(); healRandomMonsters(ctx, cfg.addHpCoefficient, cfg.maxAddHpMonsterNum, this._witchEntity); BattleAudio.playHeal(); this._scheduleNextHeal(); } private _scheduleNextHeal(): void { const cfg = this._config; if (!cfg) return; const span = cfg.effectTimeUpperLimit - cfg.effectTimeLowerLimit; this._healTimer = cfg.effectTimeLowerLimit + Math.random() * Math.max(0, span); } private _clearWitch(reason: 'killed' | 'timeout'): void { const ctx = this._ctx; this._witchEntity = null; this._phase = 'cooldown'; if (ctx) { this._nextSpawnAt = ctx.battleTime + (this._config?.intervalTime ?? 0); } } }