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

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

View File

@@ -228,11 +228,17 @@ async def set_boss_team(
pokemon_id=item.pokemon_id,
level=item.level,
order=item.order,
condition_label=item.condition_label,
)
session.add(bp)
await session.commit()
# Clear identity map so selectinload fetches everything fresh
# (expired Pokemon from deleted BossPokemon would otherwise cause
# MissingGreenlet errors during response serialization)
session.expunge_all()
# Re-fetch with eager loading
result = await session.execute(
select(BossBattle)

View File

@@ -154,6 +154,7 @@ async def export_game_bosses(
"pokemon_name": bp.pokemon.name,
"level": bp.level,
"order": bp.order,
**({"condition_label": bp.condition_label} if bp.condition_label else {}),
}
for bp in sorted(b.pokemon, key=lambda p: p.order)
],

View File

@@ -1,4 +1,4 @@
from sqlalchemy import ForeignKey, SmallInteger
from sqlalchemy import ForeignKey, SmallInteger, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
@@ -14,6 +14,7 @@ class BossPokemon(Base):
pokemon_id: Mapped[int] = mapped_column(ForeignKey("pokemon.id"), index=True)
level: Mapped[int] = mapped_column(SmallInteger)
order: Mapped[int] = mapped_column(SmallInteger)
condition_label: Mapped[str | None] = mapped_column(String(100))
boss_battle: Mapped["BossBattle"] = relationship(back_populates="pokemon")
pokemon: Mapped["Pokemon"] = relationship()

View File

@@ -9,6 +9,7 @@ class BossPokemonResponse(CamelModel):
pokemon_id: int
level: int
order: int
condition_label: str | None
pokemon: PokemonResponse
@@ -73,6 +74,7 @@ class BossPokemonInput(CamelModel):
pokemon_id: int
level: int
order: int
condition_label: str | None = None
class BossResultCreate(CamelModel):

View File

@@ -200,6 +200,7 @@ class BulkBossPokemonItem(BaseModel):
pokeapi_id: int
level: int
order: int
condition_label: str | None = None
class BulkBossItem(BaseModel):

View File

@@ -270,6 +270,7 @@ async def upsert_bosses(
pokemon_id=pokemon_id,
level=bp["level"],
order=bp["order"],
condition_label=bp.get("condition_label"),
))
count += 1