Skip to content

Commit ab19d31

Browse files
SociableSteveclaude
andcommitted
Character sheet UI: full skills, spells, and inventory
The sheet now shows all 18 skills with computed modifiers (+ expertise), the spellcasting block (save DC/attack, slots remaining, known spells by level with effects), and inventory with equipped markers + gold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8aefe03 commit ab19d31

3 files changed

Lines changed: 103 additions & 11 deletions

File tree

packages/web/src/screens/CharacterViewer.tsx

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useState } from "react";
22
import { useNavigate, useParams } from "react-router-dom";
33
import { api } from "../api.js";
4-
import { ABILITIES, ABILITY_NAMES, type CharacterDTO } from "../types.js";
4+
import { ABILITIES, ABILITY_NAMES, type CharacterDTO, SKILL_ABILITY, SKILLS } from "../types.js";
55
import { Shell } from "./Shell.js";
66

77
const mod = (score: number) => Math.floor((score - 10) / 2);
@@ -125,16 +125,21 @@ export function CharacterViewer() {
125125
</ul>
126126
</div>
127127
<div>
128-
<h3>Skill Proficiencies</h3>
129-
{c.skillProficiencies.length === 0 ? (
130-
<p className="muted">None</p>
131-
) : (
132-
<ul className="taglist">
133-
{c.skillProficiencies.map((s) => (
134-
<li key={s}>{titleize(s)}</li>
135-
))}
136-
</ul>
137-
)}
128+
<h3>Skills</h3>
129+
<ul className="proflist skills-list">
130+
{SKILLS.map((sk) => {
131+
const prof = c.skillProficiencies.includes(sk);
132+
const exp = (c.expertise ?? []).includes(sk);
133+
const total = mod(c.abilities[SKILL_ABILITY[sk]!]!) + (prof ? c.proficiencyBonus * (exp ? 2 : 1) : 0);
134+
return (
135+
<li key={sk} className={prof ? "prof" : ""}>
136+
<span className="dot" /> {titleize(sk)}
137+
{exp && <span className="exp-tag">expertise</span>}
138+
<span className="val">{fmt(total)}</span>
139+
</li>
140+
);
141+
})}
142+
</ul>
138143
<h3 style={{ marginTop: 18 }}>Attack</h3>
139144
<p className="attack-line">
140145
{fmt(atk.attackBonus)} to hit · {atk.damageDice}
@@ -143,6 +148,41 @@ export function CharacterViewer() {
143148
</div>
144149
</div>
145150

151+
{c.spellcasting && (
152+
<div className="sheet-section">
153+
<h3>
154+
Spells <span className="muted">· save DC {c.spellcasting.saveDc} · attack {fmt(c.spellcasting.attackBonus)}</span>
155+
</h3>
156+
<div className="slots">
157+
{Object.entries(c.spellcasting.slots).map(([lvl, st]) => (
158+
<span key={lvl} className="slot-pip">L{lvl}: {st.max - st.used}/{st.max}</span>
159+
))}
160+
</div>
161+
<ul className="spell-list">
162+
{[...c.spellcasting.known].sort((a, b) => a.level - b.level).map((sp) => (
163+
<li key={sp.name}>
164+
<span className="spell-lvl">{sp.level === 0 ? "C" : sp.level}</span>
165+
<span className="spell-name">{sp.name}</span>
166+
<span className="spell-desc">{sp.desc}</span>
167+
</li>
168+
))}
169+
</ul>
170+
</div>
171+
)}
172+
173+
{(c.inventory?.length || c.gold != null) && (
174+
<div className="sheet-section">
175+
<h3>Inventory <span className="muted">· {c.gold ?? 0} gp</span></h3>
176+
<ul className="taglist">
177+
{(c.inventory ?? []).map((it) => (
178+
<li key={it.id} className={it.equipped ? "equipped" : ""}>
179+
{it.name}{it.qty > 1 ? ` ×${it.qty}` : ""}{it.equipped ? " ⚔" : ""}
180+
</li>
181+
))}
182+
</ul>
183+
</div>
184+
)}
185+
146186
{character.backstory && (
147187
<div className="sheet-backstory">
148188
<h3>Back-story</h3>

packages/web/src/styles.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,19 @@ textarea { resize: vertical; }
305305
/* ── Suggestions ─────────────────────────────────────────── */
306306
.suggestions { display: flex; flex-direction: column; gap: 4px; }
307307
.suggestions-label { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); }
308+
309+
/* ── Expanded sheet: skills, spells, inventory ───────────── */
310+
.skills-list { columns: 2; column-gap: 24px; }
311+
.skills-list li { break-inside: avoid; }
312+
.exp-tag { font-size: 9px; text-transform: uppercase; letter-spacing: .5px; color: var(--gold); border: 1px solid var(--gold-dim); border-radius: 4px; padding: 0 4px; margin-left: 6px; }
313+
.sheet-section { margin-top: 22px; border-top: 1px solid var(--border); padding-top: 16px; }
314+
.sheet-section h3 { color: var(--gold); margin: 0 0 10px; }
315+
.slots { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
316+
.slot-pip { font-size: 12px; background: var(--panel-2); border: 1px solid var(--gold-dim); border-radius: 6px; padding: 3px 8px; color: var(--gold); }
317+
.spell-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 6px; }
318+
.spell-list li { display: grid; grid-template-columns: 22px 160px 1fr; gap: 10px; align-items: baseline; font-size: 14px; }
319+
.spell-lvl { text-align: center; background: var(--panel-2); border: 1px solid var(--border); border-radius: 5px; color: var(--gold); font-size: 12px; }
320+
.spell-name { font-weight: 600; }
321+
.spell-desc { color: var(--muted); font-size: 13px; }
322+
.taglist li.equipped { border-color: var(--gold-dim); color: var(--ink); }
323+
@media (max-width: 720px) { .skills-list { columns: 1; } .spell-list li { grid-template-columns: 20px 1fr; } .spell-desc { display: none; } }

packages/web/src/types.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,38 @@ export interface SrdCatalog {
4848
maxLevel: number;
4949
}
5050

51+
export const SKILL_ABILITY: Record<string, Ability> = {
52+
athletics: "str",
53+
acrobatics: "dex", sleightOfHand: "dex", stealth: "dex",
54+
arcana: "int", history: "int", investigation: "int", nature: "int", religion: "int",
55+
animalHandling: "wis", insight: "wis", medicine: "wis", perception: "wis", survival: "wis",
56+
deception: "cha", intimidation: "cha", performance: "cha", persuasion: "cha",
57+
};
58+
export const SKILLS: string[] = Object.keys(SKILL_ABILITY);
59+
60+
export interface SpellRef {
61+
name: string;
62+
level: number;
63+
school: string;
64+
desc: string;
65+
}
66+
export interface Spellcasting {
67+
ability: Ability;
68+
saveDc: number;
69+
attackBonus: number;
70+
slots: Record<number, { max: number; used: number }>;
71+
known: SpellRef[];
72+
}
73+
export interface Item {
74+
id: string;
75+
name: string;
76+
qty: number;
77+
type: string;
78+
equipped?: boolean;
79+
attuned?: boolean;
80+
description?: string;
81+
}
82+
5183
export interface CreatureSheet {
5284
id: string;
5385
name: string;
@@ -59,6 +91,10 @@ export interface CreatureSheet {
5991
speed: number;
6092
saveProficiencies: Ability[];
6193
skillProficiencies: string[];
94+
expertise: string[];
95+
spellcasting?: Spellcasting;
96+
inventory?: Item[];
97+
gold?: number;
6298
}
6399
export interface BuildSuggestion {
64100
raceId: string;

0 commit comments

Comments
 (0)