Update Healer Man server

This commit is contained in:
phenom committed 2026-09-20 17:09:16 -04:00
1 parent 246b4832b0
commit 3e30af4c98
8 files changed
+309 -119

No files matched your search

+2
View File
@@ -153,6 +153,7 @@ export function CharacterCreateScreen() {
<p>{race.description}</p>
</section>
<main className="creator-preview-stage">
<div className="creator-preview-title"><small>Live preview</small><strong>{race.name} {characterClass.name}</strong></div>
<CharacterPreview draft={draft} />
<div className="preview-rotate">
<ControllerButton controlId="rotate-left" selectedId={controller.selectedId} select={controller.select} type="button" onClick={() => updateDraft({ rotation: draft.rotation - 12 })} aria-label="Rotate character left">&#8634;</ControllerButton>
@@ -205,6 +206,7 @@ export function CharacterCreateScreen() {
)}
</section>
<footer className="creator-footer">
<div className="creator-footer__selection"><small>Selected path</small><strong>{race.name} · {characterClass.name} · {draft.gender}</strong></div>
<ControllerButton controlId="creator-back" selectedId={controller.selectedId} select={controller.select} className="front-button" type="button" onClick={cancel}>Back to roster</ControllerButton>
<ControllerButton controlId="creator-next" selectedId={controller.selectedId} select={controller.select} className="front-button front-button--primary" type="button" onClick={() => setStep("appearance")}><span>Customize &amp; name</span><small>Next step</small></ControllerButton>
</footer>
+68 -66
View File
@@ -3,6 +3,7 @@ import { CLASSES, PLAYABLE_RACES, classById, contentCategoryForClass, FACTIONS,
import { contentCategoryById } from "../app/contentCategories";
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 { ControllerButton } from "./ControllerButton";
@@ -55,78 +56,79 @@ export function CharacterSelectScreen() {
<header className="roster-header">
<BrandMark compact />
<div><span>{session?.kind === "offline" ? "Offline expedition" : `Local account / ${session?.displayName}`}</span><strong>Choose your adventurer</strong></div>
<button type="button" onClick={signOut}>Sign out</button>
<ControllerButton controlId="sign-out" selectedId={controller.selectedId} select={controller.select} type="button" onClick={signOut}>Sign out</ControllerButton>
</header>
<main className="roster-stage">
<CharacterPreview character={selected} />
{selected && race && characterClass && (
<section className="roster-character-info">
<p>{FACTIONS[race.faction].name} / {selected.gender}</p>
<h1>{selected.name}</h1>
<strong style={{ color: characterClass.color }}>{race.name} {characterClass.name}{selected.secondaryClassId ? ` / ${classById(selected.secondaryClassId).name}` : ""}</strong>
<small>{characterClass.description}</small>
<dl>
<div><dt>Level</dt><dd>{selected.level}</dd></div>
<div><dt>Last camp</dt><dd>{selected.location}</dd></div>
<div><dt>Role</dt><dd>{characterClass.role}</dd></div>
</dl>
</section>
)}
</main>
<aside className="roster-list" aria-label="Saved characters">
<div className="roster-list__heading"><span>Characters</span><b>{characters.length} / 10</b></div>
{characters.map((character) => {
const raceDefinition = raceById(character.raceId);
const classDefinition = classById(character.classId);
const categoryDefinition = contentCategoryById(character.categoryId ?? contentCategoryForClass(character.classId));
return (
<ControllerButton
key={character.id}
controlId={`character-${character.id}`}
selectedId={controller.selectedId}
select={controller.select}
className={`roster-character ${character.id === selectedId ? "is-active" : ""}`}
type="button"
onClick={() => select(character.id)}
>
<span style={{ "--portrait-accent": raceDefinition.accent } as CSSProperties}>{raceDefinition.sigil}</span>
<i><strong>{character.name}</strong><small>{categoryDefinition.label} / Level {character.level} {classDefinition.name}{character.secondaryClassId ? ` + ${classById(character.secondaryClassId).name}` : ""}</small></i>
<em>{formatLastPlayed(character.lastPlayedAt)}</em>
</ControllerButton>
);
})}
{!characters.length && <div className="roster-empty"><span>+</span><strong>No adventurers yet</strong><small>Create one to enter a dungeon.</small></div>}
<div className="roster-list__heading"><span>Your characters</span><b>{characters.length} / 10</b></div>
<div className="roster-list__entries">
{characters.map((character) => {
const classDefinition = classById(character.classId);
const specialization = specializationById(character.specializationId);
const categoryDefinition = contentCategoryById(character.categoryId ?? contentCategoryForClass(character.classId));
return (
<ControllerButton
key={character.id}
controlId={`character-${character.id}`}
selectedId={controller.selectedId}
select={controller.select}
className={`roster-character ${character.id === selectedId ? "is-active" : ""}`}
type="button"
onClick={() => select(character.id)}
>
<span
className="roster-character__icon"
style={{ "--portrait-accent": classDefinition.color } as CSSProperties}
title={`${specialization?.displayName ?? classDefinition.name} icon`}
>
<img src={specializationIconUrl(character.classId, character.specializationId)} alt="" aria-hidden="true" />
</span>
<i><strong>{character.name}</strong><small>{categoryDefinition.label} / Level {character.level} {classDefinition.name}{character.secondaryClassId ? ` + ${classById(character.secondaryClassId).name}` : ""}</small></i>
<em>{formatLastPlayed(character.lastPlayedAt)}</em>
</ControllerButton>
);
})}
{!characters.length && <div className="roster-empty"><span>+</span><strong>No adventurers yet</strong><small>Create one to enter a dungeon.</small></div>}
</div>
<ControllerButton controlId="new-character" selectedId={controller.selectedId} select={controller.select} className="roster-new" type="button" onClick={openCreator}>
<b>+</b><span><strong>Create character</strong><small>{PLAYABLE_RACES.length} races / {CLASSES.length} classes</small></span>
</ControllerButton>
</aside>
<main className="roster-stage">
<CharacterPreview character={selected} />
{selected && race && characterClass && (
<>
<section className="roster-character-info">
<p>{FACTIONS[race.faction].name} / {selected.gender}</p>
<h1>{selected.name}</h1>
<strong style={{ color: characterClass.color }}>{race.name} {characterClass.name}{selected.secondaryClassId ? ` / ${classById(selected.secondaryClassId).name}` : ""}</strong>
<small>{characterClass.description}</small>
</section>
<ControllerButton
controlId="continue-character"
selectedId={controller.selectedId}
select={controller.select}
className="front-button front-button--primary roster-stage-continue"
type="button"
onClick={continueWithCharacter}
>Continue as {selected.name}</ControllerButton>
</>
)}
</main>
<aside className="roster-details">
<p className="front-kicker">Expedition record</p>
{selected && race && characterClass ? (
<>
<h2>{selected.location}</h2>
<dl>
<div><dt>Level</dt><dd>{selected.level}</dd></div>
<div><dt>Content</dt><dd>{contentCategoryById(selected.categoryId ?? contentCategoryForClass(selected.classId)).name}</dd></div>
<div><dt>Faction</dt><dd>{FACTIONS[race.faction].name}</dd></div>
<div><dt>Class role</dt><dd>{characterClass.role}</dd></div>
<div><dt>Next step</dt><dd>Choose a game mode</dd></div>
</dl>
<ControllerButton controlId="continue-character" selectedId={controller.selectedId} select={controller.select} className="front-button front-button--primary roster-enter" type="button" onClick={continueWithCharacter}>
<span>Continue</span><small>Open the main menu</small>
</ControllerButton>
<ControllerButton controlId="delete-character" selectedId={controller.selectedId} select={controller.select} className="text-action text-action--danger" type="button" onClick={() => setConfirmDelete(true)}>Delete character</ControllerButton>
</>
) : (
<><h2>New expedition</h2><p>Create a character using the options loaded from your wow335a client.</p></>
)}
{notice && <p className="front-notice" role="status">{notice}</p>}
</aside>
<footer className="roster-footer">
<div className="roster-footer__status">
<span>{selected && race && characterClass
? `${contentCategoryById(selected.categoryId ?? contentCategoryForClass(selected.classId)).name} · ${FACTIONS[race.faction].name}`
: "Select an adventurer or forge a new one"}</span>
{notice && <p className="front-notice" role="status">{notice}</p>}
</div>
{selected && <ControllerButton controlId="delete-character" selectedId={controller.selectedId} select={controller.select} className="text-action text-action--danger" type="button" onClick={() => setConfirmDelete(true)}>Delete</ControllerButton>}
<ControllerButton
controlId="continue-character"
selectedId={controller.selectedId}
select={controller.select}
className="front-button front-button--primary roster-enter"
type="button"
disabled={!selected}
onClick={continueWithCharacter}
>
<span>{selected ? `Continue as ${selected.name}` : "Choose a character"}</span><small>{selected ? "Enter the adventure menu" : "Select from your roster"}</small>
</ControllerButton>
</footer>
<ControllerLegend />
{confirmDelete && selected && (
<div className="front-modal-backdrop">
+7 -1
View File
@@ -11,6 +11,7 @@ import {
import { useGameStore } from "../game/store";
import type { GameOverlay } from "../game/store";
import { ACTION_CONTROLS } from "../game/actionBindings";
import { specializationById, specializationIconUrl } from "../game/specializationCatalog";
import {
DUNGEON_CATALOG,
DUNGEON_CATEGORIES,
@@ -176,7 +177,12 @@ export function CompanionPanel({ snapshot, send, preferControllerBinds = false }
<div className="companion-mini-roster" aria-label="Character quick select">
{snapshot.characters.map((character) => (
<button key={character.id} type="button" className={character.id === snapshot.selectedCharacterId ? "is-active" : ""} onClick={() => send({ name: "selectCharacter", characterId: character.id })}>
<span>{raceById(character.raceId).sigil}</span><strong>{character.name}</strong><small>Lv {character.level}</small>
<span
className="companion-character-icon"
title={`${specializationById(character.specializationId)?.displayName ?? classById(character.classId).name} icon`}
>
<img src={specializationIconUrl(character.classId, character.specializationId)} alt="" aria-hidden="true" />
</span><strong>{character.name}</strong><small>Lv {character.level}</small>
</button>
))}
{!snapshot.characters.length && <p>No characters saved yet.</p>}
+18 -10
View File
@@ -118,16 +118,24 @@
.login-form__actions .front-button--primary { grid-column: auto; }
.login-form__actions .front-button small { display: none; }
.login-form footer { display: none; }
.character-select-screen { grid-template-columns: 220px minmax(0, 1fr); }
.character-select-screen { grid-template-columns: minmax(0, 1fr) 300px; grid-template-rows: 52px minmax(0, 1fr) 62px; gap: 8px; }
.roster-header, .character-select-screen > .controller-legend { grid-column: 1 / 3; }
.roster-details { display: none; }
.roster-stage-continue { display: flex; }
.character-create-screen { grid-template-columns: 42% 58%; grid-template-rows: 47px 3px minmax(0, 1fr) 58px; }
.creator-header, .creator-progress, .creator-footer { grid-column: 1 / 3; }
.roster-list { padding: 10px; }
.roster-character { min-height: 58px; }
.roster-character-info { width: min(215px, 42%); padding: 13px; }
.roster-character-info h1 { font-size: 30px; }
.roster-footer { padding-top: 6px; }
.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; }
.creator-preview-stage, .appearance-preview-stage { display: none; }
.creator-classes { grid-column: 2; }
.appearance-controls { grid-column: 1 / 3; }
.creator-preview-stage { display: block; grid-column: 2; }
.creator-classes { grid-column: 3; }
.creator-races, .creator-classes { margin-top: 8px; margin-bottom: 7px; padding: 9px; }
.creator-races { margin-right: 6px; }
.creator-classes { margin-left: 6px; }
.creator-preview-stage { margin: 8px 0 7px; }
.appearance-preview-stage { display: block; grid-column: 1; }
.appearance-controls { grid-column: 2 / 4; }
.creator-footer { gap: 8px; }
.dungeon-select-screen { grid-template-columns: 260px minmax(0, 1fr); gap: 10px; }
.dungeon-select-detail { grid-template-columns: 1fr; gap: 10px; }
@@ -136,8 +144,8 @@
.dungeon-role-option { min-height: 48px; }
.front-surface--top :is(.race-option > span, .class-option strong, .roster-character small, .main-menu-mode small) { font-size: 12px; }
.race-option > b { font-size: 10px; }
.creator-classes { grid-template-rows: auto minmax(100px, 1fr) auto; overflow-y: auto; }
.class-summary { max-height: 155px; overflow-y: auto; }
.creator-classes { grid-template-rows: auto minmax(90px, 1fr) auto auto; overflow-y: auto; }
.class-summary { max-height: 100px; overflow-y: auto; }
.class-option small { font-size: 11px; }
.game-panel :is(p, small, label, dt, dd, em), .game-panel :is(button, select, input) { font-size: 12px; }
.game-panel :is(button, select, input), .pause-menu__actions .button { min-height: 44px; }