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:
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
# nuzlocke-tracker-66hg
|
# nuzlocke-tracker-66hg
|
||||||
title: Auto-select boss team variant based on starter choice
|
title: Auto-select boss team variant based on starter choice
|
||||||
status: draft
|
status: in-progress
|
||||||
type: feature
|
type: feature
|
||||||
|
priority: normal
|
||||||
created_at: 2026-02-08T20:21:40Z
|
created_at: 2026-02-08T20:21:40Z
|
||||||
updated_at: 2026-02-08T20:21:40Z
|
updated_at: 2026-02-08T20:22:47Z
|
||||||
---
|
---
|
||||||
|
|
||||||
When a run's starter Pokemon is known, automatically match it against boss battle condition labels (e.g., "Chose Bulbasaur") and pre-select the matching variant instead of showing the pill selector.
|
When a run's starter Pokemon is known, automatically match it against boss battle condition labels (e.g., "Chose Bulbasaur") and pre-select the matching variant instead of showing the pill selector.
|
||||||
@@ -28,9 +29,9 @@ Currently, bosses with variant teams (condition_label) display a pill selector o
|
|||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] Determine how the starter Pokemon is stored/accessible from the run data
|
- [x] Determine how the starter Pokemon is stored/accessible from the run data
|
||||||
- [ ] Add matching logic to find the right variant from condition labels
|
- [x] Add matching logic to find the right variant from condition labels
|
||||||
- [ ] Update BossDefeatModal to auto-select and hide pills when starter matches
|
- [x] Update BossDefeatModal to auto-select and hide pills when starter matches
|
||||||
- [ ] Update BossTeamPreview in RunEncounters with same logic
|
- [x] Update BossTeamPreview in RunEncounters with same logic
|
||||||
- [ ] Test with variant bosses where starter matches a condition
|
- [ ] Test with variant bosses where starter matches a condition
|
||||||
- [ ] Test fallback behavior when no starter is set or no match found
|
- [ ] Test fallback behavior when no starter is set or no match found
|
||||||
@@ -7,9 +7,17 @@ interface BossDefeatModalProps {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
isPending?: boolean
|
isPending?: boolean
|
||||||
hardcoreMode?: 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 [result, setResult] = useState<'won' | 'lost'>('won')
|
||||||
const [attempts, setAttempts] = useState('1')
|
const [attempts, setAttempts] = useState('1')
|
||||||
|
|
||||||
@@ -22,8 +30,10 @@ export function BossDefeatModal({ boss, onSubmit, onClose, isPending, hardcoreMo
|
|||||||
}, [boss.pokemon])
|
}, [boss.pokemon])
|
||||||
|
|
||||||
const hasVariants = variantLabels.length > 0
|
const hasVariants = variantLabels.length > 0
|
||||||
|
const autoMatch = useMemo(() => matchVariant(variantLabels, starterName), [variantLabels, starterName])
|
||||||
|
const showPills = hasVariants && autoMatch === null
|
||||||
const [selectedVariant, setSelectedVariant] = useState<string | null>(
|
const [selectedVariant, setSelectedVariant] = useState<string | null>(
|
||||||
hasVariants ? variantLabels[0] : null,
|
autoMatch ?? (hasVariants ? variantLabels[0] : null),
|
||||||
)
|
)
|
||||||
|
|
||||||
const displayedPokemon = useMemo(() => {
|
const displayedPokemon = useMemo(() => {
|
||||||
@@ -54,7 +64,7 @@ export function BossDefeatModal({ boss, onSubmit, onClose, isPending, hardcoreMo
|
|||||||
{/* Boss team preview */}
|
{/* Boss team preview */}
|
||||||
{boss.pokemon.length > 0 && (
|
{boss.pokemon.length > 0 && (
|
||||||
<div className="px-6 py-3 border-b border-gray-200 dark:border-gray-700">
|
<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">
|
<div className="flex gap-1 mb-2 flex-wrap">
|
||||||
{variantLabels.map((label) => (
|
{variantLabels.map((label) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -146,7 +146,14 @@ function countDistinctZones(group: RouteWithChildren): number {
|
|||||||
return zones.size
|
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 variantLabels = useMemo(() => {
|
||||||
const labels = new Set<string>()
|
const labels = new Set<string>()
|
||||||
for (const bp of pokemon) {
|
for (const bp of pokemon) {
|
||||||
@@ -156,8 +163,10 @@ function BossTeamPreview({ pokemon }: { pokemon: BossPokemon[] }) {
|
|||||||
}, [pokemon])
|
}, [pokemon])
|
||||||
|
|
||||||
const hasVariants = variantLabels.length > 0
|
const hasVariants = variantLabels.length > 0
|
||||||
|
const autoMatch = useMemo(() => matchVariant(variantLabels, starterName), [variantLabels, starterName])
|
||||||
|
const showPills = hasVariants && autoMatch === null
|
||||||
const [selectedVariant, setSelectedVariant] = useState<string | null>(
|
const [selectedVariant, setSelectedVariant] = useState<string | null>(
|
||||||
hasVariants ? variantLabels[0] : null,
|
autoMatch ?? (hasVariants ? variantLabels[0] : null),
|
||||||
)
|
)
|
||||||
|
|
||||||
const displayed = useMemo(() => {
|
const displayed = useMemo(() => {
|
||||||
@@ -169,7 +178,7 @@ function BossTeamPreview({ pokemon }: { pokemon: BossPokemon[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{hasVariants && (
|
{showPills && (
|
||||||
<div className="flex gap-1 mb-2 flex-wrap">
|
<div className="flex gap-1 mb-2 flex-wrap">
|
||||||
{variantLabels.map((label) => (
|
{variantLabels.map((label) => (
|
||||||
<button
|
<button
|
||||||
@@ -492,6 +501,22 @@ export function RunEncounters() {
|
|||||||
return duped.size > 0 ? duped : undefined
|
return duped.size > 0 ? duped : undefined
|
||||||
}, [run, normalEncounters, familiesData])
|
}, [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
|
// Boss battle data
|
||||||
const defeatedBossIds = useMemo(() => {
|
const defeatedBossIds = useMemo(() => {
|
||||||
const set = new Set<number>()
|
const set = new Set<number>()
|
||||||
@@ -1186,7 +1211,7 @@ export function RunEncounters() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Boss pokemon team */}
|
{/* Boss pokemon team */}
|
||||||
{isBossExpanded && boss.pokemon.length > 0 && (
|
{isBossExpanded && boss.pokemon.length > 0 && (
|
||||||
<BossTeamPreview pokemon={boss.pokemon} />
|
<BossTeamPreview pokemon={boss.pokemon} starterName={starterName} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{sectionAfter && (
|
{sectionAfter && (
|
||||||
@@ -1259,6 +1284,7 @@ export function RunEncounters() {
|
|||||||
onClose={() => setSelectedBoss(null)}
|
onClose={() => setSelectedBoss(null)}
|
||||||
isPending={createBossResult.isPending}
|
isPending={createBossResult.isPending}
|
||||||
hardcoreMode={run?.rules?.hardcoreMode}
|
hardcoreMode={run?.rules?.hardcoreMode}
|
||||||
|
starterName={starterName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user