From 1bf37a6bd94c834a63530a69535e3e574e259868 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sun, 8 Feb 2026 14:58:49 +0100 Subject: [PATCH] Add drag-and-drop boss reordering and new feature beans Adds boss battle reorder API endpoint with two-phase order update to avoid unique constraint violations. Includes frontend mutation hook and API client. Also adds draft beans for progression dividers and conditional boss battle teams features. Co-Authored-By: Claude Opus 4.6 --- ...progression-dividers-main-story-endgame.md | 28 +++++++++++++ ...ag-and-drop-reordering-for-boss-battles.md | 5 ++- ...ol--starter-dependent-boss-battle-teams.md | 31 ++++++++++++++ backend/src/app/api/bosses.py | 40 +++++++++++++++++++ frontend/src/api/admin.ts | 4 ++ frontend/src/hooks/useAdmin.ts | 13 ++++++ 6 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 .beans/nuzlocke-tracker-3el1--run-progression-dividers-main-story-endgame.md create mode 100644 .beans/nuzlocke-tracker-x8ol--starter-dependent-boss-battle-teams.md diff --git a/.beans/nuzlocke-tracker-3el1--run-progression-dividers-main-story-endgame.md b/.beans/nuzlocke-tracker-3el1--run-progression-dividers-main-story-endgame.md new file mode 100644 index 0000000..7bfe635 --- /dev/null +++ b/.beans/nuzlocke-tracker-3el1--run-progression-dividers-main-story-endgame.md @@ -0,0 +1,28 @@ +--- +# nuzlocke-tracker-3el1 +title: Run progression dividers (main story / endgame) +status: draft +type: feature +created_at: 2026-02-08T13:40:14Z +updated_at: 2026-02-08T13:40:14Z +--- + +Add support for dividing a run's boss battle progression into sections like "Main Story" and "Endgame" (e.g., post-Elite Four content). This helps players visually distinguish where the main campaign ends and optional/endgame content begins. + +## Context + +Currently boss battles are displayed as a flat ordered list. In many Pokemon games there's a clear distinction between the main story (up through the Champion) and endgame content (rematches, Battle Frontier, Kanto in GSC/HGSS, etc.). A visual divider would make it easier to track progress through each phase. + +## Scope + +- **Admin side**: Allow marking boss battles or defining breakpoints that separate progression phases (e.g., "everything after this boss is endgame") +- **Run side**: Render a visual divider/section header between main story and endgame boss battles +- Should support at minimum two sections (main story, endgame), but consider whether the design should be flexible enough for arbitrary sections (e.g., "Kanto" in HGSS) + +## Checklist + +- [ ] Decide on data model approach (e.g., a `section` field on boss battles, or a separate progression divider entity tied to the version group) +- [ ] Add backend models and migrations +- [ ] Add API support for managing sections/dividers +- [ ] Update admin UI to allow assigning bosses to sections or inserting dividers +- [ ] Update run-side boss progression display to render section headers/dividers \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-9cx2--drag-and-drop-reordering-for-boss-battles.md b/.beans/nuzlocke-tracker-9cx2--drag-and-drop-reordering-for-boss-battles.md index 40ccf8b..d2d6e8e 100644 --- a/.beans/nuzlocke-tracker-9cx2--drag-and-drop-reordering-for-boss-battles.md +++ b/.beans/nuzlocke-tracker-9cx2--drag-and-drop-reordering-for-boss-battles.md @@ -1,10 +1,11 @@ --- # nuzlocke-tracker-9cx2 title: Drag-and-drop reordering for boss battles -status: todo +status: completed type: feature +priority: normal created_at: 2026-02-08T12:33:18Z -updated_at: 2026-02-08T12:33:18Z +updated_at: 2026-02-08T13:11:50Z parent: nuzlocke-tracker-iu5b --- diff --git a/.beans/nuzlocke-tracker-x8ol--starter-dependent-boss-battle-teams.md b/.beans/nuzlocke-tracker-x8ol--starter-dependent-boss-battle-teams.md new file mode 100644 index 0000000..9264491 --- /dev/null +++ b/.beans/nuzlocke-tracker-x8ol--starter-dependent-boss-battle-teams.md @@ -0,0 +1,31 @@ +--- +# nuzlocke-tracker-x8ol +title: Conditional boss battle teams +status: draft +type: feature +priority: normal +created_at: 2026-02-08T13:23:00Z +updated_at: 2026-02-08T13:29:26Z +--- + +Some boss battles have teams that vary based on conditions in the player's run. The most common case is starter choice (e.g., Blue's team in Gen 1 depends on whether you picked Bulbasaur, Charmander, or Squirtle), but other conditions exist too — in Pokemon Yellow, the rival's team changes based on the outcomes of two early-game fights, not the starter. This feature adds support for defining multiple team variants per boss battle, each associated with a named condition. + +## Context + +Currently each boss battle has a single fixed team (`boss_pokemon` table). This doesn't account for games where the rival/champion adapts their team based on player decisions or battle outcomes. To accurately model these encounters, boss battles need to support variant teams keyed by a general condition system — not just starter choice. + +## Scope + +- **Variant conditions**: Support a general condition system for team variants. Starter choice is the most common condition, but the design must also handle arbitrary conditions (e.g., "won/lost early rival fight" in Yellow). Each variant should have a human-readable label describing the condition. +- **Admin side**: Allow defining multiple team variants per boss battle, each with a condition label and a team composition +- **Run side**: When viewing a boss battle during a run, allow the player to select which variant applies (or auto-resolve when possible, e.g., from the run's recorded starter) +- **Fallback**: If no variant teams are defined, the boss uses the existing single team as today + +## Checklist + +- [ ] Design database schema for conditional team variants (e.g., a `boss_team_variant` table grouping pokemon by a condition label, rather than tying directly to starter) +- [ ] Add backend models and migrations +- [ ] Add API endpoints for managing team variants per boss battle +- [ ] Update admin UI (BossTeamEditor) to support defining teams per condition/variant +- [ ] Update run-side boss display to let the player pick or auto-resolve the correct variant +- [ ] Handle edge cases: boss battles with no variants (use default team), unknown conditions \ No newline at end of file diff --git a/backend/src/app/api/bosses.py b/backend/src/app/api/bosses.py index 7d5921b..5af567e 100644 --- a/backend/src/app/api/bosses.py +++ b/backend/src/app/api/bosses.py @@ -16,6 +16,7 @@ from app.schemas.boss import ( BossBattleResponse, BossBattleUpdate, BossPokemonInput, + BossReorderRequest, BossResultCreate, BossResultResponse, ) @@ -50,6 +51,45 @@ async def list_bosses( return result.scalars().all() +@router.put("/games/{game_id}/bosses/reorder", response_model=list[BossBattleResponse]) +async def reorder_bosses( + game_id: int, + data: BossReorderRequest, + session: AsyncSession = Depends(get_session), +): + vg_id = await _get_version_group_id(session, game_id) + + boss_ids = [item.id for item in data.bosses] + result = await session.execute( + select(BossBattle).where( + BossBattle.id.in_(boss_ids), BossBattle.version_group_id == vg_id + ) + ) + bosses = {b.id: b for b in result.scalars().all()} + + if len(bosses) != len(boss_ids): + raise HTTPException(status_code=400, detail="Some boss IDs not found in this game") + + # Phase 1: set temporary negative orders to avoid unique constraint violations + for i, item in enumerate(data.bosses): + bosses[item.id].order = -(i + 1) + await session.flush() + + # Phase 2: set real orders + for item in data.bosses: + bosses[item.id].order = item.order + await session.commit() + + # Re-fetch with eager loading + result = await session.execute( + select(BossBattle) + .where(BossBattle.version_group_id == vg_id) + .options(selectinload(BossBattle.pokemon).selectinload(BossPokemon.pokemon)) + .order_by(BossBattle.order) + ) + return result.scalars().all() + + @router.post("/games/{game_id}/bosses", response_model=BossBattleResponse, status_code=201) async def create_boss( game_id: int, diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index 82f7291..cdb7091 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -23,6 +23,7 @@ import type { CreateBossBattleInput, UpdateBossBattleInput, BossPokemonInput, + BossReorderItem, } from '../types' // Games @@ -123,5 +124,8 @@ export const updateBossBattle = (gameId: number, bossId: number, data: UpdateBos export const deleteBossBattle = (gameId: number, bossId: number) => api.del(`/games/${gameId}/bosses/${bossId}`) +export const reorderBosses = (gameId: number, bosses: BossReorderItem[]) => + api.put(`/games/${gameId}/bosses/reorder`, { bosses }) + export const setBossTeam = (gameId: number, bossId: number, team: BossPokemonInput[]) => api.put(`/games/${gameId}/bosses/${bossId}/pokemon`, team) diff --git a/frontend/src/hooks/useAdmin.ts b/frontend/src/hooks/useAdmin.ts index 70bf448..69d63db 100644 --- a/frontend/src/hooks/useAdmin.ts +++ b/frontend/src/hooks/useAdmin.ts @@ -16,6 +16,7 @@ import type { CreateBossBattleInput, UpdateBossBattleInput, BossPokemonInput, + BossReorderItem, } from '../types' // --- Queries --- @@ -287,6 +288,18 @@ export function useUpdateBossBattle(gameId: number) { }) } +export function useReorderBosses(gameId: number) { + const qc = useQueryClient() + return useMutation({ + mutationFn: (bosses: BossReorderItem[]) => adminApi.reorderBosses(gameId, bosses), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['games', gameId, 'bosses'] }) + toast.success('Bosses reordered') + }, + onError: (err) => toast.error(`Failed to reorder bosses: ${err.message}`), + }) +} + export function useDeleteBossBattle(gameId: number) { const qc = useQueryClient() return useMutation({