Handle Nincada split evolution (Ninjask + Shedinja)
When evolving Nincada, a confirmation prompt now offers to also add Shedinja as a new encounter on the same route. The Shedinja encounter uses a "shed_evolution" origin to bypass route-locking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import type { EncounterDetail, UpdateEncounterInput } from '../types'
|
||||
import { useState, useMemo } from 'react'
|
||||
import type { EncounterDetail, UpdateEncounterInput, CreateEncounterInput } from '../types'
|
||||
import { useEvolutions, useForms } from '../hooks/useEncounters'
|
||||
import { TypeBadge } from './TypeBadge'
|
||||
import { formatEvolutionMethod } from '../utils/formatEvolution'
|
||||
@@ -13,6 +13,7 @@ interface StatusChangeModalProps {
|
||||
onClose: () => void
|
||||
isPending: boolean
|
||||
region?: string
|
||||
onCreateEncounter?: (data: CreateEncounterInput) => void
|
||||
}
|
||||
|
||||
export function StatusChangeModal({
|
||||
@@ -21,6 +22,7 @@ export function StatusChangeModal({
|
||||
onClose,
|
||||
isPending,
|
||||
region,
|
||||
onCreateEncounter,
|
||||
}: StatusChangeModalProps) {
|
||||
const { pokemon, currentPokemon, route, nickname, catchLevel, faintLevel, deathCause } =
|
||||
encounter
|
||||
@@ -29,16 +31,27 @@ export function StatusChangeModal({
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const [showEvolve, setShowEvolve] = useState(false)
|
||||
const [showFormChange, setShowFormChange] = useState(false)
|
||||
const [showShedConfirm, setShowShedConfirm] = useState(false)
|
||||
const [pendingEvolutionId, setPendingEvolutionId] = useState<number | null>(null)
|
||||
const [shedNickname, setShedNickname] = useState('')
|
||||
const [deathLevel, setDeathLevel] = useState('')
|
||||
const [cause, setCause] = useState('')
|
||||
|
||||
const activePokemonId = currentPokemon?.id ?? pokemon.id
|
||||
const { data: evolutions, isLoading: evolutionsLoading } = useEvolutions(
|
||||
showEvolve ? activePokemonId : null,
|
||||
showEvolve || showShedConfirm ? activePokemonId : null,
|
||||
region,
|
||||
)
|
||||
const { data: forms } = useForms(isDead ? null : activePokemonId)
|
||||
|
||||
const { normalEvolutions, shedCompanion } = useMemo(() => {
|
||||
if (!evolutions) return { normalEvolutions: [], shedCompanion: null }
|
||||
return {
|
||||
normalEvolutions: evolutions.filter(e => e.trigger !== 'shed'),
|
||||
shedCompanion: evolutions.find(e => e.trigger === 'shed') ?? null,
|
||||
}
|
||||
}, [evolutions])
|
||||
|
||||
const handleConfirmDeath = () => {
|
||||
onUpdate({
|
||||
id: encounter.id,
|
||||
@@ -50,12 +63,35 @@ export function StatusChangeModal({
|
||||
}
|
||||
|
||||
const handleEvolve = (toPokemonId: number) => {
|
||||
if (shedCompanion && onCreateEncounter) {
|
||||
setPendingEvolutionId(toPokemonId)
|
||||
setShowEvolve(false)
|
||||
setShowShedConfirm(true)
|
||||
return
|
||||
}
|
||||
onUpdate({
|
||||
id: encounter.id,
|
||||
data: { currentPokemonId: toPokemonId },
|
||||
})
|
||||
}
|
||||
|
||||
const applyEvolution = (includeShed: boolean) => {
|
||||
if (pendingEvolutionId === null) return
|
||||
onUpdate({
|
||||
id: encounter.id,
|
||||
data: { currentPokemonId: pendingEvolutionId },
|
||||
})
|
||||
if (includeShed && shedCompanion && onCreateEncounter) {
|
||||
onCreateEncounter({
|
||||
routeId: encounter.routeId,
|
||||
pokemonId: shedCompanion.toPokemon.id,
|
||||
nickname: shedNickname || undefined,
|
||||
status: 'caught',
|
||||
origin: 'shed_evolution',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
||||
@@ -151,7 +187,7 @@ export function StatusChangeModal({
|
||||
)}
|
||||
|
||||
{/* Alive pokemon: actions */}
|
||||
{!isDead && !showConfirm && !showEvolve && !showFormChange && (
|
||||
{!isDead && !showConfirm && !showEvolve && !showFormChange && !showShedConfirm && (
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@@ -197,12 +233,12 @@ export function StatusChangeModal({
|
||||
{evolutionsLoading && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Loading evolutions...</p>
|
||||
)}
|
||||
{!evolutionsLoading && evolutions && evolutions.length === 0 && (
|
||||
{!evolutionsLoading && normalEvolutions.length === 0 && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">No evolutions available</p>
|
||||
)}
|
||||
{!evolutionsLoading && evolutions && evolutions.length > 0 && (
|
||||
{!evolutionsLoading && normalEvolutions.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{evolutions.map((evo) => (
|
||||
{normalEvolutions.map((evo) => (
|
||||
<button
|
||||
key={evo.id}
|
||||
type="button"
|
||||
@@ -232,6 +268,84 @@ export function StatusChangeModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Shed evolution confirmation (Nincada → Ninjask + Shedinja) */}
|
||||
{!isDead && showShedConfirm && shedCompanion && (
|
||||
<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">
|
||||
Shed Evolution
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowShedConfirm(false)
|
||||
setPendingEvolutionId(null)
|
||||
setShedNickname('')
|
||||
setShowEvolve(true)
|
||||
}}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{shedCompanion.toPokemon.spriteUrl ? (
|
||||
<img
|
||||
src={shedCompanion.toPokemon.spriteUrl}
|
||||
alt={shedCompanion.toPokemon.name}
|
||||
className="w-12 h-12"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-12 h-12 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">
|
||||
{shedCompanion.toPokemon.name[0].toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-amber-800 dark:text-amber-300">
|
||||
{displayPokemon.name} shed its shell! Would you also like to add{' '}
|
||||
<span className="font-semibold">{shedCompanion.toPokemon.name}</span>?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="shed-nickname"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Nickname{' '}
|
||||
<span className="font-normal text-gray-400">(optional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="shed-nickname"
|
||||
type="text"
|
||||
maxLength={30}
|
||||
value={shedNickname}
|
||||
onChange={(e) => setShedNickname(e.target.value)}
|
||||
placeholder={shedCompanion.toPokemon.name}
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isPending}
|
||||
onClick={() => applyEvolution(false)}
|
||||
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isPending}
|
||||
onClick={() => applyEvolution(true)}
|
||||
className="flex-1 px-4 py-2 bg-amber-600 text-white rounded-lg font-medium hover:bg-amber-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isPending ? 'Saving...' : `Add ${shedCompanion.toPokemon.name}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form change selection */}
|
||||
{!isDead && showFormChange && (
|
||||
<div className="space-y-3">
|
||||
@@ -351,7 +465,7 @@ export function StatusChangeModal({
|
||||
</div>
|
||||
|
||||
{/* Footer for dead/no-confirm/no-evolve views */}
|
||||
{(isDead || (!isDead && !showConfirm && !showEvolve && !showFormChange)) && (
|
||||
{(isDead || (!isDead && !showConfirm && !showEvolve && !showFormChange && !showShedConfirm)) && (
|
||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { useRun, useUpdateRun } from '../hooks/useRuns'
|
||||
import { useGameRoutes } from '../hooks/useGames'
|
||||
import { useUpdateEncounter } from '../hooks/useEncounters'
|
||||
import { useCreateEncounter, useUpdateEncounter } from '../hooks/useEncounters'
|
||||
import { StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components'
|
||||
import type { RunStatus, EncounterDetail } from '../types'
|
||||
|
||||
@@ -26,6 +26,7 @@ export function RunDashboard() {
|
||||
const runIdNum = Number(runId)
|
||||
const { data: run, isLoading, error } = useRun(runIdNum)
|
||||
const { data: routes } = useGameRoutes(run?.gameId ?? null)
|
||||
const createEncounter = useCreateEncounter(runIdNum)
|
||||
const updateEncounter = useUpdateEncounter(runIdNum)
|
||||
const updateRun = useUpdateRun(runIdNum)
|
||||
const [selectedEncounter, setSelectedEncounter] =
|
||||
@@ -243,6 +244,9 @@ export function RunDashboard() {
|
||||
onClose={() => setSelectedEncounter(null)}
|
||||
isPending={updateEncounter.isPending}
|
||||
region={run?.game.region}
|
||||
onCreateEncounter={(data) => {
|
||||
createEncounter.mutate(data)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1269,6 +1269,9 @@ export function RunEncounters() {
|
||||
onClose={() => setSelectedTeamEncounter(null)}
|
||||
isPending={updateEncounter.isPending}
|
||||
region={run?.game.region}
|
||||
onCreateEncounter={(data) => {
|
||||
createEncounter.mutate(data)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ export interface CreateEncounterInput {
|
||||
status: EncounterStatus
|
||||
catchLevel?: number
|
||||
isShiny?: boolean
|
||||
origin?: string
|
||||
}
|
||||
|
||||
export interface UpdateEncounterInput {
|
||||
|
||||
Reference in New Issue
Block a user