From 0e4fac87903c692a31a4dbd70eadb2b552bed97f Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sun, 8 Feb 2026 15:23:59 +0100 Subject: [PATCH] Add optional specialty type field to boss battles Gym leaders, Elite Four, and champions can now have a Pokemon type specialty (e.g. Rock, Water). Shown as a type image badge on boss cards in the run view, and editable via dropdown in the admin form. Includes migration, export, and seed pipeline support. Co-Authored-By: Claude Opus 4.6 --- ...e0f1_add_specialty_type_to_boss_battles.py | 26 +++++++++++++++++++ backend/src/app/api/export.py | 1 + backend/src/app/models/boss_battle.py | 1 + backend/src/app/schemas/boss.py | 3 +++ backend/src/app/seeds/loader.py | 2 ++ backend/src/app/seeds/run.py | 1 + .../components/admin/BossBattleFormModal.tsx | 25 +++++++++++++++++- frontend/src/pages/RunEncounters.tsx | 4 +++ frontend/src/types/admin.ts | 2 ++ frontend/src/types/game.ts | 1 + 10 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 backend/src/app/alembic/versions/a6b7c8d9e0f1_add_specialty_type_to_boss_battles.py diff --git a/backend/src/app/alembic/versions/a6b7c8d9e0f1_add_specialty_type_to_boss_battles.py b/backend/src/app/alembic/versions/a6b7c8d9e0f1_add_specialty_type_to_boss_battles.py new file mode 100644 index 0000000..fb83fa1 --- /dev/null +++ b/backend/src/app/alembic/versions/a6b7c8d9e0f1_add_specialty_type_to_boss_battles.py @@ -0,0 +1,26 @@ +"""add specialty_type to boss battles + +Revision ID: a6b7c8d9e0f1 +Revises: f5a6b7c8d9e0 +Create Date: 2026-02-08 21:00:00.000000 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = 'a6b7c8d9e0f1' +down_revision: Union[str, Sequence[str], None] = 'f5a6b7c8d9e0' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('boss_battles', sa.Column('specialty_type', sa.String(20), nullable=True)) + + +def downgrade() -> None: + op.drop_column('boss_battles', 'specialty_type') diff --git a/backend/src/app/api/export.py b/backend/src/app/api/export.py index 654aef0..6d3d01d 100644 --- a/backend/src/app/api/export.py +++ b/backend/src/app/api/export.py @@ -138,6 +138,7 @@ async def export_game_bosses( { "name": b.name, "boss_type": b.boss_type, + "specialty_type": b.specialty_type, "badge_name": b.badge_name, "badge_image_url": b.badge_image_url, "level_cap": b.level_cap, diff --git a/backend/src/app/models/boss_battle.py b/backend/src/app/models/boss_battle.py index 03b5013..3c47431 100644 --- a/backend/src/app/models/boss_battle.py +++ b/backend/src/app/models/boss_battle.py @@ -16,6 +16,7 @@ class BossBattle(Base): ) name: Mapped[str] = mapped_column(String(100)) boss_type: Mapped[str] = mapped_column(String(20)) # gym_leader, elite_four, champion, rival, evil_team, other + specialty_type: Mapped[str | None] = mapped_column(String(20), default=None) # pokemon type specialty (e.g. rock, water) badge_name: Mapped[str | None] = mapped_column(String(100)) badge_image_url: Mapped[str | None] = mapped_column(String(500)) level_cap: Mapped[int] = mapped_column(SmallInteger) diff --git a/backend/src/app/schemas/boss.py b/backend/src/app/schemas/boss.py index 61476f3..7174128 100644 --- a/backend/src/app/schemas/boss.py +++ b/backend/src/app/schemas/boss.py @@ -17,6 +17,7 @@ class BossBattleResponse(CamelModel): version_group_id: int name: str boss_type: str + specialty_type: str | None badge_name: str | None badge_image_url: str | None level_cap: int @@ -43,6 +44,7 @@ class BossResultResponse(CamelModel): class BossBattleCreate(CamelModel): name: str boss_type: str + specialty_type: str | None = None badge_name: str | None = None badge_image_url: str | None = None level_cap: int @@ -56,6 +58,7 @@ class BossBattleCreate(CamelModel): class BossBattleUpdate(CamelModel): name: str | None = None boss_type: str | None = None + specialty_type: str | None = None badge_name: str | None = None badge_image_url: str | None = None level_cap: int | None = None diff --git a/backend/src/app/seeds/loader.py b/backend/src/app/seeds/loader.py index 2ab1a84..e2a288b 100644 --- a/backend/src/app/seeds/loader.py +++ b/backend/src/app/seeds/loader.py @@ -220,6 +220,7 @@ async def upsert_bosses( version_group_id=version_group_id, name=boss["name"], boss_type=boss["boss_type"], + specialty_type=boss.get("specialty_type"), badge_name=boss.get("badge_name"), badge_image_url=boss.get("badge_image_url"), level_cap=boss["level_cap"], @@ -232,6 +233,7 @@ async def upsert_bosses( set_={ "name": boss["name"], "boss_type": boss["boss_type"], + "specialty_type": boss.get("specialty_type"), "badge_name": boss.get("badge_name"), "badge_image_url": boss.get("badge_image_url"), "level_cap": boss["level_cap"], diff --git a/backend/src/app/seeds/run.py b/backend/src/app/seeds/run.py index dc2cbe0..a742e32 100644 --- a/backend/src/app/seeds/run.py +++ b/backend/src/app/seeds/run.py @@ -429,6 +429,7 @@ async def _export_bosses(session: AsyncSession, vg_data: dict): { "name": b.name, "boss_type": b.boss_type, + "specialty_type": b.specialty_type, "badge_name": b.badge_name, "badge_image_url": b.badge_image_url, "level_cap": b.level_cap, diff --git a/frontend/src/components/admin/BossBattleFormModal.tsx b/frontend/src/components/admin/BossBattleFormModal.tsx index a89f105..e6005d8 100644 --- a/frontend/src/components/admin/BossBattleFormModal.tsx +++ b/frontend/src/components/admin/BossBattleFormModal.tsx @@ -15,6 +15,12 @@ interface BossBattleFormModalProps { onEditTeam?: () => void } +const POKEMON_TYPES = [ + 'normal', 'fire', 'water', 'electric', 'grass', 'ice', + 'fighting', 'poison', 'ground', 'flying', 'psychic', 'bug', + 'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy', +] + const BOSS_TYPES = [ { value: 'gym_leader', label: 'Gym Leader' }, { value: 'elite_four', label: 'Elite Four' }, @@ -37,6 +43,7 @@ export function BossBattleFormModal({ }: BossBattleFormModalProps) { const [name, setName] = useState(boss?.name ?? '') const [bossType, setBossType] = useState(boss?.bossType ?? 'gym_leader') + const [specialtyType, setSpecialtyType] = useState(boss?.specialtyType ?? '') const [badgeName, setBadgeName] = useState(boss?.badgeName ?? '') const [badgeImageUrl, setBadgeImageUrl] = useState(boss?.badgeImageUrl ?? '') const [levelCap, setLevelCap] = useState(String(boss?.levelCap ?? '')) @@ -51,6 +58,7 @@ export function BossBattleFormModal({ onSubmit({ name, bossType, + specialtyType: specialtyType || null, badgeName: badgeName || null, badgeImageUrl: badgeImageUrl || null, levelCap: Number(levelCap), @@ -83,7 +91,7 @@ export function BossBattleFormModal({ ) : undefined} > -
+
+
+ + +
diff --git a/frontend/src/pages/RunEncounters.tsx b/frontend/src/pages/RunEncounters.tsx index 2d34902..1343524 100644 --- a/frontend/src/pages/RunEncounters.tsx +++ b/frontend/src/pages/RunEncounters.tsx @@ -15,6 +15,7 @@ import { RuleBadges, ShinyBox, ShinyEncounterModal, + TypeBadge, } from '../components' import { BossDefeatModal } from '../components/BossDefeatModal' import type { @@ -1097,6 +1098,9 @@ export function RunEncounters() { {bossTypeLabel[boss.bossType] ?? boss.bossType} + {boss.specialtyType && ( + + )}

{boss.location} · Level Cap: {boss.levelCap} diff --git a/frontend/src/types/admin.ts b/frontend/src/types/admin.ts index ef98bee..a633ff8 100644 --- a/frontend/src/types/admin.ts +++ b/frontend/src/types/admin.ts @@ -141,6 +141,7 @@ export interface PokemonEncounterLocation { export interface CreateBossBattleInput { name: string bossType: string + specialtyType?: string | null badgeName?: string | null badgeImageUrl?: string | null levelCap: number @@ -154,6 +155,7 @@ export interface CreateBossBattleInput { export interface UpdateBossBattleInput { name?: string bossType?: string + specialtyType?: string | null badgeName?: string | null badgeImageUrl?: string | null levelCap?: number diff --git a/frontend/src/types/game.ts b/frontend/src/types/game.ts index 9c64c83..3a53af9 100644 --- a/frontend/src/types/game.ts +++ b/frontend/src/types/game.ts @@ -145,6 +145,7 @@ export interface BossBattle { versionGroupId: number name: string bossType: BossType + specialtyType: string | null badgeName: string | null badgeImageUrl: string | null levelCap: number