Release Healer Man 0.1.46

This commit is contained in:
phenom committed 2026-09-16 14:30:58 -04:00
1 parent f82daf1c13
commit 9ff574ac0b
73 files changed
+33486 -174

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
import { useDungeonSimulationStore } from "../game/dungeonSimulationStore";
import { dungeonRouteDefinition } from "../game/dungeonRoutes";
import type { DungeonSimulationSpeed } from "../game/dungeonSimulationTypes";
import { finalizeDungeonSimulation } from "../scene/DungeonSimulationSceneBridge";
import { useShellStore } from "../app/shellStore";
const SPEEDS: readonly DungeonSimulationSpeed[] = [0.5, 1, 2, 4];
function formatTime(elapsedMs: number): string {
const seconds = Math.floor(elapsedMs / 1_000);
return `${String(Math.floor(seconds / 60)).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}.${Math.floor((elapsedMs % 1_000) / 100)}`;
}
function downloadReport(): void {
const simulation = useDungeonSimulationStore.getState();
const report = simulation.replay?.report;
if (!report) return;
const url = URL.createObjectURL(new Blob([JSON.stringify(report, null, 2)], { type: "application/json" }));
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = `${report.dungeonId}-${report.seed}-SimulationReportV1.json`;
anchor.click();
URL.revokeObjectURL(url);
}
export function DungeonSimulationHud() {
const simulation = useDungeonSimulationStore();
const route = simulation.dungeonId
? dungeonRouteDefinition(simulation.dungeonId, simulation.routeId ?? "primary")
: null;
const leg = route?.legs.find((candidate) => candidate.objectiveId === simulation.objectiveId) ?? null;
const replayDuration = simulation.replay?.durationMs ?? 0;
const returnToGmTools = () => {
useDungeonSimulationStore.getState().leave();
useShellStore.getState().openGmAdmin();
};
return (
<aside className="dungeon-sim-hud" aria-label="Dungeon simulation controls">
<div className="dungeon-sim-hud__header">
<div>
<small>{simulation.sessionMode === "spectate-replay" ? "Recorded replay" : simulation.mode.replace("-", " ")}</small>
<strong>{simulation.dungeonId ?? "Dungeon simulation"}</strong>
</div>
<time>{formatTime(simulation.elapsedMs)}</time>
</div>
<div className="dungeon-sim-hud__controls">
<button type="button" onClick={simulation.toggleRunning} disabled={simulation.runState === "finished"}>
{simulation.runState === "running" ? "Pause" : "Play"}
</button>
<button type="button" onClick={simulation.step} disabled={simulation.runState !== "paused"}>Step</button>
{SPEEDS.map((speed) => (
<button
type="button"
key={speed}
className={simulation.speed === speed ? "is-active" : undefined}
onClick={() => simulation.setSpeed(speed)}
>
{speed}×
</button>
))}
<button type="button" onClick={() => simulation.setCameraMode(
simulation.cameraMode === "follow-leader" ? "free-fly" : "follow-leader",
)}>
{simulation.cameraMode === "follow-leader" ? "Follow camera" : "Free-fly camera"}
</button>
{simulation.sessionMode === "spectate-live" && simulation.runState !== "finished" ? (
<button type="button" className="is-danger" onClick={() => void finalizeDungeonSimulation("stopped")}>
Stop & save
</button>
) : null}
{simulation.replay && <button type="button" onClick={downloadReport}>Download JSON</button>}
{simulation.runState === "finished" && <button type="button" onClick={returnToGmTools}>Return to GM Tools</button>}
</div>
{simulation.sessionMode === "spectate-replay" && (
<label className="dungeon-sim-hud__seek">
<span>Replay position</span>
<input
type="range"
min={0}
max={replayDuration}
step={100}
value={Math.min(simulation.elapsedMs, replayDuration)}
onChange={(event) => simulation.seekReplay(Number(event.target.value))}
/>
</label>
)}
<dl className="dungeon-sim-hud__status">
<div><dt>State</dt><dd>{simulation.runState}{simulation.result ? ` · ${simulation.result}` : ""}</dd></div>
<div><dt>Objective</dt><dd>{leg?.objectiveName ?? simulation.objectiveId ?? "Complete"}</dd></div>
<div><dt>Checkpoint</dt><dd>{leg ? `${Math.min(simulation.checkpointIndex + 1, leg.checkpoints.length)} / ${leg.checkpoints.length}` : "—"}</dd></div>
<div><dt>Route</dt><dd>{simulation.routeHash?.slice(0, 12) ?? "—"}</dd></div>
</dl>
<section className="dungeon-sim-hud__findings">
<header><strong>Findings</strong><span>{simulation.findings.length}</span></header>
<ol>
{[...simulation.findings].reverse().slice(0, 12).map((finding) => (
<li key={finding.id} data-severity={finding.severity}>
<time>{formatTime(finding.atMs)}</time>
<span>{finding.message}</span>
</li>
))}
</ol>
</section>
</aside>
);
}