Files
healer-man/src/game/dungeonEncounterCombatEvents.ts
T
2026-09-09 11:21:33 -04:00

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