Update Healer Man server
This commit is contained in:
1 parent
3e30af4c98
commit
97ee0c13f9
25 files changed
+576
-179
No files matched your search
@@ -14,11 +14,11 @@ export function FrontSurface({ children, className = "", bottom = false, ariaLab
|
||||
);
|
||||
}
|
||||
|
||||
export function BrandMark({ compact = false }: { compact?: boolean }) {
|
||||
export function BrandMark({ compact = false, tagline = "Answer the call" }: { compact?: boolean; tagline?: string | null }) {
|
||||
return (
|
||||
<div className={`front-brand ${compact ? "front-brand--compact" : ""}`}>
|
||||
<span className="brand-sigil" aria-hidden="true">+</span>
|
||||
<span><small>Answer the call</small><strong>Healer Man</strong></span>
|
||||
<span>{tagline && <small>{tagline}</small>}<strong>Healer Man</strong></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+22
-28
@@ -1,9 +1,9 @@
|
||||
import { lazy, Suspense, useMemo, useState } from "react";
|
||||
import { classById, FACTIONS, raceById } from "../app/characterCatalog";
|
||||
import { classById } from "../app/characterCatalog";
|
||||
import { selectedCharacter, useShellStore } from "../app/shellStore";
|
||||
import { DualDisplayFrame } from "../components/DualDisplayFrame";
|
||||
import { specializationById, specializationIconUrl } from "../game/specializationCatalog";
|
||||
import { useMenuController, type MenuAction } from "../input/useMenuController";
|
||||
import { CharacterPreview } from "./CharacterPreview";
|
||||
import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel";
|
||||
import { ControllerButton } from "./ControllerButton";
|
||||
import { ContentManagerScreen } from "./ContentManagerScreen";
|
||||
@@ -170,8 +170,9 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
|
||||
onBack: returnToCharacters,
|
||||
});
|
||||
|
||||
const race = character ? raceById(character.raceId) : null;
|
||||
const characterClass = character ? classById(character.classId) : null;
|
||||
const specialization = specializationById(character?.specializationId);
|
||||
const secondaryClass = character?.secondaryClassId ? classById(character.secondaryClassId) : null;
|
||||
|
||||
return (
|
||||
<DualDisplayFrame
|
||||
@@ -179,11 +180,7 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
|
||||
top={
|
||||
<FrontSurface className="main-menu-screen" ariaLabel="Healer Man main menu">
|
||||
<header className="main-menu-header">
|
||||
<BrandMark compact />
|
||||
<div>
|
||||
<span>Adventurer selected</span>
|
||||
<strong>Choose your next journey</strong>
|
||||
</div>
|
||||
<BrandMark compact tagline={null} />
|
||||
<ControllerButton
|
||||
controlId="back-to-roster"
|
||||
selectedId={controller.selectedId}
|
||||
@@ -195,30 +192,27 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
|
||||
</ControllerButton>
|
||||
</header>
|
||||
|
||||
<aside className="main-menu-character" aria-label="Selected character">
|
||||
<div className="main-menu-character__preview">
|
||||
<CharacterPreview character={character} />
|
||||
</div>
|
||||
{character && race && characterClass && (
|
||||
<div className="main-menu-character__summary">
|
||||
<p className="front-kicker">Ready for adventure</p>
|
||||
<h1>{character.name}</h1>
|
||||
<strong style={{ color: characterClass.color }}>
|
||||
Level {character.level} {race.name} {characterClass.name}
|
||||
</strong>
|
||||
<small>{FACTIONS[race.faction].name} / {character.gender}</small>
|
||||
{isRom && <small>RoM / {character.secondaryClassId ? `${characterClass.name} + ${classById(character.secondaryClassId).name}` : "Secondary class not chosen"}</small>}
|
||||
</div>
|
||||
<aside className="main-menu-sidebar" aria-label="Character and online group">
|
||||
{character && characterClass && (
|
||||
<section className="main-menu-character" aria-label="Selected character">
|
||||
<span className="main-menu-character__icon" style={{ borderColor: characterClass.color }}>
|
||||
<img src={specializationIconUrl(character.classId, character.specializationId)} alt="" aria-hidden="true" />
|
||||
</span>
|
||||
<div className="main-menu-character__summary">
|
||||
<small>Level {character.level}</small>
|
||||
<h1>{character.name}</h1>
|
||||
<strong style={{ color: characterClass.color }}>
|
||||
{characterClass.name}{isRom
|
||||
? secondaryClass ? ` / ${secondaryClass.name}` : " / No secondary class"
|
||||
: specialization ? ` / ${specialization.displayName}` : ""}
|
||||
</strong>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<OnlineGroupPanel defaultOpen />
|
||||
</aside>
|
||||
|
||||
<main className="main-menu-modes">
|
||||
<header>
|
||||
<p className="front-kicker">Main menu</p>
|
||||
<h2>Ways to play</h2>
|
||||
<p>Select a destination. More adventures will unlock here as they are built.</p>
|
||||
</header>
|
||||
<OnlineGroupPanel />
|
||||
<nav className="main-menu-mode-grid" aria-label="Main menu destinations">
|
||||
<ControllerButton controlId="mode-companion-readiness" selectedId={controller.selectedId} select={controller.select}
|
||||
className="front-button main-menu-mode main-menu-mode--available" type="button" onClick={onOpenReadiness}>
|
||||
|
||||
@@ -9,9 +9,12 @@ import {
|
||||
type PartyRole,
|
||||
} from "../game/partyRoles";
|
||||
|
||||
export function OnlineGroupPanel({ forceAiFill = false }: { readonly forceAiFill?: boolean }) {
|
||||
export function OnlineGroupPanel({ forceAiFill = false, defaultOpen = false }: {
|
||||
readonly forceAiFill?: boolean;
|
||||
readonly defaultOpen?: boolean;
|
||||
}) {
|
||||
const [username, setUsername] = useState("");
|
||||
const [panelOpen, setPanelOpen] = useState(false);
|
||||
const [panelOpen, setPanelOpen] = useState(defaultOpen);
|
||||
const session = useShellStore((state) => state.session);
|
||||
const character = useShellStore(selectedCharacter);
|
||||
const snapshot = useOnlineGroupStore((state) => state.snapshot);
|
||||
|
||||
@@ -8,22 +8,21 @@ import { dungeonContentIsUsable, installContentPack } from "../content/contentMa
|
||||
import { useContentManagerSnapshot } from "../content/useContentManager";
|
||||
import {
|
||||
RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID,
|
||||
STORMWIND_STOCKADE_DUNGEON_ID,
|
||||
dungeonContentPackId,
|
||||
} from "../game/dungeonContentSource";
|
||||
import { randomizedBiteSizedCreatureCatalog } from "../game/dungeons/randomizedBiteSizedStockades";
|
||||
import { dungeonById } from "../game/dungeonCatalog";
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { RANDOMIZED_BITE_SIZED_CREATURE_CATALOG, requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { BrandMark, FrontSurface } from "./FrontSurface";
|
||||
import { DungeonDifficultySlider } from "./DungeonDifficultySlider";
|
||||
import { OnlineGroupPanel } from "./OnlineGroupPanel";
|
||||
import { CompanionPoolControls } from "./CompanionPoolControls";
|
||||
import { CompanionReadinessScreen } from "./CompanionReadinessScreen";
|
||||
import { ensureRandomizedBiteSizedRunContent } from "../app/randomizedBiteSizedContent";
|
||||
|
||||
installDungeonShellRuntime();
|
||||
|
||||
const DUNGEON = dungeonById(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID);
|
||||
const CANDIDATES = randomizedBiteSizedCreatureCatalog(requireDungeonDefinition(STORMWIND_STOCKADE_DUNGEON_ID));
|
||||
const CANDIDATES = RANDOMIZED_BITE_SIZED_CREATURE_CATALOG;
|
||||
|
||||
export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () => void }) {
|
||||
useContentManagerSnapshot();
|
||||
@@ -51,11 +50,21 @@ export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () =>
|
||||
const launch = async (role: PartyRole) => {
|
||||
if (!character || !DUNGEON?.available) return;
|
||||
if (!onlineGroup || !session || session.kind !== "account") {
|
||||
enterDungeon(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, role, {
|
||||
difficultyId: requireDungeonDefinition(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID).defaultDifficultyId,
|
||||
bossDifficulty,
|
||||
freshRun: true,
|
||||
});
|
||||
const runId = globalThis.crypto.randomUUID();
|
||||
setDownloading(true); setError("");
|
||||
try {
|
||||
await ensureRandomizedBiteSizedRunContent(runId);
|
||||
enterDungeon(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, role, {
|
||||
difficultyId: requireDungeonDefinition(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID).defaultDifficultyId,
|
||||
bossDifficulty,
|
||||
freshRun: true,
|
||||
runId,
|
||||
});
|
||||
} catch (caught) {
|
||||
setError(caught instanceof Error ? caught.message : "The randomized creature download failed.");
|
||||
} finally {
|
||||
setDownloading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!await updateOnlineMember(session, character, role)) return;
|
||||
@@ -99,9 +108,9 @@ export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () =>
|
||||
</header>
|
||||
<main className="randomized-bite-sized-screen__layout">
|
||||
<section className="randomized-bite-sized-screen__hero">
|
||||
<p className="front-kicker">A different cellblock every run</p>
|
||||
<p className="front-kicker">A different lineup every run</p>
|
||||
<h1>Randomized Bite-Sized</h1>
|
||||
<p>Fight through the short Stockades route with every ordinary enemy slot rerolled, then face one randomly selected warden.</p>
|
||||
<p>The mode picks an eligible short layout—currently Cellblock Rush—deals a different cross-dungeon creature into every enemy slot, then rolls the boss separately.</p>
|
||||
<div className="randomized-bite-sized-screen__odds">
|
||||
<div><strong>{mobCount}</strong><span>mob types in the shuffle</span></div>
|
||||
<div><strong>{bossCount}</strong><span>possible final bosses</span></div>
|
||||
@@ -116,11 +125,11 @@ export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () =>
|
||||
<h2>{DUNGEON?.title ?? "Cellblock Shuffle"}</h2>
|
||||
<DungeonDifficultySlider value={bossDifficulty} onChange={setBossDifficulty} selectedId="" select={() => undefined} />
|
||||
{!installed ? (
|
||||
<button className="front-button front-button--primary" type="button" disabled={downloading} onClick={() => void install()}>{downloading ? "Installing…" : "Install Stockades content"}</button>
|
||||
<button className="front-button front-button--primary" type="button" disabled={downloading} onClick={() => void install()}>{downloading ? "Installing…" : "Install randomized dungeon content"}</button>
|
||||
) : roles.length ? (
|
||||
<div className="randomized-bite-sized-screen__roles">
|
||||
<strong>Enter as</strong>
|
||||
{roles.map((role) => <button className="front-button front-button--primary" key={role} type="button" disabled={!DUNGEON?.available} onClick={() => void launch(role)}>{PARTY_ROLE_LABELS[role]}</button>)}
|
||||
{roles.map((role) => <button className="front-button front-button--primary" key={role} type="button" disabled={!DUNGEON?.available || downloading} onClick={() => void launch(role)}>{downloading ? "Preparing run…" : PARTY_ROLE_LABELS[role]}</button>)}
|
||||
</div>
|
||||
) : <p className="front-notice">Choose a specialization in your Class Hall before entering.</p>}
|
||||
{!DUNGEON?.available && <p className="front-notice">This randomized route is not enterable in the current content build.</p>}
|
||||
|
||||
@@ -6,17 +6,11 @@ import {
|
||||
useRandomizedCreatureReadiness,
|
||||
} from "../app/randomizedCreatureReadiness";
|
||||
import { useShellStore } from "../app/shellStore";
|
||||
import { STORMWIND_STOCKADE_DUNGEON_ID } from "../game/dungeonContentSource";
|
||||
import {
|
||||
randomizedBiteSizedCreatureCatalog,
|
||||
type RandomizedCreatureKind,
|
||||
} from "../game/dungeons/randomizedBiteSizedStockades";
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import type { RandomizedCreatureKind } from "../game/dungeons/randomizedBiteSizedStockades";
|
||||
import { RANDOMIZED_BITE_SIZED_CREATURE_CATALOG } from "../game/dungeonRegistry";
|
||||
import { BrandMark, FrontSurface } from "./FrontSurface";
|
||||
|
||||
const CREATURES = randomizedBiteSizedCreatureCatalog(
|
||||
requireDungeonDefinition(STORMWIND_STOCKADE_DUNGEON_ID),
|
||||
);
|
||||
const CREATURES = RANDOMIZED_BITE_SIZED_CREATURE_CATALOG;
|
||||
|
||||
type KindFilter = "all" | RandomizedCreatureKind;
|
||||
type StatusFilter = "all" | CreatureReadinessStatus;
|
||||
|
||||
@@ -125,6 +125,13 @@
|
||||
.roster-character-info { width: min(215px, 42%); padding: 13px; }
|
||||
.roster-character-info h1 { font-size: 30px; }
|
||||
.roster-footer { padding-top: 6px; }
|
||||
.main-menu-screen { grid-template-columns: minmax(0, 1fr) 285px; grid-template-rows: 50px minmax(0, 1fr) 24px; gap: 8px; }
|
||||
.main-menu-sidebar { grid-column: 2; grid-row: 2; display: block; max-height: none; overflow-y: auto; }
|
||||
.main-menu-character { grid-template-columns: 48px minmax(0, 1fr); gap: 9px; padding: 10px; }
|
||||
.main-menu-character__icon { width: 46px; height: 46px; }
|
||||
.main-menu-character__summary h1 { font-size: 21px; }
|
||||
.main-menu-modes { grid-column: 1; grid-row: 2; padding: 0 2px 8px 0; }
|
||||
.main-menu-mode-grid { margin-top: 0; }
|
||||
.character-create-screen { grid-template-columns: 225px minmax(220px, 1fr) 260px; grid-template-rows: 50px 3px minmax(0, 1fr) 60px; }
|
||||
.creator-header, .creator-progress, .creator-footer { grid-column: 1 / 4; }
|
||||
.creator-races { grid-column: 1; }
|
||||
|
||||
Reference in new issue
Block a user