forked from phenom/healer-man
Release Healer Man 0.1.59
This commit is contained in:
1 parent
11934a3979
commit
ab1434a3b8
44 files changed
+1242
-103
No files matched your search
@@ -4,7 +4,7 @@ import { formatDungeonDuration } from "../game/dungeonRunTypes";
|
||||
import { useGameStore } from "../game/store";
|
||||
import "./dungeonRace.css";
|
||||
|
||||
export function DungeonRaceHud() {
|
||||
export function DungeonRaceHud({ region }: { readonly region: "status" | "overlays" }) {
|
||||
const race = useDungeonRaceStore();
|
||||
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
@@ -18,13 +18,15 @@ export function DungeonRaceHud() {
|
||||
const elapsed = race.startAt ? Math.max(0, serverNow - race.startAt) : 0;
|
||||
const shownTime = race.clearDurationMs ?? elapsed;
|
||||
const toast = race.result && race.toastAt && now - race.toastAt < 4_000;
|
||||
return <>
|
||||
if (region === "status") return (
|
||||
<section className="dungeon-race-hud" aria-label="Dungeon race status">
|
||||
<span>Dungeon Race · {race.mode === "practice" ? "Practice" : "Ranked"}</span>
|
||||
<strong>{formatDungeonDuration(shownTime)}</strong>
|
||||
<small>vs {race.opponent.name}{race.opponent.type === "bot" ? ` · ${race.opponent.tier}` : ""}
|
||||
{race.opponentProgress !== null ? ` · ${race.opponentProgress}%` : ""}</small>
|
||||
</section>
|
||||
);
|
||||
return <>
|
||||
{race.status === "preparing" && <div className="dungeon-race-wait" role="status">Waiting for the rival party to be ready…</div>}
|
||||
{remaining !== null && remaining > 0 && race.status === "countdown" &&
|
||||
<div className="dungeon-race-countdown" role="status" aria-live="polite">Race starts in <strong>{remaining}</strong></div>}
|
||||
|
||||
@@ -13,8 +13,15 @@ export function EncounterTimeline({ now }: { readonly now: number }) {
|
||||
const mobs = useCombatStore((state) => state.mobs);
|
||||
const model = useMemo(() => encounterTimeline(mobs, now), [mobs, now]);
|
||||
if (!model) return null;
|
||||
const boss = mobs[model.bossId];
|
||||
const healthPercent = Math.max(0, Math.min(100, boss.health / Math.max(1, boss.maxHealth) * 100));
|
||||
return (
|
||||
<section className="encounter-timeline" aria-label={`${model.bossName} mechanic timeline`}>
|
||||
<div className="encounter-timeline__boss-health" aria-label={`${model.bossName} health: ${Math.ceil(boss.health)} of ${boss.maxHealth}`}>
|
||||
<div><span>Boss health</span><strong>{Math.ceil(healthPercent)}%</strong></div>
|
||||
<div className="encounter-timeline__health-track"><span style={{ width: `${healthPercent}%` }} /></div>
|
||||
<small>{Math.ceil(boss.health)} / {boss.maxHealth}</small>
|
||||
</div>
|
||||
<header><span>Mechanics</span><strong>{model.bossName}</strong></header>
|
||||
<div className="encounter-timeline__rows">
|
||||
{model.rows.map((row) => (
|
||||
|
||||
@@ -30,6 +30,7 @@ import { ExperienceBar } from "./ExperienceBar";
|
||||
import { Minimap } from "./Minimap";
|
||||
import { PlayerStatusPanel } from "./PlayerStatusPanel";
|
||||
import { EncounterTimeline } from "./EncounterTimeline";
|
||||
import { DungeonRaceHud } from "./DungeonRaceHud";
|
||||
import { PartyFrames } from "./PartyFrames";
|
||||
import { ResurrectionDestination, useResurrectionCheckpoints } from "./ResurrectionDestination";
|
||||
import type { ResurrectionCheckpoint } from "../game/resurrectionCheckpoints";
|
||||
@@ -441,6 +442,7 @@ export function Hud({ surface = "top", onLayoutChange, completion, onFinish, onR
|
||||
) : null}
|
||||
|
||||
<div className="hud-top-right">
|
||||
<DungeonRaceHud region="status" />
|
||||
<EditableHudElement id="minimap"><Minimap
|
||||
position={playerPosition}
|
||||
yaw={cameraYaw}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.dungeon-race-hud{position:absolute;z-index:42;top:8px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:1px;min-width:180px;padding:5px 14px;border:1px solid #a79261;border-radius:9px;background:#091217e8;color:#f5e8ca;text-align:center;pointer-events:none;box-shadow:0 5px 18px #0009}
|
||||
.dungeon-race-hud{position:absolute;z-index:42;top:0;right:calc(100% + 8px);display:flex;flex-direction:column;align-items:center;gap:1px;min-width:180px;padding:5px 14px;border:1px solid #a79261;border-radius:9px;background:#091217e8;color:#f5e8ca;text-align:center;pointer-events:none;box-shadow:0 5px 18px #0009}
|
||||
.dungeon-race-hud span{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:#c2ac78}
|
||||
.dungeon-race-hud strong{font-variant-numeric:tabular-nums;font-size:21px;line-height:1.1}
|
||||
.dungeon-race-hud small{font-size:10px;white-space:nowrap}
|
||||
@@ -7,7 +7,7 @@
|
||||
.dungeon-race-toast{position:absolute;z-index:65;top:74px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;padding:9px 18px;border:1px solid #a79261;border-radius:9px;background:#101516f2;color:#ffe4a3;font-size:22px;font-weight:700;pointer-events:none}
|
||||
.dungeon-race-toast--loss{border-color:#bd6b63;color:#ffb2a8}
|
||||
.dungeon-race-toast small{font-size:11px;color:#f5e8ca}
|
||||
@media(max-width:650px){.dungeon-race-hud{top:4px;min-width:145px;padding:3px 8px}.dungeon-race-hud strong{font-size:17px}.dungeon-race-toast{top:64px}}
|
||||
@media(max-width:650px){.dungeon-race-hud{min-width:145px;padding:3px 8px}.dungeon-race-hud strong{font-size:17px}.dungeon-race-toast{top:64px}}
|
||||
.dungeon-race-screen{min-height:100%;padding:16px;color:#f5e8ca}
|
||||
.dungeon-race-screen__header{display:flex;align-items:center;gap:18px}.dungeon-race-screen__header h1{margin:2px 0;font-size:25px}.dungeon-race-screen__header button{margin-left:auto}
|
||||
.dungeon-race-screen__layout{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(280px,.9fr);gap:18px;margin-top:16px}
|
||||
|
||||
@@ -31,8 +31,9 @@ const mainMenuSource = readFileSync(
|
||||
);
|
||||
|
||||
describe("gameplay panel loading integration", () => {
|
||||
it("loads talent resources behind the main-menu catalog gate, not during gameplay", () => {
|
||||
it("loads talent resources at gameplay entry and on demand in the menu editor", () => {
|
||||
expect(appSource).toContain("preloadTalentGameplayResources(talentClassId)");
|
||||
expect(appSource).toContain('preloadGameplay={phase === "game"}');
|
||||
expect(talentResourcesSource).toContain("loadTalentUiCatalog(classId)");
|
||||
expect(talentResourcesSource).toContain("loadTalentGameplayPanel()");
|
||||
expect(talentResourcesSource).toContain("image.decode()");
|
||||
@@ -44,7 +45,7 @@ describe("gameplay panel loading integration", () => {
|
||||
expect(gameplayPanelsSource).not.toContain("<Suspense fallback={null}><TalentGameplayPanel />");
|
||||
});
|
||||
|
||||
it("warms the shared runtime and first-open panels before showing the main menu", () => {
|
||||
it("warms the shared runtime and first-open panels before gameplay mounts", () => {
|
||||
expect(appSource).toContain("preloadSharedGameplayResources(originalMediaEnabled)");
|
||||
expect(appSource).toContain("getLoadedGameRuntime()");
|
||||
expect(gameplayResourcesSource).toContain('import("../GameRuntime")');
|
||||
|
||||
Reference in new issue
Block a user