Update Healer Man server
This commit is contained in:
1 parent
246b4832b0
commit
3e30af4c98
8 files changed
+309
-119
No files matched your search
@@ -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">↺</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 & name</span><small>Next step</small></ControllerButton>
|
||||
</footer>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
Reference in new issue
Block a user