Add hierarchical route grouping for multi-area locations
Locations like Mt. Moon (with 1F, B1F, B2F floors) are now grouped so only one encounter can be logged per location group, enforcing Nuzlocke first-encounter rules correctly. - Add parent_route_id column with self-referential FK to routes table - Add parent/children relationships on Route model - Update games API to return hierarchical route structure - Add validation in encounters API to prevent parent route encounters and duplicate encounters within sibling routes (409 conflict) - Update frontend with collapsible RouteGroup component - Auto-derive route groups from PokeAPI location/location-area structure - Regenerate seed data with 70 parent routes and 315 child routes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
|
||||
from app.core.database import get_session
|
||||
from app.models.encounter import Encounter
|
||||
@@ -33,11 +33,45 @@ async def create_encounter(
|
||||
if run is None:
|
||||
raise HTTPException(status_code=404, detail="Run not found")
|
||||
|
||||
# Validate route exists
|
||||
route = await session.get(Route, data.route_id)
|
||||
# Validate route exists and load its children
|
||||
result = await session.execute(
|
||||
select(Route)
|
||||
.where(Route.id == data.route_id)
|
||||
.options(selectinload(Route.children))
|
||||
)
|
||||
route = result.scalar_one_or_none()
|
||||
if route is None:
|
||||
raise HTTPException(status_code=404, detail="Route not found")
|
||||
|
||||
# Cannot create encounter on a parent route (routes with children)
|
||||
if route.children:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot create encounter on a parent route. Use a child route instead.",
|
||||
)
|
||||
|
||||
# If this route has a parent, check if any sibling already has an encounter
|
||||
if route.parent_route_id is not None:
|
||||
# Get all sibling route IDs (routes with same parent, including this one)
|
||||
siblings_result = await session.execute(
|
||||
select(Route.id).where(Route.parent_route_id == route.parent_route_id)
|
||||
)
|
||||
sibling_ids = [r for r in siblings_result.scalars().all()]
|
||||
|
||||
# Check if any sibling already has an encounter in this run
|
||||
existing_encounter = await session.execute(
|
||||
select(Encounter)
|
||||
.where(
|
||||
Encounter.run_id == run_id,
|
||||
Encounter.route_id.in_(sibling_ids),
|
||||
)
|
||||
)
|
||||
if existing_encounter.scalar_one_or_none() is not None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="This location group already has an encounter. Only one encounter per location group is allowed.",
|
||||
)
|
||||
|
||||
# Validate pokemon exists
|
||||
pokemon = await session.get(Pokemon, data.pokemon_id)
|
||||
if pokemon is None:
|
||||
|
||||
Reference in New Issue
Block a user