You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

158 lines
5.8 KiB

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