Add genlocke leg progression with advance endpoint and run context

When a run belonging to a genlocke is completed or failed, the genlocke
status updates accordingly. The run detail API now includes genlocke
context (leg order, total legs, genlocke name). A new advance endpoint
creates the next leg's run, and the frontend shows genlocke-aware UI
including a "Leg X of Y" banner, advance button, and contextual
messaging in the end-run modal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Julian Tabel
2026-02-09 09:47:28 +01:00
parent 96178622f9
commit 07343e94e2
11 changed files with 271 additions and 54 deletions

View File

@@ -1,15 +1,16 @@
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Response
from sqlalchemy import select
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, selectinload
from app.core.database import get_session
from app.models.encounter import Encounter
from app.models.game import Game
from app.models.genlocke import Genlocke, GenlockeLeg
from app.models.nuzlocke_run import NuzlockeRun
from app.schemas.run import RunCreate, RunDetailResponse, RunResponse, RunUpdate
from app.schemas.run import RunCreate, RunDetailResponse, RunGenlockeContext, RunResponse, RunUpdate
router = APIRouter()
@@ -61,7 +62,33 @@ async def get_run(run_id: int, session: AsyncSession = Depends(get_session)):
run = result.scalar_one_or_none()
if run is None:
raise HTTPException(status_code=404, detail="Run not found")
return run
# Check if this run belongs to a genlocke
genlocke_context = None
leg_result = await session.execute(
select(GenlockeLeg)
.where(GenlockeLeg.run_id == run_id)
.options(joinedload(GenlockeLeg.genlocke))
)
leg = leg_result.scalar_one_or_none()
if leg:
total_legs_result = await session.execute(
select(func.count())
.select_from(GenlockeLeg)
.where(GenlockeLeg.genlocke_id == leg.genlocke_id)
)
total_legs = total_legs_result.scalar_one()
genlocke_context = RunGenlockeContext(
genlocke_id=leg.genlocke_id,
genlocke_name=leg.genlocke.name,
leg_order=leg.leg_order,
total_legs=total_legs,
is_final_leg=leg.leg_order == total_legs,
)
response = RunDetailResponse.model_validate(run)
response.genlocke = genlocke_context
return response
@router.patch("/{run_id}", response_model=RunResponse)
@@ -87,6 +114,28 @@ async def update_run(
for field, value in update_data.items():
setattr(run, field, value)
# Genlocke side effects when run status changes
if "status" in update_data and update_data["status"] in ("completed", "failed"):
leg_result = await session.execute(
select(GenlockeLeg)
.where(GenlockeLeg.run_id == run_id)
.options(joinedload(GenlockeLeg.genlocke))
)
leg = leg_result.scalar_one_or_none()
if leg:
genlocke = leg.genlocke
if update_data["status"] == "failed":
genlocke.status = "failed"
elif update_data["status"] == "completed":
total_legs_result = await session.execute(
select(func.count())
.select_from(GenlockeLeg)
.where(GenlockeLeg.genlocke_id == genlocke.id)
)
total_legs = total_legs_result.scalar_one()
if leg.leg_order == total_legs:
genlocke.status = "completed"
await session.commit()
await session.refresh(run)
return run