forked from phenom/healer-man
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
import type { DungeonEncounterEvent } from "./dungeonEncounterTypes";
|
|
|
|
export interface DungeonEncounterMobObservation {
|
|
readonly engaged: boolean;
|
|
readonly dead: boolean;
|
|
readonly despawned: boolean;
|
|
readonly health: number;
|
|
readonly maxHealth: number;
|
|
}
|
|
|
|
export interface DungeonEncounterMobCursorEntry {
|
|
readonly engaged: boolean;
|
|
readonly dead: boolean;
|
|
readonly healthPercent: number;
|
|
}
|
|
|
|
export type DungeonEncounterMobCursor = Readonly<Record<string, DungeonEncounterMobCursorEntry>>;
|
|
|
|
type CombatEdgeEvent = Extract<
|
|
DungeonEncounterEvent,
|
|
{ readonly kind: "entity-engaged" | "entity-health" | "entity-death" }
|
|
>;
|
|
|
|
export interface DungeonEncounterCombatEventBatch {
|
|
readonly cursor: DungeonEncounterMobCursor;
|
|
readonly events: readonly CombatEdgeEvent[];
|
|
}
|
|
|
|
function healthPercent(mob: DungeonEncounterMobObservation): number {
|
|
if (!Number.isFinite(mob.maxHealth) || mob.maxHealth <= 0) return 0;
|
|
return Math.max(0, Math.min(100, (Math.max(0, mob.health) / mob.maxHealth) * 100));
|
|
}
|
|
|
|
/**
|
|
* Converts mutable combat-state edges into deterministic encounter events.
|
|
* Despawned actors are deliberately omitted so scripted cleanup never looks
|
|
* like a kill and a later respawn of the same stable id can engage normally.
|
|
*/
|
|
export function deriveDungeonEncounterCombatEvents(
|
|
previous: DungeonEncounterMobCursor,
|
|
mobs: Readonly<Record<string, DungeonEncounterMobObservation>>,
|
|
now: number,
|
|
): DungeonEncounterCombatEventBatch {
|
|
const cursor: Record<string, DungeonEncounterMobCursorEntry> = {};
|
|
const events: CombatEdgeEvent[] = [];
|
|
|
|
for (const entityId of Object.keys(mobs).sort()) {
|
|
const mob = mobs[entityId];
|
|
if (mob.despawned) continue;
|
|
const percent = healthPercent(mob);
|
|
const prior = previous[entityId];
|
|
cursor[entityId] = Object.freeze({
|
|
engaged: mob.engaged,
|
|
dead: mob.dead,
|
|
healthPercent: percent,
|
|
});
|
|
|
|
if (mob.engaged && !mob.dead && !prior?.engaged) {
|
|
events.push(Object.freeze({ kind: "entity-engaged", entityId, now }));
|
|
}
|
|
if (!mob.dead && percent < (prior?.healthPercent ?? 100)) {
|
|
events.push(Object.freeze({ kind: "entity-health", entityId, healthPercent: percent, now }));
|
|
}
|
|
if (mob.dead && !prior?.dead) {
|
|
events.push(Object.freeze({ kind: "entity-death", entityId, now }));
|
|
}
|
|
}
|
|
|
|
return Object.freeze({ cursor: Object.freeze(cursor), events: Object.freeze(events) });
|
|
}
|