diff --git a/client/src/components/CharacterSheet.module.css b/client/src/components/CharacterSheet.module.css
index 89d8432..6239766 100644
--- a/client/src/components/CharacterSheet.module.css
+++ b/client/src/components/CharacterSheet.module.css
@@ -104,6 +104,37 @@
font-weight: 600;
}
+.luckBtn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 1.1rem;
+ line-height: 1;
+ padding: 0.1rem;
+ transition: transform 0.15s;
+}
+
+.luckBtn:hover {
+ transform: scale(1.2);
+}
+
+.colorPicker {
+ width: 2rem;
+ height: 1.5rem;
+ border: 1px solid rgba(var(--gold-rgb), 0.3);
+ border-radius: 3px;
+ padding: 0;
+ cursor: pointer;
+ background: none;
+}
+
+.colorRow {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ margin-top: 0.3rem;
+}
+
.nameInput {
font-family: "Cinzel", Georgia, serif;
font-size: 1.3rem;
diff --git a/client/src/components/CharacterSheet.tsx b/client/src/components/CharacterSheet.tsx
index b1192c5..fdd8a58 100644
--- a/client/src/components/CharacterSheet.tsx
+++ b/client/src/components/CharacterSheet.tsx
@@ -89,11 +89,22 @@ export default function CharacterSheet({
+
+ {/* Luck token — always toggleable */}
+
+
+
diff --git a/client/src/components/DiceTray.tsx b/client/src/components/DiceTray.tsx
index 51ee3f9..9cc3da3 100644
--- a/client/src/components/DiceTray.tsx
+++ b/client/src/components/DiceTray.tsx
@@ -138,9 +138,12 @@ function buildNotation(expression: string, rolls: number[]): string | null {
return `${count}d${sides}@${values}`;
}
-/** Convert an HSL string like "hsl(210, 70%, 50%)" to a hex color */
-function hslToHex(hsl: string): string {
- const match = hsl.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/);
+/** Convert a color string (HSL or hex) to hex */
+function hslToHex(color: string): string {
+ // Already hex — pass through
+ if (color.startsWith("#")) return color;
+
+ const match = color.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/);
if (!match) return "#8B2020";
const h = parseInt(match[1]) / 360;
diff --git a/client/src/components/StatBlock.module.css b/client/src/components/StatBlock.module.css
index 5ed388f..3169da7 100644
--- a/client/src/components/StatBlock.module.css
+++ b/client/src/components/StatBlock.module.css
@@ -54,30 +54,3 @@
color: var(--hp);
font-size: 0.65rem;
}
-
-.btn {
- width: 22px;
- height: 22px;
- border-radius: 50%;
- border: 1px solid rgba(var(--gold-rgb), 0.25);
- background: var(--bg-inset);
- color: var(--text-primary);
- cursor: pointer;
- font-size: 0.8rem;
- display: flex;
- align-items: center;
- justify-content: center;
- line-height: 1;
- transition:
- border-color 0.15s,
- color 0.15s;
-}
-
-.btn:hover {
- border-color: var(--gold);
- color: var(--gold);
-}
-
-.rollSpace {
- width: 2.5rem;
-}
diff --git a/client/src/components/StatBlock.tsx b/client/src/components/StatBlock.tsx
index c96b3fa..d181c14 100644
--- a/client/src/components/StatBlock.tsx
+++ b/client/src/components/StatBlock.tsx
@@ -1,5 +1,6 @@
import type { Stat } from "../types";
import { getModifier, formatModifier } from "../utils/modifiers";
+import InlineNumber from "./InlineNumber";
import DiceButton from "./DiceButton";
import styles from "./StatBlock.module.css";
@@ -39,14 +40,6 @@ export default function StatBlock({
{name}
- {mode === "edit" && (
-
- )}
{formatModifier(mod)}
{mode === "view" && campaignId && (
)}
- {mode === "edit" && (
-
- )}
- {value}
+ {mode === "edit" ? (
+ onStatChange(name, v)}
+ min={1}
+ max={30}
+ />
+ ) : (
+ value
+ )}
{bonus > 0 && (+{bonus})}
diff --git a/client/src/types.ts b/client/src/types.ts
index 16d44fc..7b33926 100644
--- a/client/src/types.ts
+++ b/client/src/types.ts
@@ -54,6 +54,7 @@ export interface Character {
gear_slots_max: number;
overrides: Record;
color: string;
+ luck_token: number;
stats: Stat[];
gear: Gear[];
talents: Talent[];
diff --git a/server/src/db.ts b/server/src/db.ts
index 5c265bf..eb19117 100644
--- a/server/src/db.ts
+++ b/server/src/db.ts
@@ -111,6 +111,7 @@ const v2Columns: Array<[string, string, string]> = [
["character_gear", "effects", "TEXT DEFAULT '{}'"],
["character_talents", "game_talent_id", "INTEGER"],
["characters", "color", "TEXT DEFAULT ''"],
+ ["characters", "luck_token", "INTEGER DEFAULT 1"],
["roll_log", "nat20", "INTEGER DEFAULT 0"],
["roll_log", "character_color", "TEXT DEFAULT ''"],
];
diff --git a/server/src/routes/characters.ts b/server/src/routes/characters.ts
index fe59c35..d5a2863 100644
--- a/server/src/routes/characters.ts
+++ b/server/src/routes/characters.ts
@@ -148,6 +148,8 @@ router.patch("/:id", (req, res) => {
"cp",
"gear_slots_max",
"overrides",
+ "color",
+ "luck_token",
];
const updates: string[] = [];