From 3e30af4c98fccdd89fab51e0d8f9ab2a4731e11e Mon Sep 17 00:00:00 2001 From: phenom Date: Sun, 20 Sep 2026 17:09:16 -0400 Subject: [PATCH] Update Healer Man server --- PLAYTEST_NOTES.md | 16 +++ src/game/specializationCatalog.test.ts | 12 ++ src/game/specializationCatalog.ts | 46 +++++++ src/styles.css | 182 +++++++++++++++++++------ src/ui/CharacterCreateScreen.tsx | 2 + src/ui/CharacterSelectScreen.tsx | 134 +++++++++--------- src/ui/CompanionPanel.tsx | 8 +- src/ui/thorUi.css | 28 ++-- 8 files changed, 309 insertions(+), 119 deletions(-) diff --git a/PLAYTEST_NOTES.md b/PLAYTEST_NOTES.md index cf68b170..4dc17bd2 100644 --- a/PLAYTEST_NOTES.md +++ b/PLAYTEST_NOTES.md @@ -3683,3 +3683,19 @@ Vite's existing large-chunk advisory remains informational. - All 12 release-manager unit tests and Python compilation passed. The real Windows Tk smoke launch also initialized the default-on mode and closed cleanly. + +## Thor character shell redesign (2026-09-20) + +- The `960 x 540` top-display roster now keeps its character stage on the left, + a 300-pixel saved-character rail on the right, and the primary Continue action + inside a 62-pixel bottom bar. Both creator steps retain the shared live model + preview and fit without document overflow. +- All 47 avatar/rendering tests and the full production build passed. Browser + automation created a Human Priest, exercised both creator steps, and verified + the roster geometry without page errors. Evidence is in + `playtest-artifacts/character-shell/01-creator-path.png`, + `playtest-artifacts/character-shell/02-creator-appearance.png`, and + `playtest-artifacts/character-shell/03-character-select.png`. +- A production Wailing Caverns smoke run rendered the player and all four party + members with intact bodies and starter weapon silhouettes. Evidence is in + `playtest-artifacts/character-shell/04-in-dungeon.png`. diff --git a/src/game/specializationCatalog.test.ts b/src/game/specializationCatalog.test.ts index 6a456999..3a7ab947 100644 --- a/src/game/specializationCatalog.test.ts +++ b/src/game/specializationCatalog.test.ts @@ -3,6 +3,7 @@ import { CLASSES } from "../app/characterCatalog"; import { SPECIALIZATION_DEFINITIONS, specializationById, + specializationIconUrl, specializationsForClass, } from "./specializationCatalog"; @@ -35,4 +36,15 @@ describe("specialization registry", () => { expect(coa.every((definition) => ["tank", "healer", "damage"].includes(definition.role))).toBe(true); expect(specializationById("barbarian:ancestry")?.role).toBe("damage"); }); + + it("uses dedicated classic spec icons with safe class-icon fallbacks", () => { + expect(specializationIconUrl("priest", "priest:holy")) + .toBe("/assets/ui/talents/icons/spell_holy_guardianspirit.png"); + expect(specializationIconUrl("druid", "druid:feral-cat")) + .toBe("/assets/ui/talents/icons/ability_racial_bearform.png"); + expect(specializationIconUrl("barbarian", "barbarian:ancestry")) + .toBe("/assets/ui/spells/classicon128_barbarian.png"); + expect(specializationIconUrl("priest", "mage:frost")) + .toBe("/assets/ui/spells/classicon128_priest.png"); + }); }); diff --git a/src/game/specializationCatalog.ts b/src/game/specializationCatalog.ts index 3ba75577..7d3bc9a2 100644 --- a/src/game/specializationCatalog.ts +++ b/src/game/specializationCatalog.ts @@ -1,6 +1,7 @@ import { baseClassFor, classById, + classIconUrl, type ClassId, } from "../app/characterCatalog"; import type { ItemStats, NumericItemStatKey } from "./itemStats"; @@ -136,6 +137,40 @@ export const SPECIALIZATION_DEFINITIONS: readonly SpecializationDefinition[] = O const BY_ID = new Map(SPECIALIZATION_DEFINITIONS.map((definition) => [definition.id, definition])); +const CLASSIC_SPECIALIZATION_ICON_BASENAME: Readonly> = Object.freeze({ + "warrior:arms": "ability_rogue_eviscerate", + "warrior:fury": "ability_warrior_innerrage", + "warrior:protection": "inv_shield_06", + "paladin:holy": "spell_holy_holybolt", + "paladin:protection": "spell_holy_devotionaura", + "paladin:retribution": "spell_holy_auraoflight", + "hunter:beast-mastery": "ability_hunter_beasttaming", + "hunter:marksmanship": "ability_marksmanship", + "hunter:survival": "ability_hunter_swiftstrike", + "rogue:assassination": "ability_rogue_eviscerate", + "rogue:combat": "ability_backstab", + "rogue:subtlety": "ability_stealth", + "priest:discipline": "spell_holy_wordfortitude", + "priest:holy": "spell_holy_guardianspirit", + "priest:shadow": "spell_shadow_shadowwordpain", + "death-knight:blood": "spell_deathknight_bloodpresence", + "death-knight:frost": "spell_deathknight_frostpresence", + "death-knight:unholy": "spell_deathknight_unholypresence", + "shaman:elemental": "spell_nature_lightning", + "shaman:enhancement": "spell_nature_lightningshield", + "shaman:restoration": "spell_nature_magicimmunity", + "mage:arcane": "spell_holy_magicalsentry", + "mage:fire": "spell_fire_firebolt02", + "mage:frost": "spell_frost_frostbolt02", + "warlock:affliction": "spell_shadow_deathcoil", + "warlock:demonology": "spell_shadow_metamorphosis", + "warlock:destruction": "spell_shadow_rainoffire", + "druid:balance": "spell_nature_starfall", + "druid:feral-bear": "ability_racial_bearform", + "druid:feral-cat": "ability_racial_bearform", + "druid:restoration": "spell_nature_healingtouch", +}); + export function specializationsForClass(classId: ClassId): readonly SpecializationDefinition[] { return SPECIALIZATION_DEFINITIONS.filter((definition) => definition.classId === classId); } @@ -144,6 +179,17 @@ export function specializationById(id: string | null | undefined): Specializatio return id ? BY_ID.get(id) ?? null : null; } +/** Returns the dedicated WoW talent-tree icon when available, otherwise the class icon. */ +export function specializationIconUrl(classId: ClassId, specializationId: string | null | undefined): string { + const specialization = specializationById(specializationId); + const iconBasename = specialization?.classId === classId + ? CLASSIC_SPECIALIZATION_ICON_BASENAME[specialization.id] + : undefined; + return iconBasename + ? `/assets/ui/talents/icons/${iconBasename}.png` + : classIconUrl(classId); +} + export function normalizeSpecializationId(classId: ClassId, value: unknown): string | null { if (typeof value !== "string") return null; const definition = specializationById(value); diff --git a/src/styles.css b/src/styles.css index e6a2a72f..19859603 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2129,47 +2129,76 @@ html[data-display-layout="thor-preview"], html[data-display-layout="thor-preview .character-preview--compact figcaption small { font-size: 7px; } /* Character select */ -.character-select-screen { display: grid; grid-template-columns: 250px minmax(250px, 1fr) 260px; grid-template-rows: 54px minmax(0, 1fr) 25px; gap: 0; padding-bottom: max(8px, env(safe-area-inset-bottom)); } -.roster-header { grid-column: 1 / 4; display: flex; align-items: center; gap: 17px; border-bottom: 1px solid var(--front-line); } +.character-select-screen { + --roster-gold: #d2b16d; + --roster-ice: #9fd9df; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(280px, 34%); + grid-template-rows: 58px minmax(0, 1fr) 68px; + gap: 12px; + padding-bottom: max(10px, env(safe-area-inset-bottom)); + background: + linear-gradient(90deg, rgba(2,7,12,.42), rgba(2,7,12,.18) 58%, rgba(2,7,12,.75)), + linear-gradient(180deg, rgba(2,7,12,.36), rgba(2,7,12,.86)), + url("./assets/ui/login/frozen-gateway.webp") center 44% / cover no-repeat, + #071019; +} +.character-select-screen::before, .character-create-screen::before { + position: absolute; + inset: 0; + z-index: -1; + background: radial-gradient(circle at 39% 42%, rgba(108,197,207,.1), transparent 28%), linear-gradient(90deg, rgba(1,4,7,.28), transparent 42%, rgba(1,4,7,.48)); + content: ""; + pointer-events: none; +} +.character-select-screen .front-grain, .character-create-screen .front-grain { opacity: .1; mix-blend-mode: screen; } +.roster-header { grid-column: 1 / 3; display: flex; align-items: center; gap: 17px; padding: 0 4px 5px; border-bottom: 1px solid rgba(210,177,109,.34); } .roster-header > div:nth-child(2) { display: flex; flex: 1; flex-direction: column; } -.roster-header > div:nth-child(2) span { color: var(--front-dim); font-size: 8px; text-transform: uppercase; letter-spacing: .13em; } -.roster-header > div:nth-child(2) strong { font: 18px Georgia, serif; } -.roster-header > button, .creator-header > button { padding: 7px 10px; border: 1px solid var(--front-line); border-radius: 7px; background: transparent; color: var(--front-dim); cursor: pointer; font-size: 9px; } -.roster-list { grid-column: 1; grid-row: 2; min-height: 0; padding: 12px 12px 18px 0; overflow: auto; border-right: 1px solid var(--front-line); } -.roster-list__heading { display: flex; justify-content: space-between; margin: 0 5px 8px; color: var(--front-dim); font-size: 8px; text-transform: uppercase; letter-spacing: .12em; } -.roster-character { display: grid; width: 100%; min-height: 54px; grid-template-columns: 38px 1fr auto; align-items: center; gap: 8px; margin-bottom: 6px; padding: 7px; border: 1px solid transparent; border-radius: 9px; background: rgba(255,255,255,.025); color: var(--front-cream); cursor: pointer; text-align: left; } -.roster-character.is-active { border-color: rgba(204, 223, 170, 0.26); background: linear-gradient(100deg, rgba(107, 146, 82, .19), rgba(255,255,255,.025)); } -.roster-character > span { display: grid; width: 36px; height: 36px; place-items: center; border: 1px solid var(--portrait-accent); border-radius: 50%; background: color-mix(in srgb, var(--portrait-accent), transparent 82%); color: #e7eddb; font-size: 9px; font-weight: 800; } +.roster-header > div:nth-child(2) span { color: #b8cbc9; font-size: 8px; text-transform: uppercase; letter-spacing: .16em; } +.roster-header > div:nth-child(2) strong { color: #f4eddd; font: 21px Georgia, serif; } +.roster-header > button, .creator-header > button { padding: 8px 12px; border: 1px solid rgba(210,177,109,.28); border-radius: 4px; background: rgba(8,17,21,.72); color: #d8d2c3; cursor: pointer; font-size: 9px; } +.roster-list { grid-column: 2; grid-row: 2; display: grid; min-height: 0; grid-template-rows: auto minmax(0, 1fr) auto; padding: 14px; border: 1px solid rgba(210,177,109,.35); border-radius: 4px 4px 10px 10px; overflow: hidden; background: linear-gradient(155deg, rgba(25,35,38,.94), rgba(7,14,18,.96)); box-shadow: 0 18px 40px rgba(0,0,0,.38), inset 0 1px 0 rgba(255,255,255,.04); } +.roster-list__heading { display: flex; justify-content: space-between; margin: 0 2px 11px; padding-bottom: 8px; border-bottom: 1px solid rgba(210,177,109,.2); color: #b9c8c5; font-size: 8px; text-transform: uppercase; letter-spacing: .15em; } +.roster-list__heading b { color: var(--roster-gold); } +.roster-list__entries { min-height: 0; padding-right: 3px; overflow-y: auto; scrollbar-width: thin; } +.roster-character { display: grid; width: 100%; min-height: 61px; grid-template-columns: 42px 1fr auto; align-items: center; gap: 9px; margin-bottom: 7px; padding: 8px; border: 1px solid rgba(194,213,190,.08); border-radius: 4px; background: linear-gradient(100deg, rgba(255,255,255,.035), rgba(255,255,255,.012)); color: var(--front-cream); cursor: pointer; text-align: left; transition: border-color 150ms ease, background 150ms ease, transform 150ms ease; } +.roster-character:hover { border-color: rgba(159,217,223,.3); background: rgba(95,143,148,.1); transform: translateX(-2px); } +.roster-character.is-active { border-color: rgba(210,177,109,.68); background: linear-gradient(100deg, rgba(141,105,38,.32), rgba(74,116,122,.12)); box-shadow: inset 3px 0 0 var(--roster-gold), 0 0 18px rgba(111,190,198,.08); } +.roster-character > span { display: grid; width: 40px; height: 40px; place-items: center; overflow: hidden; border: 1px solid var(--portrait-accent); border-radius: 6px; background: color-mix(in srgb, var(--portrait-accent), transparent 78%); box-shadow: inset 0 0 14px color-mix(in srgb, var(--portrait-accent), transparent 78%), 0 2px 8px rgba(0,0,0,.48); } +.roster-character__icon img { width: 100%; height: 100%; object-fit: cover; } .roster-character i { display: flex; min-width: 0; flex-direction: column; font-style: normal; } -.roster-character i strong { overflow: hidden; font: 14px Georgia, serif; text-overflow: ellipsis; white-space: nowrap; } -.roster-character i small, .roster-character em { color: var(--front-dim); font-size: 7px; font-style: normal; } -.roster-new { display: flex; width: 100%; min-height: 51px; align-items: center; gap: 9px; padding: 7px 10px; border: 1px dashed rgba(200, 222, 173, .23); border-radius: 9px; background: transparent; color: var(--front-cream); cursor: pointer; text-align: left; } -.roster-new b { color: var(--front-green-bright); font: 24px Georgia, serif; } +.roster-character i strong { overflow: hidden; font: 16px Georgia, serif; text-overflow: ellipsis; white-space: nowrap; } +.roster-character i small, .roster-character em { color: #a9b8b6; font-size: 7px; font-style: normal; } +.roster-new { display: flex; width: 100%; min-height: 54px; align-items: center; gap: 10px; margin-top: 9px; padding: 8px 11px; border: 1px dashed rgba(210,177,109,.4); border-radius: 4px; background: rgba(119,91,38,.08); color: var(--front-cream); cursor: pointer; text-align: left; } +.roster-new b { color: var(--roster-gold); font: 26px Georgia, serif; } .roster-new span { display: flex; flex-direction: column; } -.roster-new span strong { font-size: 10px; } -.roster-new span small { color: var(--front-dim); font-size: 7px; } +.roster-new span strong { font-size: 11px; } +.roster-new span small { color: #a9b8b6; font-size: 7px; } .roster-empty { display: grid; min-height: 155px; place-content: center; justify-items: center; color: var(--front-dim); text-align: center; } -.roster-empty span { color: var(--front-green-bright); font: 32px Georgia, serif; } +.roster-empty span { color: var(--roster-gold); font: 32px Georgia, serif; } .roster-empty strong { color: var(--front-cream); font: 15px Georgia, serif; } .roster-empty small { max-width: 160px; margin: 4px 0 12px; font-size: 8px; line-height: 1.4; } -.roster-stage { position: relative; grid-column: 2; grid-row: 2; min-width: 0; min-height: 0; } -.roster-stage-continue { position: absolute; right: 12px; bottom: 14px; left: 12px; z-index: 4; display: none; justify-content: center; } -.roster-character-info { position: absolute; right: 0; bottom: 12px; left: 0; z-index: 2; display: none; text-align: center; } -.roster-character-info p { margin: 0; color: var(--front-dim); font-size: 8px; text-transform: uppercase; } -.roster-character-info h1 { margin: 2px 0; font: 26px Georgia, serif; } +.roster-stage { position: relative; grid-column: 1; grid-row: 2; min-width: 0; min-height: 0; overflow: hidden; border: 1px solid rgba(159,217,223,.2); border-radius: 10px 4px 4px 10px; background: radial-gradient(circle at 54% 40%, rgba(100,190,200,.18), transparent 34%), linear-gradient(145deg, rgba(4,12,17,.5), rgba(3,8,12,.83)); box-shadow: inset 0 0 70px rgba(28,65,72,.14), 0 18px 45px rgba(0,0,0,.3); } +.roster-stage::after { position: absolute; inset: 8px; z-index: 0; border: 1px solid rgba(210,177,109,.12); border-radius: 7px 2px 2px 7px; content: ""; pointer-events: none; } +.roster-stage .character-preview { z-index: 1; } +.roster-stage .character-preview figcaption { display: none; } +.roster-stage .character-preview__viewport { bottom: 8px; } +.roster-character-info { position: absolute; top: 18px; bottom: 18px; left: 18px; z-index: 3; display: flex; width: min(225px, 43%); flex-direction: column; justify-content: flex-end; padding: 18px; border-left: 2px solid rgba(210,177,109,.62); background: linear-gradient(90deg, rgba(3,9,13,.92), rgba(3,9,13,.62) 72%, transparent); text-align: left; pointer-events: none; } +.roster-character-info p { margin: 0; color: var(--roster-gold); font-size: 8px; letter-spacing: .14em; text-transform: uppercase; } +.roster-character-info h1 { margin: 4px 0 2px; color: #f4eddd; font: 34px Georgia, serif; line-height: .95; } .roster-character-info strong, .roster-character-info small { display: block; } -.roster-character-info strong { font-size: 10px; } -.roster-character-info small { max-width: 250px; margin: 4px auto 0; color: var(--front-dim); font-size: 8px; } -.roster-details { grid-column: 3; grid-row: 2; align-self: stretch; padding: 22px 4px 20px 16px; border-left: 1px solid var(--front-line); } -.roster-details h2 { margin: 7px 0 17px; font: 22px Georgia, serif; line-height: 1.05; } -.roster-details > p:not(.front-kicker):not(.front-notice) { color: var(--front-dim); font-size: 10px; line-height: 1.5; } -.roster-details dl, .companion-panel dl { margin: 0 0 14px; } -.roster-details dl div, .companion-panel dl div { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; border-bottom: 1px solid rgba(203, 219, 180, .09); } -.roster-details dt, .companion-panel dt { color: var(--front-dim); font-size: 8px; text-transform: uppercase; } -.roster-details dd, .companion-panel dd { margin: 0; font-size: 9px; text-align: right; } -.roster-enter { width: 100%; margin-top: 13px; } -.roster-details .text-action { display: block; margin: 10px auto 0; } -.character-select-screen > .controller-legend { grid-column: 1 / 4; grid-row: 3; position: static; align-self: end; justify-self: end; } +.roster-character-info strong { font-size: 11px; } +.roster-character-info > small { margin-top: 7px; color: #b8c5c3; font-size: 8px; line-height: 1.45; } +.roster-character-info dl { margin: 15px 0 0; } +.roster-character-info dl div { display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 8px; padding: 5px 0; border-top: 1px solid rgba(210,177,109,.13); } +.roster-character-info dt { color: #91a5a4; font-size: 7px; letter-spacing: .08em; text-transform: uppercase; } +.roster-character-info dd { overflow: hidden; margin: 0; font-size: 8px; text-align: right; text-overflow: ellipsis; white-space: nowrap; } +.roster-footer { grid-column: 1 / 3; grid-row: 3; display: flex; align-items: center; gap: 13px; padding: 10px 0 0; border-top: 1px solid rgba(210,177,109,.28); } +.roster-footer__status { min-width: 0; flex: 1; color: #aebdbb; font-size: 8px; letter-spacing: .06em; text-transform: uppercase; } +.roster-footer__status .front-notice { margin: 3px 0 0; } +.roster-footer .text-action { flex: 0 0 auto; } +.roster-enter { width: min(310px, 38%); min-height: 48px; border-radius: 4px; } +.character-select-screen > .controller-legend { display: none; } .front-modal-backdrop { position: absolute; inset: 0; z-index: 50; display: grid; place-items: center; padding: 20px; background: rgba(1,5,3,.76); backdrop-filter: blur(8px); } .front-confirm { width: min(390px, 100%); padding: 25px; border: 1px solid var(--front-line); border-radius: 14px; background: #0a1812; box-shadow: 0 30px 80px #000; } .front-confirm h2 { margin: 7px 0; font: 27px Georgia, serif; } @@ -2530,6 +2559,71 @@ html[data-display-layout="thor-preview"], html[data-display-layout="thor-preview .customization-row input[type="range"] { width: 100%; accent-color: var(--front-green); } .appearance-actions { display: grid; max-width: 610px; grid-template-columns: 1fr 95px 180px; align-items: center; gap: 8px; margin-top: 10px; } +/* Character shell polish */ +.companion-panel dl { margin: 0 0 14px; } +.companion-panel dl div { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; border-bottom: 1px solid rgba(203,219,180,.09); } +.companion-panel dt { color: var(--front-dim); font-size: 8px; text-transform: uppercase; } +.companion-panel dd { margin: 0; font-size: 9px; text-align: right; } +.character-create-screen { + --creator-gold: #d2b16d; + --creator-ice: #9fd9df; + grid-template-columns: 265px minmax(245px, 1fr) 300px; + grid-template-rows: 58px 3px minmax(0, 1fr) 66px; + background: + linear-gradient(90deg, rgba(2,7,12,.67), rgba(2,7,12,.16) 47%, rgba(2,7,12,.72)), + linear-gradient(180deg, rgba(2,7,12,.42), rgba(2,7,12,.88)), + url("./assets/ui/login/frozen-gateway.webp") center 44% / cover no-repeat, + #071019; +} +.creator-header { padding: 0 4px 5px; border-bottom-color: rgba(210,177,109,.34); } +.creator-header > div:nth-child(2) span { color: #b8cbc9; letter-spacing: .16em; } +.creator-header > div:nth-child(2) strong { color: #f4eddd; font-size: 20px; } +.creator-progress { gap: 5px; padding-top: 1px; } +.creator-progress i { background: rgba(255,255,255,.08); } +.creator-progress i.is-complete { background: linear-gradient(90deg, #8d713b, var(--creator-gold), #72b6bd); box-shadow: 0 0 10px rgba(123,202,210,.3); } +.creator-category-tabs button { min-height: 34px; border-color: rgba(210,177,109,.22); border-radius: 3px; background: rgba(7,15,19,.8); } +.creator-category-tabs button.is-active { border-color: rgba(210,177,109,.72); color: #f1d99a; background: linear-gradient(180deg, rgba(144,105,35,.32), rgba(79,62,31,.2)); box-shadow: inset 0 -2px 0 var(--creator-gold); } +.creator-races, .creator-classes { + margin-top: 12px; + margin-bottom: 10px; + padding: 13px; + border: 1px solid rgba(210,177,109,.28); + border-radius: 4px; + background: linear-gradient(155deg, rgba(24,34,37,.92), rgba(6,13,17,.95)); + box-shadow: 0 16px 36px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.035); +} +.creator-races { margin-right: 10px; overflow-y: auto; } +.creator-classes { margin-left: 10px; } +.creator-races > header, .creator-classes > header { padding-bottom: 8px; border-bottom: 1px solid rgba(210,177,109,.16); } +.creator-races > header > span, .creator-classes > header > span { color: var(--creator-gold); } +.race-option, .class-option { border-radius: 3px; border-color: rgba(194,213,190,.06); background: rgba(255,255,255,.025); } +.race-option:hover, .class-option:hover { border-color: rgba(159,217,223,.28); background: rgba(80,132,138,.1); } +.race-option.is-active, .class-option.is-active { border-color: rgba(210,177,109,.62); background: linear-gradient(100deg, rgba(141,105,38,.28), rgba(73,119,124,.1)); box-shadow: inset 2px 0 0 var(--creator-gold); } +.gender-picker button { border-radius: 3px; border-color: rgba(210,177,109,.2); background: rgba(5,12,16,.56); } +.gender-picker button.is-active { border-color: rgba(159,217,223,.4); background: rgba(77,132,138,.2); color: #d7eef0; } +.creator-preview-stage { margin: 12px 0 10px; overflow: hidden; border: 1px solid rgba(159,217,223,.2); border-radius: 4px; background: radial-gradient(circle at 50% 40%, rgba(97,184,193,.18), transparent 36%), rgba(3,9,13,.48); box-shadow: inset 0 0 55px rgba(29,75,81,.13); } +.creator-preview-stage::after { position: absolute; inset: 8px; z-index: 0; border: 1px solid rgba(210,177,109,.12); border-radius: 2px; content: ""; pointer-events: none; } +.creator-preview-title { position: absolute; top: 14px; left: 15px; z-index: 4; display: flex; flex-direction: column; padding-left: 8px; border-left: 2px solid var(--creator-gold); pointer-events: none; } +.creator-preview-title small { color: var(--creator-gold); font-size: 7px; letter-spacing: .15em; text-transform: uppercase; } +.creator-preview-title strong { color: #f4eddd; font: 14px Georgia, serif; } +.creator-preview-stage .character-preview figcaption { bottom: 36px; } +.preview-rotate { bottom: 7px; } +.preview-rotate button { border-radius: 3px; border-color: rgba(210,177,109,.3); background: rgba(5,13,17,.86); } +.class-summary { padding: 8px; border-left: 2px solid rgba(159,217,223,.5); background: rgba(5,13,17,.52); } +.creator-footer { padding-top: 9px; border-top-color: rgba(210,177,109,.28); } +.creator-footer__selection { display: flex; min-width: 0; flex: 1; flex-direction: column; } +.creator-footer__selection small { color: #93a8a7; font-size: 7px; letter-spacing: .12em; text-transform: uppercase; } +.creator-footer__selection strong { overflow: hidden; color: #e8dec7; font: 14px Georgia, serif; text-overflow: ellipsis; text-transform: capitalize; white-space: nowrap; } +.creator-footer .front-button { min-height: 48px; border-radius: 4px; } +.appearance-preview-stage { margin: 12px 10px 10px 0; border: 1px solid rgba(159,217,223,.2); border-radius: 4px; background: radial-gradient(circle at 50% 40%, rgba(97,184,193,.18), transparent 36%), rgba(3,9,13,.54); } +.appearance-preview-stage .character-preview figcaption { bottom: 34px; } +.appearance-preview-stage > p { display: none; } +.appearance-controls { margin: 12px 0 10px 10px; padding: 18px; border: 1px solid rgba(210,177,109,.28); border-radius: 4px; background: linear-gradient(155deg, rgba(24,34,37,.94), rgba(6,13,17,.96)); box-shadow: 0 16px 36px rgba(0,0,0,.3); } +.appearance-controls header h1 { color: #f4eddd; font-size: 30px; } +.customization-row { border-radius: 3px; border-color: rgba(210,177,109,.12); background: rgba(255,255,255,.025); } +.appearance-actions { position: sticky; bottom: -8px; z-index: 4; padding: 9px 0 0; background: linear-gradient(180deg, transparent, rgba(7,14,18,.98) 25%); } +.appearance-actions .front-button { min-height: 48px; border-radius: 4px; } + /* Companion / lower Thor screen */ .companion-panel { display: grid; grid-template-rows: 45px minmax(0, 1fr) 45px; padding: 12px 14px; } .companion-panel > header { display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--front-line); } @@ -2612,7 +2706,8 @@ html[data-display-layout="thor-preview"], html[data-display-layout="thor-preview .companion-mini-roster button { display: grid; width: 100%; min-height: 39px; grid-template-columns: 29px 1fr auto; align-items: center; gap: 7px; margin-bottom: 4px; padding: 5px; border: 1px solid transparent; border-radius: 7px; background: rgba(255,255,255,.025); color: var(--front-cream); cursor: pointer; text-align: left; } .companion-mini-roster button.is-active { border-color: var(--front-line); background: rgba(99,142,79,.17); } .companion-mini-roster button:disabled { cursor: not-allowed; opacity: .42; } -.companion-mini-roster button > span { display: grid; width: 27px; height: 27px; place-items: center; border: 1px solid var(--front-line); border-radius: 50%; font-size: 7px; } +.companion-mini-roster button > span { display: grid; width: 27px; height: 27px; place-items: center; overflow: hidden; border: 1px solid var(--front-line); border-radius: 5px; background: rgba(0,0,0,.35); } +.companion-character-icon img { width: 100%; height: 100%; object-fit: cover; } .companion-mini-roster button strong { font-size: 9px; } .companion-mini-roster button small { color: var(--front-dim); font-size: 7px; } .companion-mini-roster > p { color: var(--front-dim); font-size: 9px; } @@ -2722,12 +2817,10 @@ html[data-display-surface="bottom"] .single-context-toggle { display: none; } } @media (max-width: 900px) { - .character-select-screen { grid-template-columns: 235px 1fr; } + .character-select-screen { grid-template-columns: minmax(0, 1fr) 300px; } .roster-header { grid-column: 1 / 3; } - .roster-details { display: none; } - .roster-stage-continue { display: flex; } .character-select-screen > .controller-legend { grid-column: 1 / 3; } - .character-create-screen { grid-template-columns: 245px 1fr 270px; } + .character-create-screen { grid-template-columns: 235px minmax(220px, 1fr) 270px; } .main-menu-screen { grid-template-columns: minmax(225px, 31%) minmax(0, 1fr); column-gap: 16px; } .main-menu-mode { min-height: 72px; } .dungeon-select-screen { grid-template-columns: 255px minmax(0, 1fr); column-gap: 14px; } @@ -2748,15 +2841,20 @@ html[data-display-surface="bottom"] .single-context-toggle { display: none; } .login-form__actions .front-button small { font-size: 7px; } .login-form footer, .login-lore { display: none; } .controller-legend { display: none; } - .character-select-screen { display: grid; grid-template-columns: 43% 57%; grid-template-rows: 48px minmax(0,1fr); } + .character-select-screen { display: grid; grid-template-columns: 43% 57%; grid-template-rows: 48px minmax(0,1fr) 62px; gap: 7px; } .roster-header { grid-column: 1 / 3; } .roster-header > div:nth-child(2) strong { font-size: 14px; } .roster-header > button { display: none; } - .roster-list { padding-right: 7px; } + .roster-list { padding: 9px 7px 9px 9px; } .roster-character { grid-template-columns: 31px 1fr; } .roster-character > span { width: 29px; height: 29px; } .roster-character > em { display: none; } - .roster-stage .character-preview figcaption { bottom: 62px; } + .roster-character-info { top: auto; right: 8px; bottom: 8px; left: 8px; width: auto; padding: 10px; } + .roster-character-info > small, .roster-character-info dl { display: none; } + .roster-character-info h1 { font-size: 24px; } + .roster-footer { grid-column: 1 / 3; padding-top: 6px; } + .roster-footer__status { display: none; } + .roster-enter { width: min(280px, 62%); margin-left: auto; } .character-avatar { transform: perspective(600px) rotateY(var(--avatar-turn)) scale(.78); } .main-menu-screen { grid-template-columns: 1fr; grid-template-rows: 48px 86px minmax(0, 1fr); gap: 0; } .main-menu-header { grid-column: 1; grid-row: 1; } diff --git a/src/ui/CharacterCreateScreen.tsx b/src/ui/CharacterCreateScreen.tsx index 5126f938..41cc33a9 100644 --- a/src/ui/CharacterCreateScreen.tsx +++ b/src/ui/CharacterCreateScreen.tsx @@ -153,6 +153,7 @@ export function CharacterCreateScreen() {

{race.description}

+
Live preview{race.name} {characterClass.name}
updateDraft({ rotation: draft.rotation - 12 })} aria-label="Rotate character left">↺ @@ -205,6 +206,7 @@ export function CharacterCreateScreen() { )}
+
Selected path{race.name} · {characterClass.name} · {draft.gender}
Back to roster setStep("appearance")}>Customize & nameNext step
diff --git a/src/ui/CharacterSelectScreen.tsx b/src/ui/CharacterSelectScreen.tsx index 9636fdbc..e7789385 100644 --- a/src/ui/CharacterSelectScreen.tsx +++ b/src/ui/CharacterSelectScreen.tsx @@ -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() {
{session?.kind === "offline" ? "Offline expedition" : `Local account / ${session?.displayName}`}Choose your adventurer
- + Sign out
+
+ + {selected && race && characterClass && ( +
+

{FACTIONS[race.faction].name} / {selected.gender}

+

{selected.name}

+ {race.name} {characterClass.name}{selected.secondaryClassId ? ` / ${classById(selected.secondaryClassId).name}` : ""} + {characterClass.description} +
+
Level
{selected.level}
+
Last camp
{selected.location}
+
Role
{characterClass.role}
+
+
+ )} +
-
- - {selected && race && characterClass && ( - <> -
-

{FACTIONS[race.faction].name} / {selected.gender}

-

{selected.name}

- {race.name} {characterClass.name}{selected.secondaryClassId ? ` / ${classById(selected.secondaryClassId).name}` : ""} - {characterClass.description} -
- Continue as {selected.name} - - )} -
- +
+
+ {selected && race && characterClass + ? `${contentCategoryById(selected.categoryId ?? contentCategoryForClass(selected.classId)).name} · ${FACTIONS[race.faction].name}` + : "Select an adventurer or forge a new one"} + {notice &&

{notice}

} +
+ {selected && setConfirmDelete(true)}>Delete} + + {selected ? `Continue as ${selected.name}` : "Choose a character"}{selected ? "Enter the adventure menu" : "Select from your roster"} + +
{confirmDelete && selected && (
diff --git a/src/ui/CompanionPanel.tsx b/src/ui/CompanionPanel.tsx index ed673579..32b1674a 100644 --- a/src/ui/CompanionPanel.tsx +++ b/src/ui/CompanionPanel.tsx @@ -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 }
{snapshot.characters.map((character) => ( ))} {!snapshot.characters.length &&

No characters saved yet.

} diff --git a/src/ui/thorUi.css b/src/ui/thorUi.css index 0df7d5fc..2d0122d6 100644 --- a/src/ui/thorUi.css +++ b/src/ui/thorUi.css @@ -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; }