Skip to content

Commit b2fc715

Browse files
Dunky13claude
andcommitted
feat(initiative-tracker): DM HP control, status effects, current turn
- Hide monster HP from players: the DM now redacts monster hp from every snapshot sent over the wire, so joined clients never receive it (the DM still sees full health bars). - DM combat controls per combatant (gear panel): adjust HP with an amount field plus Damage/Heal (clamped 0..max), and toggle the 15 standard 5e conditions as chips or add a custom one. - Current turn is shown everywhere: the DM turn bar names the active combatant, and players get a banner ("X's turn" / "Your turn!" / not started). Co-Authored-By: Claude Opus 4.8 <[email protected]>
1 parent 2ce5fa7 commit b2fc715

6 files changed

Lines changed: 304 additions & 66 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/** The 15 standard D&D 5e conditions, for the DM's quick-toggle status picker. */
2+
export const CONDITIONS = [
3+
'Blinded',
4+
'Charmed',
5+
'Deafened',
6+
'Exhaustion',
7+
'Frightened',
8+
'Grappled',
9+
'Incapacitated',
10+
'Invisible',
11+
'Paralyzed',
12+
'Petrified',
13+
'Poisoned',
14+
'Prone',
15+
'Restrained',
16+
'Stunned',
17+
'Unconscious',
18+
] as const;

initiative-tracker/src/store/dmStore.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ import { createRoom, type Room } from '../net/room';
99

1010
const makeCode = customAlphabet('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', 4);
1111

12+
/**
13+
* Player-facing view of the encounter. Monster HP is the DM's secret, so it is
14+
* stripped before any snapshot leaves the DM — players never receive it.
15+
*/
16+
function redactForPlayers(state: EncounterState): EncounterState {
17+
return {
18+
...state,
19+
combatants: state.combatants.map((c) =>
20+
c.kind === 'monster' && c.hp ? { ...c, hp: null } : c,
21+
),
22+
};
23+
}
24+
1225
interface DmStore {
1326
roomCode: string;
1427
encounter: EncounterState;
@@ -38,26 +51,26 @@ export const useDmStore = create<DmStore>()(
3851
) {
3952
const next = applyAction(get().encounter, action);
4053
set({ encounter: next });
41-
room.broadcast(next);
54+
room.broadcast(redactForPlayers(next));
4255
}
4356
});
44-
room.onPeerJoin(() => room.broadcast(get().encounter));
57+
room.onPeerJoin(() => room.broadcast(redactForPlayers(get().encounter)));
4558
set({ room });
4659
},
4760
dispatch: (action) => {
4861
const next = applyAction(get().encounter, action);
4962
set({ encounter: next });
50-
get().room?.broadcast(next);
63+
get().room?.broadcast(redactForPlayers(next));
5164
},
5265
next: () => {
5366
const next = advanceTurn(get().encounter);
5467
set({ encounter: next });
55-
get().room?.broadcast(next);
68+
get().room?.broadcast(redactForPlayers(next));
5669
},
5770
prev: () => {
5871
const next = prevTurn(get().encounter);
5972
set({ encounter: next });
60-
get().room?.broadcast(next);
73+
get().room?.broadcast(redactForPlayers(next));
6174
},
6275
}),
6376
{

initiative-tracker/src/styles/app.css

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,37 @@ ol, ul { list-style: none; padding: 0; }
347347
color: oklch(0.85 0.08 24);
348348
}
349349

350+
/* ── DM manage panel (HP adjust + status effects) ──────────────────────────*/
351+
.manage {
352+
grid-column: 1 / -1;
353+
margin-top: var(--sp-2);
354+
padding-top: var(--sp-3);
355+
border-top: 1px solid var(--ink-700);
356+
display: flex; flex-direction: column; gap: var(--sp-3);
357+
animation: rise 0.2s ease both;
358+
}
359+
.manage__hp, .manage__status { display: flex; flex-direction: column; gap: var(--sp-2); }
360+
.hpadjust { display: flex; gap: var(--sp-2); }
361+
.hpadjust .input { flex: 0 0 4.5rem; }
362+
.hpadjust .btn { flex: 1; padding-inline: var(--sp-2); }
363+
364+
.cond-grid { display: flex; flex-wrap: wrap; gap: 0.35rem; }
365+
.cond-chip {
366+
font-size: 0.72rem; letter-spacing: 0.02em;
367+
padding: 0.3em 0.7em; min-height: 2.1rem;
368+
border-radius: 999px;
369+
background: var(--ink-900);
370+
border: 1px solid var(--ink-700);
371+
color: var(--parchment-dim);
372+
transition: background 0.15s, color 0.15s, border-color 0.15s;
373+
}
374+
.cond-chip:hover { color: var(--parchment); border-color: var(--ink-600); }
375+
.cond-chip.is-on {
376+
background: color-mix(in oklch, var(--danger) 22%, var(--ink-900));
377+
border-color: color-mix(in oklch, var(--danger) 48%, var(--ink-700));
378+
color: oklch(0.87 0.09 24);
379+
}
380+
350381
/* ── HP stepper (player) ───────────────────────────────────────────────────*/
351382
.stepper { display: flex; align-items: stretch; gap: var(--sp-2); }
352383
.stepper .input { font-size: var(--step-1); font-weight: 600; }
@@ -391,20 +422,54 @@ ol, ul { list-style: none; padding: 0; }
391422
/* ── Turn bar (DM, sticky bottom, thumb reach) ─────────────────────────────*/
392423
.turnbar {
393424
position: sticky; bottom: 0; z-index: 20;
394-
display: grid; grid-template-columns: auto 1fr 1fr; align-items: center; gap: var(--sp-2);
425+
display: flex; flex-direction: column; gap: var(--sp-2);
395426
padding: var(--sp-3);
396427
padding-bottom: calc(var(--sp-3) + env(safe-area-inset-bottom, 0));
397428
margin: 0 calc(var(--sp-3) * -1) calc(var(--sp-3) * -1);
398429
background: color-mix(in oklch, var(--ink-900) 90%, transparent);
399430
backdrop-filter: blur(10px);
400431
border-top: 1px solid var(--ink-700);
401432
}
433+
.turnbar__now {
434+
display: flex; align-items: baseline; gap: var(--sp-2);
435+
padding: 0.1rem var(--sp-2);
436+
}
437+
.turnbar__now strong {
438+
font-family: var(--font-display); font-weight: 600; font-size: var(--step-1);
439+
color: var(--ember-bright); letter-spacing: 0.01em;
440+
}
441+
.turnbar__cue { font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.16em; color: var(--muted); }
442+
.turnbar__idle { font-size: var(--step--1); color: var(--muted); font-style: italic; }
443+
.turnbar__controls {
444+
display: grid; grid-template-columns: auto 1fr 1fr; align-items: center; gap: var(--sp-2);
445+
}
402446
.turnbar__round {
403447
display: flex; flex-direction: column; line-height: 1; padding-inline: var(--sp-2);
404448
}
405449
.turnbar__round span { font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.16em; color: var(--muted); }
406450
.turnbar__round strong { font-family: var(--font-display); font-size: var(--step-2); color: var(--gold); }
407451

452+
/* ── Current-turn banner (player) ──────────────────────────────────────────*/
453+
.turn-now {
454+
text-align: center;
455+
font-family: var(--font-display);
456+
font-weight: 600;
457+
font-size: var(--step-1);
458+
letter-spacing: 0.01em;
459+
padding: var(--sp-3);
460+
border-radius: var(--radius);
461+
background: var(--ink-850);
462+
border: 1px solid var(--ink-700);
463+
color: var(--parchment-dim);
464+
}
465+
.turn-now--you {
466+
color: oklch(0.2 0.04 47);
467+
background: linear-gradient(180deg, var(--ember-bright), var(--ember));
468+
border-color: oklch(0.55 0.16 47);
469+
box-shadow: 0 10px 26px -12px oklch(0.69 0.17 47 / 0.8);
470+
animation: rise 0.3s ease both;
471+
}
472+
408473
/* ── Hero (player initiative) ──────────────────────────────────────────────*/
409474
.hero { text-align: center; gap: var(--sp-4); }
410475
.hero__value {

0 commit comments

Comments
 (0)