Add game_id FK to BossBattle for version-exclusive bosses
Version-exclusive bosses (e.g., Bea in Sword, Allister in Shield) were using the section field to indicate which game they belong to. This adds a proper game_id foreign key so the API can filter bosses per game, keeping section free for visual grouping like "Main Story". - Alembic migration adds nullable game_id column with FK and index - API list_bosses filters by game_id unless ?all=true is passed - Seed data updated to use game_slug instead of section overloading - Admin form adds "Game (version exclusive)" dropdown - Export endpoints include game_slug for exclusive bosses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -107,8 +107,9 @@
|
||||
"order": 8,
|
||||
"after_route_name": null,
|
||||
"location": "Opelucid Gym",
|
||||
"section": "Black",
|
||||
"section": null,
|
||||
"sprite_url": "/boss-sprites/black/drayden.png",
|
||||
"game_slug": "black",
|
||||
"pokemon": []
|
||||
},
|
||||
{
|
||||
@@ -121,8 +122,9 @@
|
||||
"order": 9,
|
||||
"after_route_name": null,
|
||||
"location": "Opelucid Gym",
|
||||
"section": "White",
|
||||
"section": null,
|
||||
"sprite_url": "/boss-sprites/black/iris.png",
|
||||
"game_slug": "white",
|
||||
"pokemon": []
|
||||
},
|
||||
{
|
||||
|
||||
@@ -51,8 +51,9 @@
|
||||
"order": 4,
|
||||
"after_route_name": null,
|
||||
"location": "Stow-on-Side Stadium",
|
||||
"section": "Sword",
|
||||
"section": null,
|
||||
"sprite_url": "/boss-sprites/sword/bea.png",
|
||||
"game_slug": "sword",
|
||||
"pokemon": []
|
||||
},
|
||||
{
|
||||
@@ -65,8 +66,9 @@
|
||||
"order": 5,
|
||||
"after_route_name": null,
|
||||
"location": "Stow-on-Side Stadium",
|
||||
"section": "Shield",
|
||||
"section": null,
|
||||
"sprite_url": "/boss-sprites/sword/allister.png",
|
||||
"game_slug": "shield",
|
||||
"pokemon": []
|
||||
},
|
||||
{
|
||||
@@ -93,8 +95,9 @@
|
||||
"order": 7,
|
||||
"after_route_name": null,
|
||||
"location": "Circhester Stadium",
|
||||
"section": "Sword",
|
||||
"section": null,
|
||||
"sprite_url": "/boss-sprites/sword/gordie.png",
|
||||
"game_slug": "sword",
|
||||
"pokemon": []
|
||||
},
|
||||
{
|
||||
@@ -107,8 +110,9 @@
|
||||
"order": 8,
|
||||
"after_route_name": null,
|
||||
"location": "Circhester Stadium",
|
||||
"section": "Shield",
|
||||
"section": null,
|
||||
"sprite_url": "/boss-sprites/sword/melony.png",
|
||||
"game_slug": "shield",
|
||||
"pokemon": []
|
||||
},
|
||||
{
|
||||
|
||||
@@ -239,6 +239,7 @@ async def upsert_bosses(
|
||||
bosses: list[dict],
|
||||
dex_to_id: dict[int, int],
|
||||
route_name_to_id: dict[str, int] | None = None,
|
||||
slug_to_game_id: dict[str, int] | None = None,
|
||||
) -> int:
|
||||
"""Upsert boss battles for a version group, return count of bosses upserted."""
|
||||
count = 0
|
||||
@@ -253,6 +254,16 @@ async def upsert_bosses(
|
||||
f" Warning: route '{after_route_name}' not found for boss '{boss['name']}'"
|
||||
)
|
||||
|
||||
# Resolve game_slug to game_id
|
||||
game_id = None
|
||||
game_slug = boss.get("game_slug")
|
||||
if game_slug and slug_to_game_id:
|
||||
game_id = slug_to_game_id.get(game_slug)
|
||||
if game_id is None:
|
||||
print(
|
||||
f" Warning: game '{game_slug}' not found for boss '{boss['name']}'"
|
||||
)
|
||||
|
||||
# Upsert the boss battle on (version_group_id, order) conflict
|
||||
stmt = (
|
||||
insert(BossBattle)
|
||||
@@ -269,6 +280,7 @@ async def upsert_bosses(
|
||||
location=boss["location"],
|
||||
section=boss.get("section"),
|
||||
sprite_url=boss.get("sprite_url"),
|
||||
game_id=game_id,
|
||||
)
|
||||
.on_conflict_do_update(
|
||||
constraint="uq_boss_battles_version_group_order",
|
||||
@@ -283,6 +295,7 @@ async def upsert_bosses(
|
||||
"location": boss["location"],
|
||||
"section": boss.get("section"),
|
||||
"sprite_url": boss.get("sprite_url"),
|
||||
"game_id": game_id,
|
||||
},
|
||||
)
|
||||
.returning(BossBattle.id)
|
||||
|
||||
@@ -160,7 +160,7 @@ async def seed():
|
||||
|
||||
route_name_to_id = route_maps_by_vg.get(vg_id, {})
|
||||
boss_count = await upsert_bosses(
|
||||
session, vg_id, bosses_data, dex_to_id, route_name_to_id
|
||||
session, vg_id, bosses_data, dex_to_id, route_name_to_id, slug_to_id
|
||||
)
|
||||
total_bosses += boss_count
|
||||
print(f" {vg_slug}: {boss_count} bosses")
|
||||
@@ -491,6 +491,7 @@ async def _export_bosses(session: AsyncSession, vg_data: dict):
|
||||
.options(
|
||||
selectinload(BossBattle.pokemon).selectinload(BossPokemon.pokemon),
|
||||
selectinload(BossBattle.after_route),
|
||||
selectinload(BossBattle.game),
|
||||
)
|
||||
.order_by(BossBattle.order)
|
||||
)
|
||||
@@ -525,30 +526,31 @@ async def _export_bosses(session: AsyncSession, vg_data: dict):
|
||||
downloaded_sprites,
|
||||
)
|
||||
|
||||
data.append(
|
||||
{
|
||||
"name": b.name,
|
||||
"boss_type": b.boss_type,
|
||||
"specialty_type": b.specialty_type,
|
||||
"badge_name": b.badge_name,
|
||||
"badge_image_url": badge_image_url,
|
||||
"level_cap": b.level_cap,
|
||||
"order": b.order,
|
||||
"after_route_name": b.after_route.name if b.after_route else None,
|
||||
"location": b.location,
|
||||
"section": b.section,
|
||||
"sprite_url": sprite_url,
|
||||
"pokemon": [
|
||||
{
|
||||
"pokeapi_id": bp.pokemon.pokeapi_id,
|
||||
"pokemon_name": bp.pokemon.name,
|
||||
"level": bp.level,
|
||||
"order": bp.order,
|
||||
}
|
||||
for bp in sorted(b.pokemon, key=lambda p: p.order)
|
||||
],
|
||||
}
|
||||
)
|
||||
boss_dict: dict = {
|
||||
"name": b.name,
|
||||
"boss_type": b.boss_type,
|
||||
"specialty_type": b.specialty_type,
|
||||
"badge_name": b.badge_name,
|
||||
"badge_image_url": badge_image_url,
|
||||
"level_cap": b.level_cap,
|
||||
"order": b.order,
|
||||
"after_route_name": b.after_route.name if b.after_route else None,
|
||||
"location": b.location,
|
||||
"section": b.section,
|
||||
"sprite_url": sprite_url,
|
||||
"pokemon": [
|
||||
{
|
||||
"pokeapi_id": bp.pokemon.pokeapi_id,
|
||||
"pokemon_name": bp.pokemon.name,
|
||||
"level": bp.level,
|
||||
"order": bp.order,
|
||||
}
|
||||
for bp in sorted(b.pokemon, key=lambda p: p.order)
|
||||
],
|
||||
}
|
||||
if b.game_id:
|
||||
boss_dict["game_slug"] = b.game.slug
|
||||
data.append(boss_dict)
|
||||
|
||||
_write_json(f"{first_game_slug}-bosses.json", data)
|
||||
exported += 1
|
||||
|
||||
Reference in New Issue
Block a user