Add boss battles, level caps, and badge tracking

Introduces full boss battle system: data models (BossBattle, BossPokemon,
BossResult), API endpoints for CRUD and per-run defeat tracking, and frontend
UI including a sticky level cap bar with badge display on the run page,
interleaved boss battle cards in the encounter list, and an admin panel
section for managing boss battles and their pokemon teams.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 11:16:13 +01:00
parent 3b87397432
commit 190b08eb26
23 changed files with 1614 additions and 61 deletions

View File

@@ -13,6 +13,9 @@ import type {
UpdateRouteEncounterInput,
CreateEvolutionInput,
UpdateEvolutionInput,
CreateBossBattleInput,
UpdateBossBattleInput,
BossPokemonInput,
} from '../types'
// --- Queries ---
@@ -256,3 +259,54 @@ export function useRemoveRouteEncounter(routeId: number) {
onError: (err) => toast.error(`Failed to remove encounter: ${err.message}`),
})
}
// --- Boss Battle Mutations ---
export function useCreateBossBattle(gameId: number) {
const qc = useQueryClient()
return useMutation({
mutationFn: (data: CreateBossBattleInput) => adminApi.createBossBattle(gameId, data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['games', gameId, 'bosses'] })
toast.success('Boss battle created')
},
onError: (err) => toast.error(`Failed to create boss battle: ${err.message}`),
})
}
export function useUpdateBossBattle(gameId: number) {
const qc = useQueryClient()
return useMutation({
mutationFn: ({ bossId, data }: { bossId: number; data: UpdateBossBattleInput }) =>
adminApi.updateBossBattle(gameId, bossId, data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['games', gameId, 'bosses'] })
toast.success('Boss battle updated')
},
onError: (err) => toast.error(`Failed to update boss battle: ${err.message}`),
})
}
export function useDeleteBossBattle(gameId: number) {
const qc = useQueryClient()
return useMutation({
mutationFn: (bossId: number) => adminApi.deleteBossBattle(gameId, bossId),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['games', gameId, 'bosses'] })
toast.success('Boss battle deleted')
},
onError: (err) => toast.error(`Failed to delete boss battle: ${err.message}`),
})
}
export function useSetBossTeam(gameId: number, bossId: number) {
const qc = useQueryClient()
return useMutation({
mutationFn: (team: BossPokemonInput[]) => adminApi.setBossTeam(gameId, bossId, team),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['games', gameId, 'bosses'] })
toast.success('Boss team updated')
},
onError: (err) => toast.error(`Failed to update boss team: ${err.message}`),
})
}

View File

@@ -0,0 +1,43 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { getGameBosses, getBossResults, createBossResult, deleteBossResult } from '../api/bosses'
import type { CreateBossResultInput } from '../types/game'
export function useGameBosses(gameId: number | null) {
return useQuery({
queryKey: ['games', gameId, 'bosses'],
queryFn: () => getGameBosses(gameId!),
enabled: gameId != null,
})
}
export function useBossResults(runId: number) {
return useQuery({
queryKey: ['runs', runId, 'boss-results'],
queryFn: () => getBossResults(runId),
})
}
export function useCreateBossResult(runId: number) {
const qc = useQueryClient()
return useMutation({
mutationFn: (data: CreateBossResultInput) => createBossResult(runId, data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['runs', runId, 'boss-results'] })
toast.success('Boss result recorded')
},
onError: (err) => toast.error(`Failed to record result: ${err.message}`),
})
}
export function useDeleteBossResult(runId: number) {
const qc = useQueryClient()
return useMutation({
mutationFn: (resultId: number) => deleteBossResult(runId, resultId),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['runs', runId, 'boss-results'] })
toast.success('Boss result removed')
},
onError: (err) => toast.error(`Failed to remove result: ${err.message}`),
})
}