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:
@@ -79,3 +79,86 @@ async def create_genlocke(
|
||||
)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{genlocke_id}/legs/{leg_order}/advance",
|
||||
response_model=GenlockeResponse,
|
||||
)
|
||||
async def advance_leg(
|
||||
genlocke_id: int,
|
||||
leg_order: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
# Load genlocke with legs
|
||||
result = await session.execute(
|
||||
select(Genlocke)
|
||||
.where(Genlocke.id == genlocke_id)
|
||||
.options(
|
||||
selectinload(Genlocke.legs).selectinload(GenlockeLeg.game),
|
||||
)
|
||||
)
|
||||
genlocke = result.scalar_one_or_none()
|
||||
if genlocke is None:
|
||||
raise HTTPException(status_code=404, detail="Genlocke not found")
|
||||
|
||||
if genlocke.status != "active":
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Genlocke is not active"
|
||||
)
|
||||
|
||||
# Find the current leg
|
||||
current_leg = None
|
||||
next_leg = None
|
||||
for leg in genlocke.legs:
|
||||
if leg.leg_order == leg_order:
|
||||
current_leg = leg
|
||||
elif leg.leg_order == leg_order + 1:
|
||||
next_leg = leg
|
||||
|
||||
if current_leg is None:
|
||||
raise HTTPException(status_code=404, detail="Leg not found")
|
||||
|
||||
# Verify current leg's run is completed
|
||||
if current_leg.run_id is None:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Current leg has no run"
|
||||
)
|
||||
current_run = await session.get(NuzlockeRun, current_leg.run_id)
|
||||
if current_run is None or current_run.status != "completed":
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Current leg's run is not completed"
|
||||
)
|
||||
|
||||
if next_leg is None:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="No next leg to advance to"
|
||||
)
|
||||
|
||||
if next_leg.run_id is not None:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Next leg already has a run"
|
||||
)
|
||||
|
||||
# Create a new run for the next leg
|
||||
new_run = NuzlockeRun(
|
||||
game_id=next_leg.game_id,
|
||||
name=f"{genlocke.name} \u2014 Leg {next_leg.leg_order}",
|
||||
status="active",
|
||||
rules=genlocke.nuzlocke_rules,
|
||||
)
|
||||
session.add(new_run)
|
||||
await session.flush()
|
||||
|
||||
next_leg.run_id = new_run.id
|
||||
await session.commit()
|
||||
|
||||
# Reload with relationships
|
||||
result = await session.execute(
|
||||
select(Genlocke)
|
||||
.where(Genlocke.id == genlocke_id)
|
||||
.options(
|
||||
selectinload(Genlocke.legs).selectinload(GenlockeLeg.game),
|
||||
)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,7 +37,7 @@ from app.schemas.pokemon import (
|
||||
RouteEncounterResponse,
|
||||
RouteEncounterUpdate,
|
||||
)
|
||||
from app.schemas.run import RunCreate, RunDetailResponse, RunResponse, RunUpdate
|
||||
from app.schemas.run import RunCreate, RunDetailResponse, RunGenlockeContext, RunResponse, RunUpdate
|
||||
|
||||
__all__ = [
|
||||
"BossBattleCreate",
|
||||
@@ -75,6 +75,7 @@ __all__ = [
|
||||
"RouteUpdate",
|
||||
"RunCreate",
|
||||
"RunDetailResponse",
|
||||
"RunGenlockeContext",
|
||||
"RunResponse",
|
||||
"RunUpdate",
|
||||
]
|
||||
|
||||
@@ -27,6 +27,15 @@ class RunResponse(CamelModel):
|
||||
completed_at: datetime | None
|
||||
|
||||
|
||||
class RunGenlockeContext(CamelModel):
|
||||
genlocke_id: int
|
||||
genlocke_name: str
|
||||
leg_order: int
|
||||
total_legs: int
|
||||
is_final_leg: bool
|
||||
|
||||
|
||||
class RunDetailResponse(RunResponse):
|
||||
game: GameResponse
|
||||
encounters: list[EncounterDetailResponse] = []
|
||||
genlocke: RunGenlockeContext | None = None
|
||||
|
||||
Reference in New Issue
Block a user