Restrict transfers to HoF team and prevent blocking starter route

Transfer modal now only appears when a Hall of Fame team is selected,
using the existing hofTeam data instead of the survivors endpoint.
Without a HoF selection, advance proceeds directly with no transfer step.

Transferred encounters are now a separate category: they appear in their
own "Transferred Pokemon" section, don't occupy route slots in the
encounter map, and don't block the route-lock check (excluded via
genlocke_transfers subquery). The run detail endpoint returns
transferEncounterIds so the frontend can distinguish them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Julian Tabel
2026-02-09 11:35:05 +01:00
parent e1dac10d27
commit c2e946f500
6 changed files with 115 additions and 65 deletions

View File

@@ -1,16 +1,16 @@
import { useState } from 'react'
import type { SurvivorEncounter } from '../types'
import type { EncounterDetail } from '../types'
interface TransferModalProps {
survivors: SurvivorEncounter[]
hofTeam: EncounterDetail[]
onSubmit: (encounterIds: number[]) => void
onSkip: () => void
isPending: boolean
}
export function TransferModal({ survivors, onSubmit, onSkip, isPending }: TransferModalProps) {
export function TransferModal({ hofTeam, onSubmit, onSkip, isPending }: TransferModalProps) {
const [selected, setSelected] = useState<Set<number>>(
() => new Set(survivors.map((s) => s.id)),
() => new Set(hofTeam.map((e) => e.id)),
)
const toggle = (id: number) => {
@@ -39,54 +39,48 @@ export function TransferModal({ survivors, onSubmit, onSkip, isPending }: Transf
</div>
<div className="px-6 py-4">
{survivors.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-8">
No surviving Pokemon to transfer.
</p>
) : (
<div className="grid grid-cols-3 gap-2">
{survivors.map((survivor) => {
const displayPokemon = survivor.currentPokemon ?? survivor.pokemon
const isSelected = selected.has(survivor.id)
<div className="grid grid-cols-3 gap-2">
{hofTeam.map((enc) => {
const displayPokemon = enc.currentPokemon ?? enc.pokemon
const isSelected = selected.has(enc.id)
return (
<button
key={survivor.id}
type="button"
onClick={() => toggle(survivor.id)}
className={`flex flex-col items-center p-3 rounded-lg border-2 text-center transition-colors ${
isSelected
? 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
{displayPokemon.spriteUrl ? (
<img
src={displayPokemon.spriteUrl}
alt={displayPokemon.name}
className="w-14 h-14"
/>
) : (
<div className="w-14 h-14 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-lg font-bold">
{displayPokemon.name[0].toUpperCase()}
</div>
)}
<span className="text-xs font-medium text-gray-700 dark:text-gray-300 mt-1 capitalize">
{survivor.nickname || displayPokemon.name}
return (
<button
key={enc.id}
type="button"
onClick={() => toggle(enc.id)}
className={`flex flex-col items-center p-3 rounded-lg border-2 text-center transition-colors ${
isSelected
? 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
{displayPokemon.spriteUrl ? (
<img
src={displayPokemon.spriteUrl}
alt={displayPokemon.name}
className="w-14 h-14"
/>
) : (
<div className="w-14 h-14 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-lg font-bold">
{displayPokemon.name[0].toUpperCase()}
</div>
)}
<span className="text-xs font-medium text-gray-700 dark:text-gray-300 mt-1 capitalize">
{enc.nickname || displayPokemon.name}
</span>
{enc.nickname && (
<span className="text-[10px] text-gray-400">
{displayPokemon.name}
</span>
{survivor.nickname && (
<span className="text-[10px] text-gray-400">
{displayPokemon.name}
</span>
)}
<span className="text-[10px] text-gray-400 mt-0.5">
{survivor.routeName}
</span>
</button>
)
})}
</div>
)}
)}
<span className="text-[10px] text-gray-400 mt-0.5">
{enc.route.name}
</span>
</button>
)
})}
</div>
</div>
<div className="sticky bottom-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-6 py-4 rounded-b-xl flex items-center justify-between">
@@ -100,7 +94,7 @@ export function TransferModal({ survivors, onSubmit, onSkip, isPending }: Transf
</button>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-400 dark:text-gray-500">
{selected.size}/{survivors.length} selected
{selected.size}/{hofTeam.length} selected
</span>
<button
type="button"