Release Healer Man 0.1.40

This commit is contained in:
phenom committed 2026-09-13 13:21:27 -04:00
1 parent ed0d2fea3d
commit 197a64117c
215 files changed
+5131 -181

No files matched your search

+43 -7
View File
@@ -1,10 +1,24 @@
import { useState, type KeyboardEvent } from "react";
import { inputKeyForEvent } from "../input/keybinds";
import { getInputPreferences, isCapturingInput } from "../input/keybindRuntime";
import { useInputBindingLabels } from "../input/useInputBindingLabels";
import { useGameStore } from "../game/store";
import { useMenuController } from "../input/useMenuController";
import { DungeonMapGraphic } from "./DungeonMapGraphic";
import { DungeonMapGraphic, type MapArtworkStatus } from "./DungeonMapGraphic";
import { requireDungeonDefinition } from "../game/dungeonRegistry";
import { resolveStagePresentation } from "../game/manastormStagePresentation";
import { useManastormStore } from "../game/manastormStore";
import { wowDungeonMapFor, wowMapContains, wowMapFloorFor, wowMapPoint } from "../game/wowDungeonMap";
export function closeMapForKey(event: Pick<KeyboardEvent, "code" | "ctrlKey" | "altKey" | "shiftKey" | "metaKey" | "repeat" | "defaultPrevented" | "preventDefault" | "stopPropagation">, close: () => void) {
if (event.repeat || event.defaultPrevented || isCapturingInput()) return;
const key = inputKeyForEvent(event);
if (!key || key !== getInputPreferences().pc.map) return;
// The gameplay listener intentionally ignores focused form controls.
event.preventDefault();
event.stopPropagation();
close();
}
function MapDialog() {
const bindingLabel = useInputBindingLabels();
@@ -17,6 +31,13 @@ function MapDialog() {
const encounter = useManastormStore((state) => state.currentEncounter);
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
const dungeon = requireDungeonDefinition(activeDungeonId);
const map = wowDungeonMapFor(dungeon);
const [floorLevel, setFloorLevel] = useState<number | null>(null);
const [showPatrols, setShowPatrols] = useState(false);
const [artworkStatus, setArtworkStatus] = useState<MapArtworkStatus>(map ? "loading" : "unavailable");
const floor = map ? wowMapFloorFor(map, position, floorLevel) : null;
const mapPosition = map && floor ? wowMapPoint(map, floor, position) : null;
const positionOnMap = Boolean(map && floor && wowMapContains(map, floor, position));
const stagePresentation = resolveStagePresentation(gameMode, encounter, {
title: dungeon.title,
mapId: dungeon.mapId,
@@ -25,27 +46,41 @@ function MapDialog() {
useMenuController([], { onBack: () => openOverlay("pause") });
return (
<aside className="map-panel" role="dialog" aria-modal="true" aria-labelledby="map-title">
<aside className="map-panel" role="dialog" aria-modal="true" aria-labelledby="map-title" onKeyDown={(event) => closeMapForKey(event, close)}>
<header>
<div>
<p className="eyebrow">Expedition survey</p>
<p className="eyebrow">{artworkStatus === "ready" ? "Dungeon atlas" : "Expedition survey"}</p>
<h2 id="map-title">{stagePresentation.title}</h2>
</div>
<button type="button" className="icon-button" onClick={close} aria-label="Close map">X</button>
</header>
<div className="map-panel__tools">
{map && map.floors.length > 1 && <label>Map floor
<select value={floorLevel ?? "auto"} onChange={(event) => setFloorLevel(event.target.value === "auto" ? null : Number(event.target.value))}>
<option value="auto">Auto — best match</option>
{map.floors.map((entry) => <option key={entry.level} value={entry.level}>{entry.name}</option>)}
</select>
</label>}
<label className="map-panel__patrol-toggle"><input type="checkbox" checked={showPatrols} onChange={(event) => setShowPatrols(event.target.checked)} />Patrol routes</label>
</div>
<div className="map-panel__canvas">
<DungeonMapGraphic position={position} yaw={yaw} />
<DungeonMapGraphic position={position} yaw={yaw} floorLevel={floorLevel} showPatrols={showPatrols} onArtworkStatus={setArtworkStatus} />
</div>
<div className="map-panel__readout">
<div><span>Current area</span><strong>{stagePresentation.areaName}</strong></div>
<div><span>Position</span><strong>{position.map((value) => Math.round(value)).join(" / ")}</strong></div>
<div><span>{artworkStatus === "ready" ? floor?.name : "Position"}</span><strong>{artworkStatus === "ready" && mapPosition
? positionOnMap ? `${(mapPosition[0] / 1002 * 100).toFixed(1)}%, ${(mapPosition[1] / 668 * 100).toFixed(1)}%` : "Outside selected floor"
: position.map((value) => Math.round(value)).join(" / ")}</strong></div>
</div>
<p className="map-panel__note">
{stagePresentation.usingFallbackEnvironment
? "Stage location is authoritative; the survey artwork is temporary fallback geometry. "
: "Gold markers show encounters. Dashed lines show database patrol routes. "}
: artworkStatus === "ready"
? "Original game artwork. White: you and camera direction. Green: entrance. Gold: numbered encounters. "
: artworkStatus === "loading" ? "Loading original game artwork. " : "Original artwork unavailable; showing the survey fallback. "}
{map && map.floors.length > 1 && "Choose a floor when rooms overlap. "}
Press {bindingLabel("pc", "map")} to close or {bindingLabel("controller", "menu-back")} to return to Pause.
</p>
</aside>
@@ -54,5 +89,6 @@ function MapDialog() {
export function MapPanel() {
const open = useGameStore((state) => state.overlay === "map");
return open ? <MapDialog /> : null;
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
return open ? <MapDialog key={activeDungeonId} /> : null;
}