When a run has a starter Pokemon, automatically match its species name against boss battle condition labels (e.g., "charmander" matches "Chose Charmander"). If exactly one variant matches, pre-select it and hide the pill selector. Falls back to showing pills when no match is found. Fixes starter lookup to use game routes data (which has encounterMethods populated) instead of the run detail route (which defaults to empty). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
174 lines
6.8 KiB
TypeScript
174 lines
6.8 KiB
TypeScript
import { type FormEvent, useState, useMemo } from 'react'
|
|
import type { BossBattle, CreateBossResultInput } from '../types/game'
|
|
|
|
interface BossDefeatModalProps {
|
|
boss: BossBattle
|
|
onSubmit: (data: CreateBossResultInput) => void
|
|
onClose: () => void
|
|
isPending?: boolean
|
|
hardcoreMode?: boolean
|
|
starterName?: string | null
|
|
}
|
|
|
|
function matchVariant(labels: string[], starterName?: string | null): string | null {
|
|
if (!starterName || labels.length === 0) return null
|
|
const lower = starterName.toLowerCase()
|
|
const matches = labels.filter((l) => l.toLowerCase().includes(lower))
|
|
return matches.length === 1 ? matches[0] : null
|
|
}
|
|
|
|
export function BossDefeatModal({ boss, onSubmit, onClose, isPending, hardcoreMode, starterName }: BossDefeatModalProps) {
|
|
const [result, setResult] = useState<'won' | 'lost'>('won')
|
|
const [attempts, setAttempts] = useState('1')
|
|
|
|
const variantLabels = useMemo(() => {
|
|
const labels = new Set<string>()
|
|
for (const bp of boss.pokemon) {
|
|
if (bp.conditionLabel) labels.add(bp.conditionLabel)
|
|
}
|
|
return [...labels].sort()
|
|
}, [boss.pokemon])
|
|
|
|
const hasVariants = variantLabels.length > 0
|
|
const autoMatch = useMemo(() => matchVariant(variantLabels, starterName), [variantLabels, starterName])
|
|
const showPills = hasVariants && autoMatch === null
|
|
const [selectedVariant, setSelectedVariant] = useState<string | null>(
|
|
autoMatch ?? (hasVariants ? variantLabels[0] : null),
|
|
)
|
|
|
|
const displayedPokemon = useMemo(() => {
|
|
if (!hasVariants) return boss.pokemon
|
|
return boss.pokemon.filter(
|
|
(bp) => bp.conditionLabel === selectedVariant || bp.conditionLabel === null,
|
|
)
|
|
}, [boss.pokemon, hasVariants, selectedVariant])
|
|
|
|
const handleSubmit = (e: FormEvent) => {
|
|
e.preventDefault()
|
|
onSubmit({
|
|
bossBattleId: boss.id,
|
|
result: hardcoreMode ? 'won' : result,
|
|
attempts: hardcoreMode ? 1 : Number(attempts) || 1,
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
|
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4">
|
|
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
<h2 className="text-lg font-semibold">Battle: {boss.name}</h2>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">{boss.location}</p>
|
|
</div>
|
|
|
|
{/* Boss team preview */}
|
|
{boss.pokemon.length > 0 && (
|
|
<div className="px-6 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
{showPills && (
|
|
<div className="flex gap-1 mb-2 flex-wrap">
|
|
{variantLabels.map((label) => (
|
|
<button
|
|
key={label}
|
|
type="button"
|
|
onClick={() => setSelectedVariant(label)}
|
|
className={`px-2 py-0.5 text-xs font-medium rounded-full transition-colors ${
|
|
selectedVariant === label
|
|
? 'bg-blue-600 text-white'
|
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
|
}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
<div className="flex flex-wrap gap-3">
|
|
{[...displayedPokemon]
|
|
.sort((a, b) => a.order - b.order)
|
|
.map((bp) => (
|
|
<div key={bp.id} className="flex flex-col items-center">
|
|
{bp.pokemon.spriteUrl ? (
|
|
<img src={bp.pokemon.spriteUrl} alt={bp.pokemon.name} className="w-10 h-10" />
|
|
) : (
|
|
<div className="w-10 h-10 bg-gray-200 dark:bg-gray-700 rounded-full" />
|
|
)}
|
|
<span className="text-xs text-gray-500 dark:text-gray-400 capitalize">
|
|
{bp.pokemon.name}
|
|
</span>
|
|
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
|
Lv.{bp.level}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="px-6 py-4 space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Result</label>
|
|
<div className="flex gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setResult('won')}
|
|
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md border transition-colors ${
|
|
result === 'won'
|
|
? 'bg-green-600 text-white border-green-600'
|
|
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
Won
|
|
</button>
|
|
{!hardcoreMode && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setResult('lost')}
|
|
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md border transition-colors ${
|
|
result === 'lost'
|
|
? 'bg-red-600 text-white border-red-600'
|
|
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
Lost
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{!hardcoreMode && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">Attempts</label>
|
|
<input
|
|
type="number"
|
|
min={1}
|
|
value={attempts}
|
|
onChange={(e) => setAttempts(e.target.value)}
|
|
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isPending}
|
|
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
{isPending ? 'Saving...' : 'Save Result'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|