From 859ac64868eb33c9d759a4b292683274a22aee00 Mon Sep 17 00:00:00 2001 From: Aaron Wood Date: Thu, 9 Apr 2026 13:46:41 -0400 Subject: [PATCH] Fix crit glow: only the weapon that scored nat 20 shows crit indicator, not all weapons --- client/src/components/AttackBlock.tsx | 10 +++++----- client/src/components/CharacterDetail.tsx | 6 +++--- client/src/components/CharacterSheet.tsx | 6 +++--- client/src/components/StatsPanel.tsx | 6 +++--- client/src/pages/CampaignView.tsx | 19 ++++++++++++------- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/client/src/components/AttackBlock.tsx b/client/src/components/AttackBlock.tsx index f5125c8..cc1852d 100644 --- a/client/src/components/AttackBlock.tsx +++ b/client/src/components/AttackBlock.tsx @@ -9,7 +9,7 @@ interface AttackBlockProps { characterName?: string; characterColor?: string; mode?: "view" | "edit"; - isCrit?: boolean; + critKeys?: Set; } export default function AttackBlock({ @@ -19,7 +19,7 @@ export default function AttackBlock({ characterName, characterColor, mode, - isCrit, + critKeys, }: AttackBlockProps) { const weapons = attacks.filter((a) => !a.isTalent); const talents = attacks.filter((a) => a.isTalent); @@ -65,13 +65,13 @@ export default function AttackBlock({ type="attack" dice={atk.damage} label={`${atk.name} damage`} - icon={isCrit ? "💥" : "💥"} + icon="💥" title={ - isCrit + critKeys?.has(`${characterId}:${atk.name}`) ? `CRIT damage: double ${atk.damage}` : `Damage: ${atk.damage} (Shift: crit = double dice)` } - forceCrit={isCrit} + forceCrit={critKeys?.has(`${characterId}:${atk.name}`)} /> ) : ( diff --git a/client/src/components/CharacterDetail.tsx b/client/src/components/CharacterDetail.tsx index 366985a..49fd554 100644 --- a/client/src/components/CharacterDetail.tsx +++ b/client/src/components/CharacterDetail.tsx @@ -6,7 +6,7 @@ import styles from "./CharacterDetail.module.css"; interface CharacterDetailProps { character: Character; campaignId: number; - isCrit?: boolean; + critKeys?: Set; onUpdate: (id: number, data: Partial) => void; onStatChange: (characterId: number, statName: string, value: number) => void; onAddGearFromItem: (characterId: number, item: GameItem) => void; @@ -32,7 +32,7 @@ interface CharacterDetailProps { export default function CharacterDetail({ character, campaignId, - isCrit, + critKeys, onUpdate, onStatChange, onAddGearFromItem, @@ -64,7 +64,7 @@ export default function CharacterDetail({ character={character} mode={mode} campaignId={campaignId} - isCrit={isCrit} + critKeys={critKeys} onUpdate={onUpdate} onStatChange={onStatChange} onAddGearFromItem={onAddGearFromItem} diff --git a/client/src/components/CharacterSheet.tsx b/client/src/components/CharacterSheet.tsx index b372450..56754c9 100644 --- a/client/src/components/CharacterSheet.tsx +++ b/client/src/components/CharacterSheet.tsx @@ -12,7 +12,7 @@ interface CharacterSheetProps { character: Character; mode: "view" | "edit"; campaignId: number; - isCrit?: boolean; + critKeys?: Set; onUpdate: (id: number, data: Partial) => void; onStatChange: (characterId: number, statName: string, value: number) => void; onAddGearFromItem: (characterId: number, item: GameItem) => void; @@ -38,7 +38,7 @@ export default function CharacterSheet({ character, mode, campaignId, - isCrit, + critKeys, onUpdate, onStatChange, onAddGearFromItem, @@ -165,7 +165,7 @@ export default function CharacterSheet({ character={character} mode={mode} campaignId={campaignId} - isCrit={isCrit} + critKeys={critKeys} onStatChange={onStatChange} /> ; onStatChange: (characterId: number, statName: string, value: number) => void; } @@ -16,7 +16,7 @@ export default function StatsPanel({ character, mode, campaignId, - isCrit, + critKeys, onStatChange, }: StatsPanelProps) { const attacks = generateAttacks(character); @@ -43,7 +43,7 @@ export default function StatsPanel({ characterName={character.name} characterColor={character.color} mode={mode} - isCrit={isCrit} + critKeys={critKeys} /> ); diff --git a/client/src/pages/CampaignView.tsx b/client/src/pages/CampaignView.tsx index 778c864..5e21394 100644 --- a/client/src/pages/CampaignView.tsx +++ b/client/src/pages/CampaignView.tsx @@ -36,7 +36,7 @@ export default function CampaignView() { }); const [rolls, setRolls] = useState([]); const [freshIds, setFreshIds] = useState>(new Set()); - const [critCharIds, setCritCharIds] = useState>(new Set()); + const [critKeys, setCritKeys] = useState>(new Set()); // Fetch characters and join socket room useEffect(() => { @@ -172,15 +172,20 @@ export default function CampaignView() { }); }, 2000); - // Track crits: if this is a nat20 attack, mark character for crit damage + // Track crits by character+weapon key (e.g. "1:SHORTSWORD") if (roll.nat20 && roll.character_id && roll.label.includes("attack")) { - setCritCharIds((prev) => new Set(prev).add(roll.character_id!)); + const weaponName = roll.label.replace(" attack", ""); + const key = `${roll.character_id}:${weaponName}`; + setCritKeys((prev) => new Set(prev).add(key)); } - // If this is a damage roll, clear the crit flag for that character if (roll.character_id && roll.label.includes("damage")) { - setCritCharIds((prev) => { + const weaponName = roll.label + .replace(" damage", "") + .replace(" (CRIT)", ""); + const key = `${roll.character_id}:${weaponName}`; + setCritKeys((prev) => { const next = new Set(prev); - next.delete(roll.character_id!); + next.delete(key); return next; }); } @@ -316,7 +321,7 @@ export default function CampaignView() {