Add Hall of Fame team selection for completed runs

After marking a run as completed, a modal prompts the player to select
which Pokemon (up to 6) entered the Hall of Fame. The selection is stored
as hof_encounter_ids on the run, displayed in the victory banner, and
can be edited later. This lays the foundation for scoping genlocke
retireHoF to only the actual HoF team.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Julian Tabel
2026-02-09 10:19:56 +01:00
parent 89f46e2b12
commit 08a5e5c621
9 changed files with 266 additions and 10 deletions

View File

@@ -0,0 +1,30 @@
"""add hof_encounter_ids to nuzlocke_runs
Revision ID: d4e5f6a7b9c0
Revises: c3d4e5f6a7b9
Create Date: 2026-02-09 20:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
# revision identifiers, used by Alembic.
revision: str = 'd4e5f6a7b9c0'
down_revision: Union[str, Sequence[str], None] = 'c3d4e5f6a7b9'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
'nuzlocke_runs',
sa.Column('hof_encounter_ids', JSONB(), nullable=True),
)
def downgrade() -> None:
op.drop_column('nuzlocke_runs', 'hof_encounter_ids')

View File

@@ -120,6 +120,38 @@ async def update_run(
update_data = data.model_dump(exclude_unset=True)
# Validate hof_encounter_ids if provided
if "hof_encounter_ids" in update_data and update_data["hof_encounter_ids"] is not None:
hof_ids = update_data["hof_encounter_ids"]
if len(hof_ids) > 6:
raise HTTPException(
status_code=400, detail="HoF team cannot have more than 6 Pokemon"
)
if hof_ids:
# Validate all encounter IDs belong to this run and are alive
enc_result = await session.execute(
select(Encounter).where(
Encounter.id.in_(hof_ids),
Encounter.run_id == run_id,
)
)
found = {e.id: e for e in enc_result.scalars().all()}
missing = [eid for eid in hof_ids if eid not in found]
if missing:
raise HTTPException(
status_code=400,
detail=f"Encounters not found in this run: {missing}",
)
not_alive = [
eid for eid, e in found.items()
if e.status != "caught" or e.faint_level is not None
]
if not_alive:
raise HTTPException(
status_code=400,
detail=f"Encounters are not alive: {not_alive}",
)
# Auto-set completed_at when ending a run
if "status" in update_data and update_data["status"] in ("completed", "failed"):
if run.status != "active":

View File

@@ -19,6 +19,7 @@ class NuzlockeRun(Base):
DateTime(timezone=True), server_default=func.now()
)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
hof_encounter_ids: Mapped[list[int] | None] = mapped_column(JSONB, default=None)
game: Mapped["Game"] = relationship(back_populates="runs")
encounters: Mapped[list["Encounter"]] = relationship(back_populates="run")

View File

@@ -15,6 +15,7 @@ class RunUpdate(CamelModel):
name: str | None = None
status: str | None = None
rules: dict | None = None
hof_encounter_ids: list[int] | None = None
class RunResponse(CamelModel):
@@ -23,6 +24,7 @@ class RunResponse(CamelModel):
name: str
status: str
rules: dict
hof_encounter_ids: list[int] | None = None
started_at: datetime
completed_at: datetime | None