From 768c55c6b9ca61d74462b87f1c2a794ee53ccc2a Mon Sep 17 00:00:00 2001 From: Aaron Wood Date: Sat, 11 Apr 2026 03:53:00 -0400 Subject: [PATCH] fix: dice overlay no longer blocks clicks, fog alpha wired to intensity, title auto-updates from class/alignment/level - DiceTray: remove pointer-events:auto from .active so UI remains clickable during dice animation - FogOverlay: accept intensity prop, map 0-100 to 0.15-1.0 opacity - CampaignView: pass fog intensity to FogOverlay - InfoPanel: auto-derive Shadowdark title when class/alignment/level changes - Add shadowdark-titles.ts utility with full title lookup table from Player Quickstart - Add CLAUDE.md with project instructions and pre-approved permissions Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 27 +++++++++++ client/src/components/DiceTray.module.css | 5 -- client/src/components/FogOverlay.tsx | 8 ++- client/src/components/InfoPanel.tsx | 18 +++++-- client/src/pages/CampaignView.tsx | 2 +- client/src/utils/shadowdark-titles.ts | 59 +++++++++++++++++++++++ 6 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 CLAUDE.md create mode 100644 client/src/utils/shadowdark-titles.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9995036 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,27 @@ +# Darkwatch — Claude Code Instructions + +## Permissions + +All tool uses are pre-approved for this project. No confirmation needed for: +- Reading, writing, or editing any file in this project +- Running bash commands (npm, git, docker, curl, lsof, kill, pkill, etc.) +- Spawning subagents +- Searching files (Glob, Grep) + +## Project Overview + +This is **Darkwatch**, a real-time session manager for the Shadowdark TTRPG. + +- **Client:** `client/` — React + Vite + TypeScript + CSS Modules +- **Server:** `server/` — Express + Socket.IO + MariaDB (mysql2/promise) +- **DB:** MariaDB 11 in Docker (`darkwatch-maria` container, port 3307) +- **Worktrees:** `.worktrees/` (gitignored) +- **Roadmap:** `docs/ROADMAP.md` + +## Important Rules + +- NEVER touch the `mysql` Docker container — only `darkwatch-*` containers +- DB port is 3307 (not 3306) to avoid conflicts +- Local git repo only — commits are fine, NEVER push +- All imports use `.js` extensions (ES modules) +- Dev seed accounts: `dm@darkwatch.test` / `player@darkwatch.test` (password: `password`) diff --git a/client/src/components/DiceTray.module.css b/client/src/components/DiceTray.module.css index c7ec80c..7408754 100644 --- a/client/src/components/DiceTray.module.css +++ b/client/src/components/DiceTray.module.css @@ -8,7 +8,6 @@ } .tray.active { - pointer-events: auto; opacity: 1; } @@ -17,7 +16,3 @@ height: 100% !important; pointer-events: none !important; } - -.tray.active :global(canvas) { - pointer-events: auto !important; -} diff --git a/client/src/components/FogOverlay.tsx b/client/src/components/FogOverlay.tsx index 16d7962..6d2b696 100644 --- a/client/src/components/FogOverlay.tsx +++ b/client/src/components/FogOverlay.tsx @@ -2,13 +2,17 @@ import styles from "./FogOverlay.module.css"; interface FogOverlayProps { active: boolean; + intensity: number; // 0–100 } -export default function FogOverlay({ active }: FogOverlayProps) { +export default function FogOverlay({ active, intensity }: FogOverlayProps) { if (!active) return null; + // Map 0–100 to 0.15–1.0 so fog is always at least faintly visible when active + const opacity = 0.15 + (intensity / 100) * 0.85; + return ( -
+
diff --git a/client/src/components/InfoPanel.tsx b/client/src/components/InfoPanel.tsx index a91624e..47e4676 100644 --- a/client/src/components/InfoPanel.tsx +++ b/client/src/components/InfoPanel.tsx @@ -2,6 +2,7 @@ import { useRef, useEffect } from "react"; import type { Character } from "../types"; import TalentList from "./TalentList"; import SelectDropdown from "./SelectDropdown"; +import { getShadowdarkTitle } from "../utils/shadowdark-titles.js"; import styles from "./InfoPanel.module.css"; const CLASSES = ["Fighter", "Priest", "Thief", "Wizard"]; @@ -40,13 +41,24 @@ export default function InfoPanel({ }, []); function handleField(field: string, value: string | number) { - if (typeof value === "string") { + const update: Partial = { [field]: value }; + + // Auto-update title when class, alignment, or level changes + if (field === "class" || field === "alignment" || field === "level") { + const newClass = field === "class" ? String(value) : character.class; + const newAlignment = field === "alignment" ? String(value) : character.alignment; + const newLevel = field === "level" ? Number(value) : character.level; + const derived = getShadowdarkTitle(newClass, newAlignment, newLevel); + if (derived) update.title = derived; + } + + if (typeof value === "string" && field !== "class" && field !== "alignment") { if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { - onUpdate(character.id, { [field]: value }); + onUpdate(character.id, update); }, 400); } else { - onUpdate(character.id, { [field]: value }); + onUpdate(character.id, update); } } diff --git a/client/src/pages/CampaignView.tsx b/client/src/pages/CampaignView.tsx index 2a8fb8a..a6695c7 100644 --- a/client/src/pages/CampaignView.tsx +++ b/client/src/pages/CampaignView.tsx @@ -509,7 +509,7 @@ export default function CampaignView() {
- +
diff --git a/client/src/utils/shadowdark-titles.ts b/client/src/utils/shadowdark-titles.ts new file mode 100644 index 0000000..e1dd858 --- /dev/null +++ b/client/src/utils/shadowdark-titles.ts @@ -0,0 +1,59 @@ +type Alignment = "Lawful" | "Neutral" | "Chaotic"; +type CharClass = "Fighter" | "Priest" | "Thief" | "Wizard"; + +// Title table from the Shadowdark Player Quickstart +// [level range min, level range max, lawful, chaotic, neutral] +const TITLES: Record = { + Fighter: [ + [1, 2, "Squire", "Knave", "Warrior"], + [3, 4, "Cavalier", "Bandit", "Barbarian"], + [5, 6, "Knight", "Slayer", "Battlerager"], + [7, 8, "Thane", "Reaver", "Warchief"], + [9, 10, "Lord", "Warlord", "Chieftain"], + ], + Priest: [ + [1, 2, "Acolyte", "Initiate", "Seeker"], + [3, 4, "Crusader", "Zealot", "Invoker"], + [5, 6, "Templar", "Cultist", "Haruspex"], + [7, 8, "Champion", "Scourge", "Mystic"], + [9, 10, "Paladin", "Chaos Knight", "Oracle"], + ], + Thief: [ + [1, 2, "Footpad", "Thug", "Robber"], + [3, 4, "Burglar", "Cutthroat", "Outlaw"], + [5, 6, "Rook", "Shadow", "Rogue"], + [7, 8, "Underboss", "Assassin", "Renegade"], + [9, 10, "Boss", "Wraith", "Bandit King"], + ], + Wizard: [ + [1, 2, "Apprentice", "Adept", "Shaman"], + [3, 4, "Conjurer", "Channeler", "Seer"], + [5, 6, "Arcanist", "Witch", "Warden"], + [7, 8, "Mage", "Diabolist", "Sage"], + [9, 10, "Archmage", "Sorcerer", "Druid"], + ], +}; + +const ALIGNMENT_INDEX: Record = { + Lawful: 0, + Chaotic: 1, + Neutral: 2, +}; + +export function getShadowdarkTitle( + charClass: string, + alignment: string, + level: number +): string | null { + const classTable = TITLES[charClass as CharClass]; + if (!classTable) return null; + + const alignIndex = ALIGNMENT_INDEX[alignment as Alignment]; + if (alignIndex === undefined) return null; + + const clampedLevel = Math.min(Math.max(level, 1), 10); + const row = classTable.find(([min, max]) => clampedLevel >= min && clampedLevel <= max); + if (!row) return null; + + return row[2 + alignIndex] as string; +}