Add conditional boss battle teams (variant teams by condition)

Wire up the existing condition_label column on boss_pokemon to support
variant teams throughout the UI. Boss battles can now have multiple team
configurations based on conditions (e.g., starter choice in Gen 1).

- Add condition_label to BossPokemonInput schema (frontend + backend bulk import)
- Rewrite BossTeamEditor with variant tabs (Default + named conditions)
- Add variant pill selector to BossDefeatModal team preview
- Add BossTeamPreview component to RunEncounters boss cards
- Fix MissingGreenlet error in set_boss_team via session.expunge_all()
- Fix PokemonSelector state bleed between tabs via composite React key
- Add Alembic migration for condition_label column

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 21:20:30 +01:00
parent 8931424ef4
commit a6bf8b4af2
14 changed files with 309 additions and 52 deletions

View File

@@ -1,4 +1,4 @@
import { type FormEvent, useState } from 'react'
import { type FormEvent, useState, useMemo } from 'react'
import type { BossBattle, CreateBossResultInput } from '../types/game'
interface BossDefeatModalProps {
@@ -13,6 +13,26 @@ export function BossDefeatModal({ boss, onSubmit, onClose, isPending, hardcoreMo
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 [selectedVariant, setSelectedVariant] = useState<string | null>(
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({
@@ -34,8 +54,26 @@ export function BossDefeatModal({ boss, onSubmit, onClose, isPending, hardcoreMo
{/* Boss team preview */}
{boss.pokemon.length > 0 && (
<div className="px-6 py-3 border-b border-gray-200 dark:border-gray-700">
{hasVariants && (
<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">
{boss.pokemon
{[...displayedPokemon]
.sort((a, b) => a.order - b.order)
.map((bp) => (
<div key={bp.id} className="flex flex-col items-center">