Add genlocke leg progression with advance endpoint and run context

When a run belonging to a genlocke is completed or failed, the genlocke
status updates accordingly. The run detail API now includes genlocke
context (leg order, total legs, genlocke name). A new advance endpoint
creates the next leg's run, and the frontend shows genlocke-aware UI
including a "Leg X of Y" banner, advance button, and contextual
messaging in the end-run modal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Julian Tabel
2026-02-09 09:47:28 +01:00
parent 96178622f9
commit 07343e94e2
11 changed files with 271 additions and 54 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useMemo, useEffect, useCallback } from 'react'
import { useParams, Link } from 'react-router-dom'
import { useParams, Link, useNavigate } from 'react-router-dom'
import { useRun, useUpdateRun } from '../hooks/useRuns'
import { useAdvanceLeg } from '../hooks/useGenlockes'
import { useGameRoutes } from '../hooks/useGames'
import { useCreateEncounter, useUpdateEncounter, useBulkRandomize } from '../hooks/useEncounters'
import { usePokemonFamilies } from '../hooks/usePokemon'
@@ -389,8 +390,10 @@ function RouteGroup({
export function RunEncounters() {
const { runId } = useParams<{ runId: string }>()
const navigate = useNavigate()
const runIdNum = Number(runId)
const { data: run, isLoading, error } = useRun(runIdNum)
const advanceLeg = useAdvanceLeg()
const { data: routes, isLoading: loadingRoutes } = useGameRoutes(
run?.gameId ?? null,
)
@@ -745,6 +748,11 @@ export function RunEncounters() {
day: 'numeric',
})}
</p>
{run.genlocke && (
<p className="text-sm text-purple-600 dark:text-purple-400 mt-1 font-medium">
Leg {run.genlocke.legOrder} of {run.genlocke.totalLegs} &mdash; {run.genlocke.genlockeName}
</p>
)}
</div>
<div className="flex items-center gap-2">
{isActive && run.rules?.shinyClause && (
@@ -789,39 +797,70 @@ export function RunEncounters() {
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl">{run.status === 'completed' ? '\u{1f3c6}' : '\u{1faa6}'}</span>
<div>
<p
className={`font-semibold ${
run.status === 'completed'
? 'text-blue-800 dark:text-blue-200'
: 'text-red-800 dark:text-red-200'
}`}
>
{run.status === 'completed' ? 'Victory!' : 'Defeat'}
</p>
<p
className={`text-sm ${
run.status === 'completed'
? 'text-blue-600 dark:text-blue-400'
: 'text-red-600 dark:text-red-400'
}`}
>
{run.completedAt && (
<>
Ended{' '}
{new Date(run.completedAt).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
})}
{' \u00b7 '}
Duration: {formatDuration(run.startedAt, run.completedAt)}
</>
)}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{run.status === 'completed' ? '\u{1f3c6}' : '\u{1faa6}'}</span>
<div>
<p
className={`font-semibold ${
run.status === 'completed'
? 'text-blue-800 dark:text-blue-200'
: 'text-red-800 dark:text-red-200'
}`}
>
{run.status === 'completed'
? run.genlocke?.isFinalLeg
? 'Genlocke Complete!'
: 'Victory!'
: run.genlocke
? 'Genlocke Failed'
: 'Defeat'}
</p>
<p
className={`text-sm ${
run.status === 'completed'
? 'text-blue-600 dark:text-blue-400'
: 'text-red-600 dark:text-red-400'
}`}
>
{run.completedAt && (
<>
Ended{' '}
{new Date(run.completedAt).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
})}
{' \u00b7 '}
Duration: {formatDuration(run.startedAt, run.completedAt)}
</>
)}
</p>
</div>
</div>
{run.status === 'completed' && run.genlocke && !run.genlocke.isFinalLeg && (
<button
onClick={() => {
advanceLeg.mutate(
{ genlockeId: run.genlocke!.genlockeId, legOrder: run.genlocke!.legOrder },
{
onSuccess: (genlocke) => {
const nextLeg = genlocke.legs.find(
(l) => l.legOrder === run.genlocke!.legOrder + 1,
)
if (nextLeg?.runId) {
navigate(`/runs/${nextLeg.runId}`)
}
},
},
)
}}
disabled={advanceLeg.isPending}
className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
{advanceLeg.isPending ? 'Advancing...' : 'Advance to Next Leg'}
</button>
)}
</div>
</div>
)}
@@ -1323,6 +1362,7 @@ export function RunEncounters() {
}}
onClose={() => setShowEndRun(false)}
isPending={updateRun.isPending}
genlockeContext={run.genlocke}
/>
)}
</div>