Auto-select boss team variant based on starter choice

When a run has a starter Pokemon, automatically match its species name
against boss battle condition labels (e.g., "charmander" matches "Chose
Charmander"). If exactly one variant matches, pre-select it and hide the
pill selector. Falls back to showing pills when no match is found.

Fixes starter lookup to use game routes data (which has encounterMethods
populated) instead of the run detail route (which defaults to empty).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 21:33:28 +01:00
parent 3de99859a1
commit 512be228a2
3 changed files with 50 additions and 13 deletions

View File

@@ -7,9 +7,17 @@ interface BossDefeatModalProps {
onClose: () => void
isPending?: boolean
hardcoreMode?: boolean
starterName?: string | null
}
export function BossDefeatModal({ boss, onSubmit, onClose, isPending, hardcoreMode }: BossDefeatModalProps) {
function matchVariant(labels: string[], starterName?: string | null): string | null {
if (!starterName || labels.length === 0) return null
const lower = starterName.toLowerCase()
const matches = labels.filter((l) => l.toLowerCase().includes(lower))
return matches.length === 1 ? matches[0] : null
}
export function BossDefeatModal({ boss, onSubmit, onClose, isPending, hardcoreMode, starterName }: BossDefeatModalProps) {
const [result, setResult] = useState<'won' | 'lost'>('won')
const [attempts, setAttempts] = useState('1')
@@ -22,8 +30,10 @@ export function BossDefeatModal({ boss, onSubmit, onClose, isPending, hardcoreMo
}, [boss.pokemon])
const hasVariants = variantLabels.length > 0
const autoMatch = useMemo(() => matchVariant(variantLabels, starterName), [variantLabels, starterName])
const showPills = hasVariants && autoMatch === null
const [selectedVariant, setSelectedVariant] = useState<string | null>(
hasVariants ? variantLabels[0] : null,
autoMatch ?? (hasVariants ? variantLabels[0] : null),
)
const displayedPokemon = useMemo(() => {
@@ -54,7 +64,7 @@ export function BossDefeatModal({ boss, onSubmit, onClose, isPending, hardcoreMo
{/* Boss team preview */}
{boss.pokemon.length > 0 && (
<div className="px-6 py-3 border-b border-gray-200 dark:border-gray-700">
{hasVariants && (
{showPills && (
<div className="flex gap-1 mb-2 flex-wrap">
{variantLabels.map((label) => (
<button

View File

@@ -146,7 +146,14 @@ function countDistinctZones(group: RouteWithChildren): number {
return zones.size
}
function BossTeamPreview({ pokemon }: { pokemon: BossPokemon[] }) {
function matchVariant(labels: string[], starterName?: string | null): string | null {
if (!starterName || labels.length === 0) return null
const lower = starterName.toLowerCase()
const matches = labels.filter((l) => l.toLowerCase().includes(lower))
return matches.length === 1 ? matches[0] : null
}
function BossTeamPreview({ pokemon, starterName }: { pokemon: BossPokemon[]; starterName?: string | null }) {
const variantLabels = useMemo(() => {
const labels = new Set<string>()
for (const bp of pokemon) {
@@ -156,8 +163,10 @@ function BossTeamPreview({ pokemon }: { pokemon: BossPokemon[] }) {
}, [pokemon])
const hasVariants = variantLabels.length > 0
const autoMatch = useMemo(() => matchVariant(variantLabels, starterName), [variantLabels, starterName])
const showPills = hasVariants && autoMatch === null
const [selectedVariant, setSelectedVariant] = useState<string | null>(
hasVariants ? variantLabels[0] : null,
autoMatch ?? (hasVariants ? variantLabels[0] : null),
)
const displayed = useMemo(() => {
@@ -169,7 +178,7 @@ function BossTeamPreview({ pokemon }: { pokemon: BossPokemon[] }) {
return (
<div className="mt-2">
{hasVariants && (
{showPills && (
<div className="flex gap-1 mb-2 flex-wrap">
{variantLabels.map((label) => (
<button
@@ -492,6 +501,22 @@ export function RunEncounters() {
return duped.size > 0 ? duped : undefined
}, [run, normalEncounters, familiesData])
// Find starter Pokemon name for auto-matching variant boss teams
// Note: enc.route from the run detail doesn't include encounterMethods
// (it's computed only in the game routes endpoint), so we look up the
// route from the separately-fetched routes data instead.
const starterName = useMemo(() => {
if (!routes) return null
const routeMap = new Map(routes.map((r) => [r.id, r]))
for (const enc of normalEncounters) {
const route = routeMap.get(enc.routeId)
if (route?.encounterMethods.includes('starter')) {
return enc.pokemon.name
}
}
return null
}, [normalEncounters, routes])
// Boss battle data
const defeatedBossIds = useMemo(() => {
const set = new Set<number>()
@@ -1186,7 +1211,7 @@ export function RunEncounters() {
</div>
{/* Boss pokemon team */}
{isBossExpanded && boss.pokemon.length > 0 && (
<BossTeamPreview pokemon={boss.pokemon} />
<BossTeamPreview pokemon={boss.pokemon} starterName={starterName} />
)}
</div>
{sectionAfter && (
@@ -1259,6 +1284,7 @@ export function RunEncounters() {
onClose={() => setSelectedBoss(null)}
isPending={createBossResult.isPending}
hardcoreMode={run?.rules?.hardcoreMode}
starterName={starterName}
/>
)}