diff --git a/src/components/encounters/effects/AddEffectModal.tsx b/src/components/encounters/effects/AddEffectModal.tsx index 6b4c2219..59a936fd 100644 --- a/src/components/encounters/effects/AddEffectModal.tsx +++ b/src/components/encounters/effects/AddEffectModal.tsx @@ -11,6 +11,7 @@ import { Stack, Box, IconButton, + Typography, } from "@mui/material" import CircleIcon from "@mui/icons-material/Circle" import { useState } from "react" @@ -49,6 +50,8 @@ export default function AddEffectModal({ change: "", character_id: character.id, shot_id: character.shot_id, + end_sequence: null, + end_shot: null, }) // Define available action values based on character type @@ -72,10 +75,19 @@ export default function AddEffectModal({ const handleChange = (field: keyof CharacterEffect) => (event: React.ChangeEvent) => { - setEffect(prev => ({ - ...prev, - [field]: event.target.value, - })) + const value = event.target.value + // Handle numeric fields that can be null + if (field === "end_sequence" || field === "end_shot") { + setEffect(prev => ({ + ...prev, + [field]: value === "" ? null : Number(value), + })) + } else { + setEffect(prev => ({ + ...prev, + [field]: value, + })) + } } const handleSubmit = async () => { @@ -113,6 +125,8 @@ export default function AddEffectModal({ change: "", character_id: character.id, shot_id: character.shot_id, + end_sequence: null, + end_shot: null, }) onClose() } @@ -212,6 +226,38 @@ export default function AddEffectModal({ /> + + + + Expiry (Optional) + + + Leave empty for permanent effect. Effect expires when the shot + counter passes the specified shot. + + + + + + diff --git a/src/components/encounters/effects/CharacterEffectsDisplay.tsx b/src/components/encounters/effects/CharacterEffectsDisplay.tsx index 564c047d..dc765816 100644 --- a/src/components/encounters/effects/CharacterEffectsDisplay.tsx +++ b/src/components/encounters/effects/CharacterEffectsDisplay.tsx @@ -145,6 +145,21 @@ export default function CharacterEffectsDisplay({ return `${actionValueLabel(effect)} = ${effect.change}` } + const formatExpiry = (effect: CharacterEffect) => { + if (effect.end_sequence == null && effect.end_shot == null) { + return null + } + + const parts: string[] = [] + if (effect.end_sequence != null) { + parts.push(`Seq ${effect.end_sequence}`) + } + if (effect.end_shot != null) { + parts.push(`Shot ${effect.end_shot}`) + } + return `Expires: ${parts.join(", ")}` + } + const severityOrder: Severity[] = ["error", "warning", "info", "success"] // Cleanup timeouts on unmount @@ -219,50 +234,62 @@ export default function CharacterEffectsDisplay({ onMouseEnter={handlePopoverMouseEnter} onMouseLeave={handlePopoverClose} > - {groupedEffects[openSeverity].map((effect, index) => ( - - - - - {effect.name} - {effect.action_value && effect.change && ( + {groupedEffects[openSeverity].map((effect, index) => { + const expiry = formatExpiry(effect) + return ( + + + + + {effect.name} + {effect.action_value && effect.change && ( + + ({formatChange(effect)}) + + )} + + {expiry && ( - ({formatChange(effect)}) + {expiry} )} - - - {isGamemaster && ( - handleDeleteEffect(effect)} - sx={{ padding: 0.25 }} - > - - - )} - - ))} + + {isGamemaster && ( + handleDeleteEffect(effect)} + sx={{ padding: 0.25 }} + > + + + )} + + ) + })} )} diff --git a/src/types/resources.ts b/src/types/resources.ts index c4920a42..1954600c 100644 --- a/src/types/resources.ts +++ b/src/types/resources.ts @@ -443,6 +443,8 @@ export interface CharacterEffect { change?: string action_value?: string shot_id: string + end_sequence?: number | null + end_shot?: number | null } export interface CharacterEffects {