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:
@@ -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} — {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>
|
||||
|
||||
Reference in New Issue
Block a user