Add non-evolution form change support (Rotom, Oricorio, etc.)
Add a "Change Form" button in StatusChangeModal for Pokemon with alternate forms sharing the same national_dex number. Mirrors the existing evolution UI pattern, reusing currentPokemonId to track the active form. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
# nuzlocke-tracker-9z2k
|
||||||
|
title: Implement non-evolution form changes
|
||||||
|
status: completed
|
||||||
|
type: feature
|
||||||
|
priority: normal
|
||||||
|
created_at: 2026-02-08T11:51:18Z
|
||||||
|
updated_at: 2026-02-08T11:52:36Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Add ability to change Pokemon forms (e.g. Rotom appliances, Oricorio nectar forms) without evolving. Mirror existing evolution UI pattern with a new 'Change Form' button in StatusChangeModal.
|
||||||
@@ -158,6 +158,22 @@ async def get_pokemon(
|
|||||||
return pokemon
|
return pokemon
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/pokemon/{pokemon_id}/forms", response_model=list[PokemonResponse])
|
||||||
|
async def get_pokemon_forms(
|
||||||
|
pokemon_id: int, session: AsyncSession = Depends(get_session)
|
||||||
|
):
|
||||||
|
pokemon = await session.get(Pokemon, pokemon_id)
|
||||||
|
if pokemon is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Pokemon not found")
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(Pokemon)
|
||||||
|
.where(Pokemon.national_dex == pokemon.national_dex, Pokemon.id != pokemon_id)
|
||||||
|
.order_by(Pokemon.pokeapi_id)
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/pokemon/{pokemon_id}/evolutions", response_model=list[EvolutionResponse])
|
@router.get("/pokemon/{pokemon_id}/evolutions", response_model=list[EvolutionResponse])
|
||||||
async def get_pokemon_evolutions(
|
async def get_pokemon_evolutions(
|
||||||
pokemon_id: int,
|
pokemon_id: int,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
CreateEncounterInput,
|
CreateEncounterInput,
|
||||||
UpdateEncounterInput,
|
UpdateEncounterInput,
|
||||||
Evolution,
|
Evolution,
|
||||||
|
Pokemon,
|
||||||
} from '../types/game'
|
} from '../types/game'
|
||||||
|
|
||||||
export function createEncounter(
|
export function createEncounter(
|
||||||
@@ -28,3 +29,7 @@ export function fetchEvolutions(pokemonId: number, region?: string): Promise<Evo
|
|||||||
const params = region ? `?region=${encodeURIComponent(region)}` : ''
|
const params = region ? `?region=${encodeURIComponent(region)}` : ''
|
||||||
return api.get(`/pokemon/${pokemonId}/evolutions${params}`)
|
return api.get(`/pokemon/${pokemonId}/evolutions${params}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchForms(pokemonId: number): Promise<Pokemon[]> {
|
||||||
|
return api.get(`/pokemon/${pokemonId}/forms`)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import type { EncounterDetail, UpdateEncounterInput } from '../types'
|
import type { EncounterDetail, UpdateEncounterInput } from '../types'
|
||||||
import { useEvolutions } from '../hooks/useEncounters'
|
import { useEvolutions, useForms } from '../hooks/useEncounters'
|
||||||
import { TypeBadge } from './TypeBadge'
|
import { TypeBadge } from './TypeBadge'
|
||||||
|
|
||||||
interface StatusChangeModalProps {
|
interface StatusChangeModalProps {
|
||||||
@@ -49,6 +49,7 @@ export function StatusChangeModal({
|
|||||||
const displayPokemon = currentPokemon ?? pokemon
|
const displayPokemon = currentPokemon ?? pokemon
|
||||||
const [showConfirm, setShowConfirm] = useState(false)
|
const [showConfirm, setShowConfirm] = useState(false)
|
||||||
const [showEvolve, setShowEvolve] = useState(false)
|
const [showEvolve, setShowEvolve] = useState(false)
|
||||||
|
const [showFormChange, setShowFormChange] = useState(false)
|
||||||
const [deathLevel, setDeathLevel] = useState('')
|
const [deathLevel, setDeathLevel] = useState('')
|
||||||
const [cause, setCause] = useState('')
|
const [cause, setCause] = useState('')
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ export function StatusChangeModal({
|
|||||||
showEvolve ? activePokemonId : null,
|
showEvolve ? activePokemonId : null,
|
||||||
region,
|
region,
|
||||||
)
|
)
|
||||||
|
const { data: forms } = useForms(isDead ? null : activePokemonId)
|
||||||
|
|
||||||
const handleConfirmDeath = () => {
|
const handleConfirmDeath = () => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
@@ -170,7 +172,7 @@ export function StatusChangeModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Alive pokemon: actions */}
|
{/* Alive pokemon: actions */}
|
||||||
{!isDead && !showConfirm && !showEvolve && (
|
{!isDead && !showConfirm && !showEvolve && !showFormChange && (
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -179,6 +181,15 @@ export function StatusChangeModal({
|
|||||||
>
|
>
|
||||||
Evolve
|
Evolve
|
||||||
</button>
|
</button>
|
||||||
|
{forms && forms.length > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowFormChange(true)}
|
||||||
|
className="flex-1 px-4 py-2 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 transition-colors"
|
||||||
|
>
|
||||||
|
Change Form
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowConfirm(true)}
|
onClick={() => setShowConfirm(true)}
|
||||||
@@ -242,6 +253,55 @@ export function StatusChangeModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Form change selection */}
|
||||||
|
{!isDead && showFormChange && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Change form to:
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowFormChange(false)}
|
||||||
|
className="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{forms && forms.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{forms.map((form) => (
|
||||||
|
<button
|
||||||
|
key={form.id}
|
||||||
|
type="button"
|
||||||
|
disabled={isPending}
|
||||||
|
onClick={() => handleEvolve(form.id)}
|
||||||
|
className="w-full flex items-center gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-purple-50 dark:hover:bg-purple-900/20 hover:border-purple-300 dark:hover:border-purple-600 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{form.spriteUrl ? (
|
||||||
|
<img src={form.spriteUrl} alt={form.name} className="w-10 h-10" />
|
||||||
|
) : (
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-sm font-bold text-gray-600 dark:text-gray-300">
|
||||||
|
{form.name[0].toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-medium text-gray-900 dark:text-gray-100 text-sm">
|
||||||
|
{form.name}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{form.types.map((type) => (
|
||||||
|
<TypeBadge key={type} type={type} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Confirmation form */}
|
{/* Confirmation form */}
|
||||||
{!isDead && showConfirm && (
|
{!isDead && showConfirm && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -312,7 +372,7 @@ export function StatusChangeModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer for dead/no-confirm/no-evolve views */}
|
{/* Footer for dead/no-confirm/no-evolve views */}
|
||||||
{(isDead || (!isDead && !showConfirm && !showEvolve)) && (
|
{(isDead || (!isDead && !showConfirm && !showEvolve && !showFormChange)) && (
|
||||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
|
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
updateEncounter,
|
updateEncounter,
|
||||||
deleteEncounter,
|
deleteEncounter,
|
||||||
fetchEvolutions,
|
fetchEvolutions,
|
||||||
|
fetchForms,
|
||||||
} from '../api/encounters'
|
} from '../api/encounters'
|
||||||
import type { CreateEncounterInput, UpdateEncounterInput } from '../types/game'
|
import type { CreateEncounterInput, UpdateEncounterInput } from '../types/game'
|
||||||
|
|
||||||
@@ -50,3 +51,11 @@ export function useEvolutions(pokemonId: number | null, region?: string) {
|
|||||||
enabled: pokemonId !== null,
|
enabled: pokemonId !== null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useForms(pokemonId: number | null) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['forms', pokemonId],
|
||||||
|
queryFn: () => fetchForms(pokemonId!),
|
||||||
|
enabled: pokemonId !== null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user