forked from phenom/healer-man
Release Healer Man 0.1.46
This commit is contained in:
1 parent
f82daf1c13
commit
9ff574ac0b
73 files changed
+33486
-174
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user