Add pokemon status management with death tracking

Implement status change workflow (alive → dead) with confirmation modal,
death cause recording, and visual status indicators on pokemon cards.
Includes backend migration for death_cause field and graveyard view
on the run dashboard.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 18:36:08 +01:00
parent 628c621fa9
commit a911259ef5
12 changed files with 462 additions and 53 deletions

View File

@@ -1,30 +1,39 @@
--- ---
# nuzlocke-tracker-hm6t # nuzlocke-tracker-hm6t
title: Pokemon Status Management title: Pokemon Status Management
status: todo status: completed
type: task type: task
priority: normal
created_at: 2026-02-04T15:44:37Z created_at: 2026-02-04T15:44:37Z
updated_at: 2026-02-04T15:44:37Z updated_at: 2026-02-05T16:47:18Z
parent: nuzlocke-tracker-f5ob parent: nuzlocke-tracker-f5ob
--- ---
Implement the system for tracking Pokémon status (alive, dead, boxed). Implement the system for tracking Pokémon status (alive, dead, boxed).
## Checklist ## Checklist
- [ ] Create Pokémon card/tile component showing: - [x] Create Pokémon card/tile component showing:
- [ ] Sprite, name, nickname - [x] Sprite, name, nickname
- [ ] Current status with visual indicator - [x] Current status with visual indicator (green/red dot)
- [ ] Location caught - [x] Location caught
- [ ] Implement status transitions: - [x] Implement status transitions:
- [ ] Alive → Dead (fainted in battle) - [x] Alive → Dead (fainted in battle) via StatusChangeModal with confirmation
- [ ] Alive → Boxed (stored in PC) - [ ] Alive → Boxed (stored in PC) — deferred, no boxed tracking yet
- [ ] Boxed → Alive (added to party) - [ ] Boxed → Alive (added to party) — deferred, no boxed tracking yet
- [ ] Add death recording: - [x] Add death recording:
- [ ] Optional: record cause of death (trainer, wild, gym leader) - [x] Optional: record cause of death (free text, max 100 chars)
- [ ] Optional: record level at death - [x] Optional: record level at death
- [ ] Create "Graveyard" view for fallen Pokémon - [x] Create "Graveyard" view for fallen Pokémon (on RunDashboard)
- [ ] Create "Box" view for stored Pokémon - [ ] Create "Box" view for stored Pokémon — deferred, no boxed tracking yet
## Implementation (death_cause feature)
- Backend: Alembic migration adds `death_cause` VARCHAR(100) to encounters
- Backend: Model + schemas updated with `death_cause` field
- Frontend: `StatusChangeModal` for recording death with confirmation from RunDashboard
- Frontend: `PokemonCard` now clickable with status indicator dot and death cause display
- Frontend: `EncounterModal` includes death cause input alongside faint level
- Frontend: `RunEncounters` shows death cause in route list
## Notes ## Notes
- Status changes should be confirmable (prevent accidental deaths) - Status changes should be confirmable (prevent accidental deaths)
- Consider undo functionality for misclicks - Consider undo functionality for misclicks — not implemented (Nuzlocke rules: death is permanent)

View File

@@ -1 +1,44 @@
# nuzlocke-tracker # nuzlocke-tracker
A full-stack Nuzlocke run tracker for Pokemon games.
## Getting Started
### Prerequisites
- Docker & Docker Compose
### Start the Stack
```bash
docker compose up
```
This starts three services:
| Service | URL |
|------------|--------------------------|
| Frontend | http://localhost:5173 |
| API | http://localhost:8000 |
| API Docs | http://localhost:8000/docs|
| PostgreSQL | localhost:5432 |
### Run Migrations
```bash
docker compose exec api alembic -c /app/alembic.ini upgrade head
```
### Seed the Database
```bash
docker compose exec api python -m app.seeds
```
To seed and verify the data was loaded correctly:
```bash
docker compose exec api python -m app.seeds --verify
```
This loads game data, Pokemon, routes, and encounter tables for FireRed, LeafGreen, Emerald, HeartGold, and SoulSilver.

View File

@@ -0,0 +1,26 @@
"""add death_cause to encounters
Revision ID: a1b2c3d4e5f6
Revises: 9afcbafe9888
Create Date: 2026-02-05 17:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'a1b2c3d4e5f6'
down_revision: Union[str, Sequence[str], None] = '9afcbafe9888'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column('encounters', sa.Column('death_cause', sa.String(100), nullable=True))
def downgrade() -> None:
op.drop_column('encounters', 'death_cause')

View File

@@ -17,6 +17,7 @@ class Encounter(Base):
status: Mapped[str] = mapped_column(String(20)) # caught, fainted, missed status: Mapped[str] = mapped_column(String(20)) # caught, fainted, missed
catch_level: Mapped[int | None] = mapped_column(SmallInteger) catch_level: Mapped[int | None] = mapped_column(SmallInteger)
faint_level: Mapped[int | None] = mapped_column(SmallInteger) faint_level: Mapped[int | None] = mapped_column(SmallInteger)
death_cause: Mapped[str | None] = mapped_column(String(100))
caught_at: Mapped[datetime] = mapped_column( caught_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now() DateTime(timezone=True), server_default=func.now()
) )

View File

@@ -17,6 +17,7 @@ class EncounterUpdate(CamelModel):
nickname: str | None = None nickname: str | None = None
status: str | None = None status: str | None = None
faint_level: int | None = None faint_level: int | None = None
death_cause: str | None = None
class EncounterResponse(CamelModel): class EncounterResponse(CamelModel):
@@ -28,6 +29,7 @@ class EncounterResponse(CamelModel):
status: str status: str
catch_level: int | None catch_level: int | None
faint_level: int | None faint_level: int | None
death_cause: str | None
caught_at: datetime caught_at: datetime

View File

@@ -19,7 +19,12 @@ interface EncounterModalProps {
}) => void }) => void
onUpdate?: (data: { onUpdate?: (data: {
id: number id: number
data: { nickname?: string; status?: EncounterStatus; faintLevel?: number } data: {
nickname?: string
status?: EncounterStatus
faintLevel?: number
deathCause?: string
}
}) => void }) => void
onClose: () => void onClose: () => void
isPending: boolean isPending: boolean
@@ -69,6 +74,7 @@ export function EncounterModal({
existing?.catchLevel?.toString() ?? '', existing?.catchLevel?.toString() ?? '',
) )
const [faintLevel, setFaintLevel] = useState<string>('') const [faintLevel, setFaintLevel] = useState<string>('')
const [deathCause, setDeathCause] = useState('')
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const isEditing = !!existing const isEditing = !!existing
@@ -95,6 +101,7 @@ export function EncounterModal({
nickname: nickname || undefined, nickname: nickname || undefined,
status, status,
faintLevel: faintLevel ? Number(faintLevel) : undefined, faintLevel: faintLevel ? Number(faintLevel) : undefined,
deathCause: deathCause || undefined,
}, },
}) })
} else if (selectedPokemon) { } else if (selectedPokemon) {
@@ -301,31 +308,53 @@ export function EncounterModal({
</div> </div>
)} )}
{/* Faint Level (only when editing a caught pokemon to mark dead) */} {/* Faint Level + Death Cause (only when editing a caught pokemon to mark dead) */}
{isEditing && {isEditing &&
existing?.status === 'caught' && existing?.status === 'caught' &&
existing?.faintLevel === null && ( existing?.faintLevel === null && (
<div> <>
<label <div>
htmlFor="faint-level" <label
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" htmlFor="faint-level"
> className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
Faint Level{' '} >
<span className="font-normal text-gray-400"> Faint Level{' '}
(mark as dead) <span className="font-normal text-gray-400">
</span> (mark as dead)
</label> </span>
<input </label>
id="faint-level" <input
type="number" id="faint-level"
min={1} type="number"
max={100} min={1}
value={faintLevel} max={100}
onChange={(e) => setFaintLevel(e.target.value)} value={faintLevel}
placeholder="Leave empty if still alive" onChange={(e) => setFaintLevel(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Leave empty if still alive"
/> className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
</div> />
</div>
<div>
<label
htmlFor="death-cause"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Cause of Death{' '}
<span className="font-normal text-gray-400">
(optional)
</span>
</label>
<input
id="death-cause"
type="text"
maxLength={100}
value={deathCause}
onChange={(e) => setDeathCause(e.target.value)}
placeholder="e.g. Crit from rival's Charizard"
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</>
)} )}
</div> </div>

View File

@@ -3,6 +3,7 @@ import type { EncounterDetail } from '../types'
interface PokemonCardProps { interface PokemonCardProps {
encounter: EncounterDetail encounter: EncounterDetail
showFaintLevel?: boolean showFaintLevel?: boolean
onClick?: () => void
} }
const typeColors: Record<string, string> = { const typeColors: Record<string, string> = {
@@ -26,15 +27,16 @@ const typeColors: Record<string, string> = {
fairy: 'bg-pink-300', fairy: 'bg-pink-300',
} }
export function PokemonCard({ encounter, showFaintLevel }: PokemonCardProps) { export function PokemonCard({ encounter, showFaintLevel, onClick }: PokemonCardProps) {
const { pokemon, route, nickname, catchLevel, faintLevel } = encounter const { pokemon, route, nickname, catchLevel, faintLevel, deathCause } = encounter
const isDead = faintLevel !== null const isDead = faintLevel !== null
return ( return (
<div <div
onClick={onClick}
className={`bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex flex-col items-center text-center ${ className={`bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex flex-col items-center text-center ${
isDead ? 'opacity-60 grayscale' : '' isDead ? 'opacity-60 grayscale' : ''
}`} } ${onClick ? 'cursor-pointer hover:ring-2 hover:ring-blue-400 transition-shadow' : ''}`}
> >
{pokemon.spriteUrl ? ( {pokemon.spriteUrl ? (
<img <img
@@ -48,8 +50,13 @@ export function PokemonCard({ encounter, showFaintLevel }: PokemonCardProps) {
</div> </div>
)} )}
<div className="mt-2 font-semibold text-gray-900 dark:text-gray-100 text-sm"> <div className="mt-2 flex items-center gap-1.5">
{nickname || pokemon.name} <span
className={`w-2 h-2 rounded-full shrink-0 ${isDead ? 'bg-red-500' : 'bg-green-500'}`}
/>
<span className="font-semibold text-gray-900 dark:text-gray-100 text-sm">
{nickname || pokemon.name}
</span>
</div> </div>
{nickname && ( {nickname && (
<div className="text-xs text-gray-500 dark:text-gray-400"> <div className="text-xs text-gray-500 dark:text-gray-400">
@@ -77,6 +84,12 @@ export function PokemonCard({ encounter, showFaintLevel }: PokemonCardProps) {
<div className="text-xs text-gray-400 dark:text-gray-500 mt-0.5"> <div className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
{route.name} {route.name}
</div> </div>
{isDead && deathCause && (
<div className="text-[10px] italic text-gray-400 dark:text-gray-500 mt-0.5 line-clamp-2">
{deathCause}
</div>
)}
</div> </div>
) )
} }

View File

@@ -0,0 +1,247 @@
import { useState } from 'react'
import type { EncounterDetail } from '../types'
interface StatusChangeModalProps {
encounter: EncounterDetail
onUpdate: (data: {
id: number
data: { faintLevel?: number; deathCause?: string }
}) => void
onClose: () => void
isPending: boolean
}
const typeColors: Record<string, string> = {
normal: 'bg-gray-400',
fire: 'bg-red-500',
water: 'bg-blue-500',
electric: 'bg-yellow-400',
grass: 'bg-green-500',
ice: 'bg-cyan-300',
fighting: 'bg-red-700',
poison: 'bg-purple-500',
ground: 'bg-amber-600',
flying: 'bg-indigo-300',
psychic: 'bg-pink-500',
bug: 'bg-lime-500',
rock: 'bg-amber-700',
ghost: 'bg-purple-700',
dragon: 'bg-indigo-600',
dark: 'bg-gray-700',
steel: 'bg-gray-400',
fairy: 'bg-pink-300',
}
export function StatusChangeModal({
encounter,
onUpdate,
onClose,
isPending,
}: StatusChangeModalProps) {
const { pokemon, route, nickname, catchLevel, faintLevel, deathCause } =
encounter
const isDead = faintLevel !== null
const [showConfirm, setShowConfirm] = useState(false)
const [deathLevel, setDeathLevel] = useState('')
const [cause, setCause] = useState('')
const handleConfirmDeath = () => {
onUpdate({
id: encounter.id,
data: {
faintLevel: deathLevel ? Number(deathLevel) : undefined,
deathCause: cause || undefined,
},
})
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-sm w-full">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{isDead ? 'Death Details' : 'Pokemon Status'}
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div className="px-6 py-4">
{/* Pokemon info */}
<div className="flex items-center gap-4 mb-4">
{pokemon.spriteUrl ? (
<img
src={pokemon.spriteUrl}
alt={pokemon.name}
className={`w-16 h-16 ${isDead ? 'grayscale opacity-60' : ''}`}
/>
) : (
<div className="w-16 h-16 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-xl font-bold text-gray-600 dark:text-gray-300">
{pokemon.name[0].toUpperCase()}
</div>
)}
<div>
<div className="font-semibold text-gray-900 dark:text-gray-100">
{nickname || pokemon.name}
</div>
{nickname && (
<div className="text-xs text-gray-500 dark:text-gray-400 capitalize">
{pokemon.name}
</div>
)}
<div className="flex gap-1 mt-1">
{pokemon.types.map((type) => (
<span
key={type}
className={`px-1.5 py-0.5 rounded text-[10px] font-medium text-white ${typeColors[type] ?? 'bg-gray-500'}`}
>
{type}
</span>
))}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Lv. {catchLevel ?? '?'} &middot; {route.name}
</div>
</div>
</div>
{/* Dead pokemon: view-only details */}
{isDead && (
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2 text-red-700 dark:text-red-400 font-medium text-sm">
<span className="w-2 h-2 rounded-full bg-red-500" />
Deceased
</div>
{faintLevel !== null && (
<div className="text-sm text-gray-700 dark:text-gray-300">
<span className="text-gray-500 dark:text-gray-400">
Level at death:
</span>{' '}
{faintLevel}
</div>
)}
{deathCause && (
<div className="text-sm text-gray-700 dark:text-gray-300">
<span className="text-gray-500 dark:text-gray-400">
Cause:
</span>{' '}
{deathCause}
</div>
)}
</div>
)}
{/* Alive pokemon: mark as dead */}
{!isDead && !showConfirm && (
<button
type="button"
onClick={() => setShowConfirm(true)}
className="w-full px-4 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors"
>
Mark as Dead
</button>
)}
{/* Confirmation form */}
{!isDead && showConfirm && (
<div className="space-y-3">
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-3">
<p className="text-sm text-red-700 dark:text-red-400 font-medium">
This cannot be undone (Nuzlocke rules).
</p>
</div>
<div>
<label
htmlFor="death-level"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Level at Death{' '}
<span className="font-normal text-gray-400">(optional)</span>
</label>
<input
id="death-level"
type="number"
min={1}
max={100}
value={deathLevel}
onChange={(e) => setDeathLevel(e.target.value)}
placeholder="Level"
className="w-24 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-red-500"
/>
</div>
<div>
<label
htmlFor="death-cause"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
>
Cause of Death{' '}
<span className="font-normal text-gray-400">(optional)</span>
</label>
<input
id="death-cause"
type="text"
maxLength={100}
value={cause}
onChange={(e) => setCause(e.target.value)}
placeholder="e.g. Crit from rival's Charizard"
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-red-500"
/>
</div>
<div className="flex gap-3 pt-1">
<button
type="button"
onClick={() => setShowConfirm(false)}
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
Cancel
</button>
<button
type="button"
disabled={isPending}
onClick={handleConfirmDeath}
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isPending ? 'Saving...' : 'Confirm Death'}
</button>
</div>
</div>
)}
</div>
{/* Footer for dead/no-confirm views */}
{(isDead || (!isDead && !showConfirm)) && (
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
<button
type="button"
onClick={onClose}
className="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
Close
</button>
</div>
)}
</div>
</div>
)
}

View File

@@ -4,6 +4,7 @@ export { GameGrid } from './GameGrid'
export { Layout } from './Layout' export { Layout } from './Layout'
export { PokemonCard } from './PokemonCard' export { PokemonCard } from './PokemonCard'
export { RuleBadges } from './RuleBadges' export { RuleBadges } from './RuleBadges'
export { StatusChangeModal } from './StatusChangeModal'
export { RuleToggle } from './RuleToggle' export { RuleToggle } from './RuleToggle'
export { RulesConfiguration } from './RulesConfiguration' export { RulesConfiguration } from './RulesConfiguration'
export { StatCard } from './StatCard' export { StatCard } from './StatCard'

View File

@@ -1,8 +1,10 @@
import { useState } from 'react'
import { useParams, Link } from 'react-router-dom' import { useParams, Link } from 'react-router-dom'
import { useRun } from '../hooks/useRuns' import { useRun } from '../hooks/useRuns'
import { useGameRoutes } from '../hooks/useGames' import { useGameRoutes } from '../hooks/useGames'
import { StatCard, PokemonCard, RuleBadges } from '../components' import { useUpdateEncounter } from '../hooks/useEncounters'
import type { RunStatus } from '../types' import { StatCard, PokemonCard, RuleBadges, StatusChangeModal } from '../components'
import type { RunStatus, EncounterDetail } from '../types'
const statusStyles: Record<RunStatus, string> = { const statusStyles: Record<RunStatus, string> = {
active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300', active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300',
@@ -13,8 +15,12 @@ const statusStyles: Record<RunStatus, string> = {
export function RunDashboard() { export function RunDashboard() {
const { runId } = useParams<{ runId: string }>() const { runId } = useParams<{ runId: string }>()
const { data: run, isLoading, error } = useRun(Number(runId)) const runIdNum = Number(runId)
const { data: run, isLoading, error } = useRun(runIdNum)
const { data: routes } = useGameRoutes(run?.gameId ?? null) const { data: routes } = useGameRoutes(run?.gameId ?? null)
const updateEncounter = useUpdateEncounter(runIdNum)
const [selectedEncounter, setSelectedEncounter] =
useState<EncounterDetail | null>(null)
if (isLoading) { if (isLoading) {
return ( return (
@@ -118,7 +124,11 @@ export function RunDashboard() {
) : ( ) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{alive.map((enc) => ( {alive.map((enc) => (
<PokemonCard key={enc.id} encounter={enc} /> <PokemonCard
key={enc.id}
encounter={enc}
onClick={() => setSelectedEncounter(enc)}
/>
))} ))}
</div> </div>
)} )}
@@ -132,7 +142,12 @@ export function RunDashboard() {
</h2> </h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{dead.map((enc) => ( {dead.map((enc) => (
<PokemonCard key={enc.id} encounter={enc} showFaintLevel /> <PokemonCard
key={enc.id}
encounter={enc}
showFaintLevel
onClick={() => setSelectedEncounter(enc)}
/>
))} ))}
</div> </div>
</div> </div>
@@ -147,6 +162,20 @@ export function RunDashboard() {
Log Encounter Log Encounter
</Link> </Link>
</div> </div>
{/* Status Change Modal */}
{selectedEncounter && (
<StatusChangeModal
encounter={selectedEncounter}
onUpdate={(data) => {
updateEncounter.mutate(data, {
onSuccess: () => setSelectedEncounter(null),
})
}}
onClose={() => setSelectedEncounter(null)}
isPending={updateEncounter.isPending}
/>
)}
</div> </div>
) )
} }

View File

@@ -121,7 +121,12 @@ export function RunEncounters() {
const handleUpdate = (data: { const handleUpdate = (data: {
id: number id: number
data: { nickname?: string; status?: EncounterStatus; faintLevel?: number } data: {
nickname?: string
status?: EncounterStatus
faintLevel?: number
deathCause?: string
}
}) => { }) => {
updateEncounter.mutate(data, { updateEncounter.mutate(data, {
onSuccess: () => { onSuccess: () => {
@@ -225,7 +230,9 @@ export function RunEncounters() {
{encounter.nickname ?? encounter.pokemon.name} {encounter.nickname ?? encounter.pokemon.name}
{encounter.status === 'caught' && {encounter.status === 'caught' &&
encounter.faintLevel !== null && encounter.faintLevel !== null &&
' (dead)'} (encounter.deathCause
? `${encounter.deathCause}`
: ' (dead)')}
</span> </span>
</div> </div>
)} )}

View File

@@ -48,6 +48,7 @@ export interface Encounter {
status: EncounterStatus status: EncounterStatus
catchLevel: number | null catchLevel: number | null
faintLevel: number | null faintLevel: number | null
deathCause: string | null
caughtAt: string caughtAt: string
} }
@@ -97,6 +98,7 @@ export interface UpdateEncounterInput {
nickname?: string nickname?: string
status?: EncounterStatus status?: EncounterStatus
faintLevel?: number faintLevel?: number
deathCause?: string
} }
// Re-export for convenience // Re-export for convenience