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.
159 lines
5.8 KiB
159 lines
5.8 KiB
|
3 weeks ago
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|