Fix crit glow: only the weapon that scored nat 20 shows crit indicator, not all weapons

This commit is contained in:
Aaron Wood 2026-04-09 13:46:41 -04:00
parent b0fa709767
commit 859ac64868
5 changed files with 26 additions and 21 deletions

View file

@ -9,7 +9,7 @@ interface AttackBlockProps {
characterName?: string; characterName?: string;
characterColor?: string; characterColor?: string;
mode?: "view" | "edit"; mode?: "view" | "edit";
isCrit?: boolean; critKeys?: Set<string>;
} }
export default function AttackBlock({ export default function AttackBlock({
@ -19,7 +19,7 @@ export default function AttackBlock({
characterName, characterName,
characterColor, characterColor,
mode, mode,
isCrit, critKeys,
}: AttackBlockProps) { }: AttackBlockProps) {
const weapons = attacks.filter((a) => !a.isTalent); const weapons = attacks.filter((a) => !a.isTalent);
const talents = attacks.filter((a) => a.isTalent); const talents = attacks.filter((a) => a.isTalent);
@ -65,13 +65,13 @@ export default function AttackBlock({
type="attack" type="attack"
dice={atk.damage} dice={atk.damage}
label={`${atk.name} damage`} label={`${atk.name} damage`}
icon={isCrit ? "💥" : "💥"} icon="💥"
title={ title={
isCrit critKeys?.has(`${characterId}:${atk.name}`)
? `CRIT damage: double ${atk.damage}` ? `CRIT damage: double ${atk.damage}`
: `Damage: ${atk.damage} (Shift: crit = double dice)` : `Damage: ${atk.damage} (Shift: crit = double dice)`
} }
forceCrit={isCrit} forceCrit={critKeys?.has(`${characterId}:${atk.name}`)}
/> />
</span> </span>
) : ( ) : (

View file

@ -6,7 +6,7 @@ import styles from "./CharacterDetail.module.css";
interface CharacterDetailProps { interface CharacterDetailProps {
character: Character; character: Character;
campaignId: number; campaignId: number;
isCrit?: boolean; critKeys?: Set<string>;
onUpdate: (id: number, data: Partial<Character>) => void; onUpdate: (id: number, data: Partial<Character>) => void;
onStatChange: (characterId: number, statName: string, value: number) => void; onStatChange: (characterId: number, statName: string, value: number) => void;
onAddGearFromItem: (characterId: number, item: GameItem) => void; onAddGearFromItem: (characterId: number, item: GameItem) => void;
@ -32,7 +32,7 @@ interface CharacterDetailProps {
export default function CharacterDetail({ export default function CharacterDetail({
character, character,
campaignId, campaignId,
isCrit, critKeys,
onUpdate, onUpdate,
onStatChange, onStatChange,
onAddGearFromItem, onAddGearFromItem,
@ -64,7 +64,7 @@ export default function CharacterDetail({
character={character} character={character}
mode={mode} mode={mode}
campaignId={campaignId} campaignId={campaignId}
isCrit={isCrit} critKeys={critKeys}
onUpdate={onUpdate} onUpdate={onUpdate}
onStatChange={onStatChange} onStatChange={onStatChange}
onAddGearFromItem={onAddGearFromItem} onAddGearFromItem={onAddGearFromItem}

View file

@ -12,7 +12,7 @@ interface CharacterSheetProps {
character: Character; character: Character;
mode: "view" | "edit"; mode: "view" | "edit";
campaignId: number; campaignId: number;
isCrit?: boolean; critKeys?: Set<string>;
onUpdate: (id: number, data: Partial<Character>) => void; onUpdate: (id: number, data: Partial<Character>) => void;
onStatChange: (characterId: number, statName: string, value: number) => void; onStatChange: (characterId: number, statName: string, value: number) => void;
onAddGearFromItem: (characterId: number, item: GameItem) => void; onAddGearFromItem: (characterId: number, item: GameItem) => void;
@ -38,7 +38,7 @@ export default function CharacterSheet({
character, character,
mode, mode,
campaignId, campaignId,
isCrit, critKeys,
onUpdate, onUpdate,
onStatChange, onStatChange,
onAddGearFromItem, onAddGearFromItem,
@ -165,7 +165,7 @@ export default function CharacterSheet({
character={character} character={character}
mode={mode} mode={mode}
campaignId={campaignId} campaignId={campaignId}
isCrit={isCrit} critKeys={critKeys}
onStatChange={onStatChange} onStatChange={onStatChange}
/> />
<InfoPanel <InfoPanel

View file

@ -8,7 +8,7 @@ interface StatsPanelProps {
character: Character; character: Character;
mode: "view" | "edit"; mode: "view" | "edit";
campaignId: number; campaignId: number;
isCrit?: boolean; critKeys?: Set<string>;
onStatChange: (characterId: number, statName: string, value: number) => void; onStatChange: (characterId: number, statName: string, value: number) => void;
} }
@ -16,7 +16,7 @@ export default function StatsPanel({
character, character,
mode, mode,
campaignId, campaignId,
isCrit, critKeys,
onStatChange, onStatChange,
}: StatsPanelProps) { }: StatsPanelProps) {
const attacks = generateAttacks(character); const attacks = generateAttacks(character);
@ -43,7 +43,7 @@ export default function StatsPanel({
characterName={character.name} characterName={character.name}
characterColor={character.color} characterColor={character.color}
mode={mode} mode={mode}
isCrit={isCrit} critKeys={critKeys}
/> />
</div> </div>
); );

View file

@ -36,7 +36,7 @@ export default function CampaignView() {
}); });
const [rolls, setRolls] = useState<RollResult[]>([]); const [rolls, setRolls] = useState<RollResult[]>([]);
const [freshIds, setFreshIds] = useState<Set<number>>(new Set()); const [freshIds, setFreshIds] = useState<Set<number>>(new Set());
const [critCharIds, setCritCharIds] = useState<Set<number>>(new Set()); const [critKeys, setCritKeys] = useState<Set<string>>(new Set());
// Fetch characters and join socket room // Fetch characters and join socket room
useEffect(() => { useEffect(() => {
@ -172,15 +172,20 @@ export default function CampaignView() {
}); });
}, 2000); }, 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")) { 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")) { 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); const next = new Set(prev);
next.delete(roll.character_id!); next.delete(key);
return next; return next;
}); });
} }
@ -316,7 +321,7 @@ export default function CampaignView() {
<CharacterDetail <CharacterDetail
character={selectedCharacter} character={selectedCharacter}
campaignId={campaignId} campaignId={campaignId}
isCrit={critCharIds.has(selectedCharacter.id)} critKeys={critKeys}
onUpdate={handleUpdate} onUpdate={handleUpdate}
onStatChange={handleStatChange} onStatChange={handleStatChange}
onAddGearFromItem={handleAddGearFromItem} onAddGearFromItem={handleAddGearFromItem}