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 <noreply@anthropic.com>
This commit is contained in:
parent
b6ca67ff8b
commit
768c55c6b9
6 changed files with 108 additions and 11 deletions
27
CLAUDE.md
Normal file
27
CLAUDE.md
Normal file
|
|
@ -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`)
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tray.active {
|
.tray.active {
|
||||||
pointer-events: auto;
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17,7 +16,3 @@
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tray.active :global(canvas) {
|
|
||||||
pointer-events: auto !important;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,17 @@ import styles from "./FogOverlay.module.css";
|
||||||
|
|
||||||
interface FogOverlayProps {
|
interface FogOverlayProps {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
intensity: number; // 0–100
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FogOverlay({ active }: FogOverlayProps) {
|
export default function FogOverlay({ active, intensity }: FogOverlayProps) {
|
||||||
if (!active) return null;
|
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 (
|
return (
|
||||||
<div className={styles.overlay}>
|
<div className={styles.overlay} style={{ opacity }}>
|
||||||
<div className={styles.layer1} />
|
<div className={styles.layer1} />
|
||||||
<div className={styles.layer2} />
|
<div className={styles.layer2} />
|
||||||
<div className={styles.layer3} />
|
<div className={styles.layer3} />
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useRef, useEffect } from "react";
|
||||||
import type { Character } from "../types";
|
import type { Character } from "../types";
|
||||||
import TalentList from "./TalentList";
|
import TalentList from "./TalentList";
|
||||||
import SelectDropdown from "./SelectDropdown";
|
import SelectDropdown from "./SelectDropdown";
|
||||||
|
import { getShadowdarkTitle } from "../utils/shadowdark-titles.js";
|
||||||
import styles from "./InfoPanel.module.css";
|
import styles from "./InfoPanel.module.css";
|
||||||
|
|
||||||
const CLASSES = ["Fighter", "Priest", "Thief", "Wizard"];
|
const CLASSES = ["Fighter", "Priest", "Thief", "Wizard"];
|
||||||
|
|
@ -40,13 +41,24 @@ export default function InfoPanel({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function handleField(field: string, value: string | number) {
|
function handleField(field: string, value: string | number) {
|
||||||
if (typeof value === "string") {
|
const update: Partial<Character> = { [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);
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||||
debounceRef.current = setTimeout(() => {
|
debounceRef.current = setTimeout(() => {
|
||||||
onUpdate(character.id, { [field]: value });
|
onUpdate(character.id, update);
|
||||||
}, 400);
|
}, 400);
|
||||||
} else {
|
} else {
|
||||||
onUpdate(character.id, { [field]: value });
|
onUpdate(character.id, update);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ export default function CampaignView() {
|
||||||
</div>
|
</div>
|
||||||
<RollLog campaignId={campaignId} rolls={rolls} freshIds={freshIds} />
|
<RollLog campaignId={campaignId} rolls={rolls} freshIds={freshIds} />
|
||||||
<DiceTray roll={diceRoll} onAnimationComplete={handleDiceComplete} />
|
<DiceTray roll={diceRoll} onAnimationComplete={handleDiceComplete} />
|
||||||
<FogOverlay active={atmosphere.fog.active} />
|
<FogOverlay active={atmosphere.fog.active} intensity={atmosphere.fog.intensity} />
|
||||||
<ThreeFireOverlay active={atmosphere.fire.active} intensity={atmosphere.fire.intensity} />
|
<ThreeFireOverlay active={atmosphere.fire.active} intensity={atmosphere.fire.intensity} />
|
||||||
<ParticleOverlay atmosphere={atmosphere} />
|
<ParticleOverlay atmosphere={atmosphere} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
59
client/src/utils/shadowdark-titles.ts
Normal file
59
client/src/utils/shadowdark-titles.ts
Normal file
|
|
@ -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<CharClass, [number, number, string, string, string][]> = {
|
||||||
|
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<Alignment, 0 | 1 | 2> = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue