Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 50 additions & 4 deletions src/components/encounters/effects/AddEffectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Stack,
Box,
IconButton,
Typography,
} from "@mui/material"
import CircleIcon from "@mui/icons-material/Circle"
import { useState } from "react"
Expand Down Expand Up @@ -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
Expand All @@ -72,10 +75,19 @@ export default function AddEffectModal({
const handleChange =
(field: keyof CharacterEffect) =>
(event: React.ChangeEvent<HTMLInputElement>) => {
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 () => {
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -212,6 +226,38 @@ export default function AddEffectModal({
/>
</Stack>
</Box>

<Box>
<Box sx={{ mb: 1 }}>
<strong>Expiry (Optional)</strong>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Leave empty for permanent effect. Effect expires when the shot
counter passes the specified shot.
</Typography>
<Stack direction="row" spacing={2}>
<TextField
type="number"
label="Expires on Sequence"
name="end_sequence"
value={effect.end_sequence ?? ""}
onChange={handleChange("end_sequence")}
fullWidth
inputProps={{ min: 1 }}
helperText="Sequence number (1, 2, 3...)"
/>
<TextField
type="number"
label="Expires on Shot"
name="end_shot"
value={effect.end_shot ?? ""}
onChange={handleChange("end_shot")}
fullWidth
inputProps={{ min: 0, max: 30 }}
helperText="Shot value (0-30)"
/>
</Stack>
</Box>
</Stack>
</DialogContent>
<DialogActions>
Expand Down
107 changes: 67 additions & 40 deletions src/components/encounters/effects/CharacterEffectsDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -219,50 +234,62 @@ export default function CharacterEffectsDisplay({
onMouseEnter={handlePopoverMouseEnter}
onMouseLeave={handlePopoverClose}
>
{groupedEffects[openSeverity].map((effect, index) => (
<Stack
key={effect.id || index}
direction="row"
alignItems="center"
spacing={1}
sx={{
py: 0.5,
borderBottom:
index < groupedEffects[openSeverity].length - 1
? "1px solid"
: "none",
borderColor: "divider",
}}
>
<InfoOutlinedIcon
fontSize="small"
color={severityColors[openSeverity]}
/>
<Box sx={{ flex: 1 }}>
<Typography variant="body2">
{effect.name}
{effect.action_value && effect.change && (
{groupedEffects[openSeverity].map((effect, index) => {
const expiry = formatExpiry(effect)
return (
<Stack
key={effect.id || index}
direction="row"
alignItems="center"
spacing={1}
sx={{
py: 0.5,
borderBottom:
index < groupedEffects[openSeverity].length - 1
? "1px solid"
: "none",
borderColor: "divider",
}}
>
<InfoOutlinedIcon
fontSize="small"
color={severityColors[openSeverity]}
/>
<Box sx={{ flex: 1 }}>
<Typography variant="body2">
{effect.name}
{effect.action_value && effect.change && (
<Typography
component="span"
variant="body2"
sx={{ ml: 1, fontWeight: "bold" }}
>
({formatChange(effect)})
</Typography>
)}
</Typography>
{expiry && (
<Typography
component="span"
variant="body2"
sx={{ ml: 1, fontWeight: "bold" }}
variant="caption"
color="text.secondary"
sx={{ display: "block" }}
>
({formatChange(effect)})
{expiry}
</Typography>
)}
</Typography>
</Box>
{isGamemaster && (
<IconButton
size="small"
onClick={() => handleDeleteEffect(effect)}
sx={{ padding: 0.25 }}
>
<DeleteIcon sx={{ fontSize: 16 }} />
</IconButton>
)}
</Stack>
))}
</Box>
{isGamemaster && (
<IconButton
size="small"
onClick={() => handleDeleteEffect(effect)}
sx={{ padding: 0.25 }}
>
<DeleteIcon sx={{ fontSize: 16 }} />
</IconButton>
)}
</Stack>
)
})}
</Box>
)}
</Popover>
Expand Down
2 changes: 2 additions & 0 deletions src/types/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading