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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
# nuzlocke-tracker-9cx2
|
# nuzlocke-tracker-9cx2
|
||||||
title: Drag-and-drop reordering for boss battles
|
title: Drag-and-drop reordering for boss battles
|
||||||
status: todo
|
status: completed
|
||||||
type: feature
|
type: feature
|
||||||
|
priority: normal
|
||||||
created_at: 2026-02-08T12:33:18Z
|
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
|
parent: nuzlocke-tracker-iu5b
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -16,6 +16,7 @@ from app.schemas.boss import (
|
|||||||
BossBattleResponse,
|
BossBattleResponse,
|
||||||
BossBattleUpdate,
|
BossBattleUpdate,
|
||||||
BossPokemonInput,
|
BossPokemonInput,
|
||||||
|
BossReorderRequest,
|
||||||
BossResultCreate,
|
BossResultCreate,
|
||||||
BossResultResponse,
|
BossResultResponse,
|
||||||
)
|
)
|
||||||
@@ -50,6 +51,45 @@ async def list_bosses(
|
|||||||
return result.scalars().all()
|
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)
|
@router.post("/games/{game_id}/bosses", response_model=BossBattleResponse, status_code=201)
|
||||||
async def create_boss(
|
async def create_boss(
|
||||||
game_id: int,
|
game_id: int,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import type {
|
|||||||
CreateBossBattleInput,
|
CreateBossBattleInput,
|
||||||
UpdateBossBattleInput,
|
UpdateBossBattleInput,
|
||||||
BossPokemonInput,
|
BossPokemonInput,
|
||||||
|
BossReorderItem,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
// Games
|
// Games
|
||||||
@@ -123,5 +124,8 @@ export const updateBossBattle = (gameId: number, bossId: number, data: UpdateBos
|
|||||||
export const deleteBossBattle = (gameId: number, bossId: number) =>
|
export const deleteBossBattle = (gameId: number, bossId: number) =>
|
||||||
api.del(`/games/${gameId}/bosses/${bossId}`)
|
api.del(`/games/${gameId}/bosses/${bossId}`)
|
||||||
|
|
||||||
|
export const reorderBosses = (gameId: number, bosses: BossReorderItem[]) =>
|
||||||
|
api.put<BossBattle[]>(`/games/${gameId}/bosses/reorder`, { bosses })
|
||||||
|
|
||||||
export const setBossTeam = (gameId: number, bossId: number, team: BossPokemonInput[]) =>
|
export const setBossTeam = (gameId: number, bossId: number, team: BossPokemonInput[]) =>
|
||||||
api.put<BossBattle>(`/games/${gameId}/bosses/${bossId}/pokemon`, team)
|
api.put<BossBattle>(`/games/${gameId}/bosses/${bossId}/pokemon`, team)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
CreateBossBattleInput,
|
CreateBossBattleInput,
|
||||||
UpdateBossBattleInput,
|
UpdateBossBattleInput,
|
||||||
BossPokemonInput,
|
BossPokemonInput,
|
||||||
|
BossReorderItem,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
// --- Queries ---
|
// --- 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) {
|
export function useDeleteBossBattle(gameId: number) {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|||||||
Reference in New Issue
Block a user