From a2127f212627160b0ca23196f3eb611490d7164a Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sun, 8 Feb 2026 21:47:35 +0100 Subject: [PATCH] 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 --- ...incada-split-evolution-ninjask-shedinja.md | 11 ++ backend/src/app/api/encounters.py | 2 +- backend/src/app/schemas/encounter.py | 1 + frontend/src/components/StatusChangeModal.tsx | 130 ++++++++++++++++-- frontend/src/pages/RunDashboard.tsx | 6 +- frontend/src/pages/RunEncounters.tsx | 3 + frontend/src/types/game.ts | 1 + 7 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 .beans/nuzlocke-tracker-ztx8--handle-nincada-split-evolution-ninjask-shedinja.md diff --git a/.beans/nuzlocke-tracker-ztx8--handle-nincada-split-evolution-ninjask-shedinja.md b/.beans/nuzlocke-tracker-ztx8--handle-nincada-split-evolution-ninjask-shedinja.md new file mode 100644 index 0000000..c2d9953 --- /dev/null +++ b/.beans/nuzlocke-tracker-ztx8--handle-nincada-split-evolution-ninjask-shedinja.md @@ -0,0 +1,11 @@ +--- +# nuzlocke-tracker-ztx8 +title: Handle Nincada Split Evolution (Ninjask + Shedinja) +status: completed +type: feature +priority: normal +created_at: 2026-02-08T20:42:44Z +updated_at: 2026-02-08T20:44:58Z +--- + +Implement UI and backend support for Nincada's unique split evolution into both Ninjask and Shedinja. When evolving Nincada, show a confirmation prompt offering to also add Shedinja as a new encounter. \ No newline at end of file diff --git a/backend/src/app/api/encounters.py b/backend/src/app/api/encounters.py index d21c7f0..92e97f6 100644 --- a/backend/src/app/api/encounters.py +++ b/backend/src/app/api/encounters.py @@ -58,7 +58,7 @@ async def create_encounter( # Shiny clause: shiny encounters bypass the route-lock check shiny_clause_on = run.rules.get("shinyClause", True) if run.rules else True - skip_route_lock = data.is_shiny and shiny_clause_on + skip_route_lock = (data.is_shiny and shiny_clause_on) or data.origin == "shed_evolution" # If this route has a parent, check if sibling already has an encounter if route.parent_route_id is not None and not skip_route_lock: diff --git a/backend/src/app/schemas/encounter.py b/backend/src/app/schemas/encounter.py index 92b88fc..90a97ba 100644 --- a/backend/src/app/schemas/encounter.py +++ b/backend/src/app/schemas/encounter.py @@ -12,6 +12,7 @@ class EncounterCreate(CamelModel): status: str catch_level: int | None = None is_shiny: bool = False + origin: str | None = None class EncounterUpdate(CamelModel): diff --git a/frontend/src/components/StatusChangeModal.tsx b/frontend/src/components/StatusChangeModal.tsx index 8fa1a98..a0794a2 100644 --- a/frontend/src/components/StatusChangeModal.tsx +++ b/frontend/src/components/StatusChangeModal.tsx @@ -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(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 (
@@ -151,7 +187,7 @@ export function StatusChangeModal({ )} {/* Alive pokemon: actions */} - {!isDead && !showConfirm && !showEvolve && !showFormChange && ( + {!isDead && !showConfirm && !showEvolve && !showFormChange && !showShedConfirm && (
+
+
+
+ {shedCompanion.toPokemon.spriteUrl ? ( + {shedCompanion.toPokemon.name} + ) : ( +
+ {shedCompanion.toPokemon.name[0].toUpperCase()} +
+ )} +

+ {displayPokemon.name} shed its shell! Would you also like to add{' '} + {shedCompanion.toPokemon.name}? +

+
+
+
+ + 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" + /> +
+
+ + +
+
+ )} + {/* Form change selection */} {!isDead && showFormChange && (
@@ -351,7 +465,7 @@ export function StatusChangeModal({
{/* Footer for dead/no-confirm/no-evolve views */} - {(isDead || (!isDead && !showConfirm && !showEvolve && !showFormChange)) && ( + {(isDead || (!isDead && !showConfirm && !showEvolve && !showFormChange && !showShedConfirm)) && (