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:
@@ -80,8 +80,8 @@ A dedicated page showing:
|
|||||||
- [x] The first leg starts automatically upon genlocke creation
|
- [x] The first leg starts automatically upon genlocke creation
|
||||||
- [ ] Each leg is a full nuzlocke run, tracked identically to standalone runs
|
- [ ] Each leg is a full nuzlocke run, tracked identically to standalone runs
|
||||||
- [ ] Completing a leg triggers a transfer step where surviving Pokemon can be carried forward
|
- [ ] Completing a leg triggers a transfer step where surviving Pokemon can be carried forward
|
||||||
- [ ] Failing a leg marks the entire genlocke as failed
|
- [x] Failing a leg marks the entire genlocke as failed
|
||||||
- [ ] Completing the final leg marks the genlocke as completed
|
- [x] Completing the final leg marks the genlocke as completed
|
||||||
- [ ] A genlocke overview page shows progress, configuration, cumulative stats, lineage, and graveyard
|
- [ ] A genlocke overview page shows progress, configuration, cumulative stats, lineage, and graveyard
|
||||||
- [ ] Transferred Pokemon appear as eggs (base form, level 1) in the next leg
|
- [ ] Transferred Pokemon appear as eggs (base form, level 1) in the next leg
|
||||||
- [ ] Pokemon lineage is trackable across multiple legs
|
- [ ] Pokemon lineage is trackable across multiple legs
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
# nuzlocke-tracker-thbz
|
# nuzlocke-tracker-thbz
|
||||||
title: Genlocke leg progression
|
title: Genlocke leg progression
|
||||||
status: todo
|
status: in-progress
|
||||||
type: feature
|
type: feature
|
||||||
priority: normal
|
priority: normal
|
||||||
created_at: 2026-02-09T07:42:33Z
|
created_at: 2026-02-09T07:42:33Z
|
||||||
updated_at: 2026-02-09T07:45:55Z
|
updated_at: 2026-02-09T08:42:15Z
|
||||||
parent: nuzlocke-tracker-25mh
|
parent: nuzlocke-tracker-25mh
|
||||||
blocking:
|
blocking:
|
||||||
- nuzlocke-tracker-p74f
|
- nuzlocke-tracker-p74f
|
||||||
@@ -45,12 +45,12 @@ Handle the sequential progression of legs within a genlocke. When one leg is com
|
|||||||
- After completion, redirect to the transfer step (or to the genlocke overview if it was the final leg)
|
- After completion, redirect to the transfer step (or to the genlocke overview if it was the final leg)
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
- [ ] Add backend logic to detect when a completed/failed run belongs to a genlocke (query GenlockeLeg by run_id)
|
- [x] Add backend logic to detect when a completed/failed run belongs to a genlocke (query GenlockeLeg by run_id)
|
||||||
- [ ] On run completion: update GenlockeLeg status, determine if there's a next leg
|
- [x] On run completion: update GenlockeLeg status, determine if there's a next leg
|
||||||
- [ ] On run failure: mark the genlocke as failed, prevent further leg creation
|
- [x] On run failure: mark the genlocke as failed, prevent further leg creation
|
||||||
- [ ] On final leg completion: mark the genlocke as completed
|
- [x] On final leg completion: mark the genlocke as completed
|
||||||
- [ ] Implement `POST /api/v1/genlockes/{id}/legs/{leg_order}/advance` endpoint to create the next leg's run
|
- [x] Implement `POST /api/v1/genlockes/{id}/legs/{leg_order}/advance` endpoint to create the next leg's run
|
||||||
- [ ] Add genlocke context to the run detail API response (genlocke name, leg number, total legs) when the run belongs to a genlocke
|
- [x] Add genlocke context to the run detail API response (genlocke name, leg number, total legs) when the run belongs to a genlocke
|
||||||
- [ ] Update the frontend run page header to show "Leg X of Y — {Genlocke Name}" when applicable
|
- [x] Update the frontend run page header to show "Leg X of Y — {Genlocke Name}" when applicable
|
||||||
- [ ] Update the run completion flow on the frontend to detect genlocke membership and redirect to transfer step instead of the standard completion screen
|
- [x] Update the run completion flow on the frontend to detect genlocke membership and redirect to transfer step instead of the standard completion screen
|
||||||
- [ ] Handle edge case: what happens if a genlocke run is manually deleted?
|
- [ ] Handle edge case: what happens if a genlocke run is manually deleted?
|
||||||
@@ -79,3 +79,86 @@ async def create_genlocke(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return result.scalar_one()
|
return result.scalar_one()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{genlocke_id}/legs/{leg_order}/advance",
|
||||||
|
response_model=GenlockeResponse,
|
||||||
|
)
|
||||||
|
async def advance_leg(
|
||||||
|
genlocke_id: int,
|
||||||
|
leg_order: int,
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
# Load genlocke with legs
|
||||||
|
result = await session.execute(
|
||||||
|
select(Genlocke)
|
||||||
|
.where(Genlocke.id == genlocke_id)
|
||||||
|
.options(
|
||||||
|
selectinload(Genlocke.legs).selectinload(GenlockeLeg.game),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
genlocke = result.scalar_one_or_none()
|
||||||
|
if genlocke is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Genlocke not found")
|
||||||
|
|
||||||
|
if genlocke.status != "active":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Genlocke is not active"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find the current leg
|
||||||
|
current_leg = None
|
||||||
|
next_leg = None
|
||||||
|
for leg in genlocke.legs:
|
||||||
|
if leg.leg_order == leg_order:
|
||||||
|
current_leg = leg
|
||||||
|
elif leg.leg_order == leg_order + 1:
|
||||||
|
next_leg = leg
|
||||||
|
|
||||||
|
if current_leg is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Leg not found")
|
||||||
|
|
||||||
|
# Verify current leg's run is completed
|
||||||
|
if current_leg.run_id is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Current leg has no run"
|
||||||
|
)
|
||||||
|
current_run = await session.get(NuzlockeRun, current_leg.run_id)
|
||||||
|
if current_run is None or current_run.status != "completed":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Current leg's run is not completed"
|
||||||
|
)
|
||||||
|
|
||||||
|
if next_leg is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="No next leg to advance to"
|
||||||
|
)
|
||||||
|
|
||||||
|
if next_leg.run_id is not None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Next leg already has a run"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a new run for the next leg
|
||||||
|
new_run = NuzlockeRun(
|
||||||
|
game_id=next_leg.game_id,
|
||||||
|
name=f"{genlocke.name} \u2014 Leg {next_leg.leg_order}",
|
||||||
|
status="active",
|
||||||
|
rules=genlocke.nuzlocke_rules,
|
||||||
|
)
|
||||||
|
session.add(new_run)
|
||||||
|
await session.flush()
|
||||||
|
|
||||||
|
next_leg.run_id = new_run.id
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
# Reload with relationships
|
||||||
|
result = await session.execute(
|
||||||
|
select(Genlocke)
|
||||||
|
.where(Genlocke.id == genlocke_id)
|
||||||
|
.options(
|
||||||
|
selectinload(Genlocke.legs).selectinload(GenlockeLeg.game),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result.scalar_one()
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response
|
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||||
from sqlalchemy import select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import joinedload, selectinload
|
from sqlalchemy.orm import joinedload, selectinload
|
||||||
|
|
||||||
from app.core.database import get_session
|
from app.core.database import get_session
|
||||||
from app.models.encounter import Encounter
|
from app.models.encounter import Encounter
|
||||||
from app.models.game import Game
|
from app.models.game import Game
|
||||||
|
from app.models.genlocke import Genlocke, GenlockeLeg
|
||||||
from app.models.nuzlocke_run import NuzlockeRun
|
from app.models.nuzlocke_run import NuzlockeRun
|
||||||
from app.schemas.run import RunCreate, RunDetailResponse, RunResponse, RunUpdate
|
from app.schemas.run import RunCreate, RunDetailResponse, RunGenlockeContext, RunResponse, RunUpdate
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -61,7 +62,33 @@ async def get_run(run_id: int, session: AsyncSession = Depends(get_session)):
|
|||||||
run = result.scalar_one_or_none()
|
run = result.scalar_one_or_none()
|
||||||
if run is None:
|
if run is None:
|
||||||
raise HTTPException(status_code=404, detail="Run not found")
|
raise HTTPException(status_code=404, detail="Run not found")
|
||||||
return run
|
|
||||||
|
# Check if this run belongs to a genlocke
|
||||||
|
genlocke_context = None
|
||||||
|
leg_result = await session.execute(
|
||||||
|
select(GenlockeLeg)
|
||||||
|
.where(GenlockeLeg.run_id == run_id)
|
||||||
|
.options(joinedload(GenlockeLeg.genlocke))
|
||||||
|
)
|
||||||
|
leg = leg_result.scalar_one_or_none()
|
||||||
|
if leg:
|
||||||
|
total_legs_result = await session.execute(
|
||||||
|
select(func.count())
|
||||||
|
.select_from(GenlockeLeg)
|
||||||
|
.where(GenlockeLeg.genlocke_id == leg.genlocke_id)
|
||||||
|
)
|
||||||
|
total_legs = total_legs_result.scalar_one()
|
||||||
|
genlocke_context = RunGenlockeContext(
|
||||||
|
genlocke_id=leg.genlocke_id,
|
||||||
|
genlocke_name=leg.genlocke.name,
|
||||||
|
leg_order=leg.leg_order,
|
||||||
|
total_legs=total_legs,
|
||||||
|
is_final_leg=leg.leg_order == total_legs,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = RunDetailResponse.model_validate(run)
|
||||||
|
response.genlocke = genlocke_context
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{run_id}", response_model=RunResponse)
|
@router.patch("/{run_id}", response_model=RunResponse)
|
||||||
@@ -87,6 +114,28 @@ async def update_run(
|
|||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
setattr(run, field, value)
|
setattr(run, field, value)
|
||||||
|
|
||||||
|
# Genlocke side effects when run status changes
|
||||||
|
if "status" in update_data and update_data["status"] in ("completed", "failed"):
|
||||||
|
leg_result = await session.execute(
|
||||||
|
select(GenlockeLeg)
|
||||||
|
.where(GenlockeLeg.run_id == run_id)
|
||||||
|
.options(joinedload(GenlockeLeg.genlocke))
|
||||||
|
)
|
||||||
|
leg = leg_result.scalar_one_or_none()
|
||||||
|
if leg:
|
||||||
|
genlocke = leg.genlocke
|
||||||
|
if update_data["status"] == "failed":
|
||||||
|
genlocke.status = "failed"
|
||||||
|
elif update_data["status"] == "completed":
|
||||||
|
total_legs_result = await session.execute(
|
||||||
|
select(func.count())
|
||||||
|
.select_from(GenlockeLeg)
|
||||||
|
.where(GenlockeLeg.genlocke_id == genlocke.id)
|
||||||
|
)
|
||||||
|
total_legs = total_legs_result.scalar_one()
|
||||||
|
if leg.leg_order == total_legs:
|
||||||
|
genlocke.status = "completed"
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(run)
|
await session.refresh(run)
|
||||||
return run
|
return run
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from app.schemas.pokemon import (
|
|||||||
RouteEncounterResponse,
|
RouteEncounterResponse,
|
||||||
RouteEncounterUpdate,
|
RouteEncounterUpdate,
|
||||||
)
|
)
|
||||||
from app.schemas.run import RunCreate, RunDetailResponse, RunResponse, RunUpdate
|
from app.schemas.run import RunCreate, RunDetailResponse, RunGenlockeContext, RunResponse, RunUpdate
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BossBattleCreate",
|
"BossBattleCreate",
|
||||||
@@ -75,6 +75,7 @@ __all__ = [
|
|||||||
"RouteUpdate",
|
"RouteUpdate",
|
||||||
"RunCreate",
|
"RunCreate",
|
||||||
"RunDetailResponse",
|
"RunDetailResponse",
|
||||||
|
"RunGenlockeContext",
|
||||||
"RunResponse",
|
"RunResponse",
|
||||||
"RunUpdate",
|
"RunUpdate",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ class RunResponse(CamelModel):
|
|||||||
completed_at: datetime | None
|
completed_at: datetime | None
|
||||||
|
|
||||||
|
|
||||||
|
class RunGenlockeContext(CamelModel):
|
||||||
|
genlocke_id: int
|
||||||
|
genlocke_name: str
|
||||||
|
leg_order: int
|
||||||
|
total_legs: int
|
||||||
|
is_final_leg: bool
|
||||||
|
|
||||||
|
|
||||||
class RunDetailResponse(RunResponse):
|
class RunDetailResponse(RunResponse):
|
||||||
game: GameResponse
|
game: GameResponse
|
||||||
encounters: list[EncounterDetailResponse] = []
|
encounters: list[EncounterDetailResponse] = []
|
||||||
|
genlocke: RunGenlockeContext | None = None
|
||||||
|
|||||||
@@ -8,3 +8,7 @@ export function createGenlocke(data: CreateGenlockeInput): Promise<Genlocke> {
|
|||||||
export function getGamesByRegion(): Promise<Region[]> {
|
export function getGamesByRegion(): Promise<Region[]> {
|
||||||
return api.get('/games/by-region')
|
return api.get('/games/by-region')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function advanceLeg(genlockeId: number, legOrder: number): Promise<Genlocke> {
|
||||||
|
return api.post(`/genlockes/${genlockeId}/legs/${legOrder}/advance`, {})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import type { RunStatus } from '../types'
|
import type { RunStatus, RunGenlockeContext } from '../types'
|
||||||
|
|
||||||
interface EndRunModalProps {
|
interface EndRunModalProps {
|
||||||
onConfirm: (status: RunStatus) => void
|
onConfirm: (status: RunStatus) => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
isPending?: boolean
|
isPending?: boolean
|
||||||
|
genlockeContext?: RunGenlockeContext | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EndRunModal({ onConfirm, onClose, isPending }: EndRunModalProps) {
|
export function EndRunModal({ onConfirm, onClose, isPending, genlockeContext }: EndRunModalProps) {
|
||||||
|
const victoryDescription = genlockeContext
|
||||||
|
? genlockeContext.isFinalLeg
|
||||||
|
? 'Complete the final leg of your genlocke!'
|
||||||
|
: 'Complete this leg and continue your genlocke'
|
||||||
|
: 'Beat the game successfully'
|
||||||
|
|
||||||
|
const defeatDescription = genlockeContext
|
||||||
|
? 'This will end the entire genlocke'
|
||||||
|
: 'All Pokemon fainted or gave up'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
||||||
@@ -25,7 +36,7 @@ export function EndRunModal({ onConfirm, onClose, isPending }: EndRunModalProps)
|
|||||||
className="w-full px-4 py-3 rounded-lg font-medium text-left border-2 border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 hover:border-blue-400 dark:hover:border-blue-600 disabled:opacity-50 transition-colors"
|
className="w-full px-4 py-3 rounded-lg font-medium text-left border-2 border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 hover:border-blue-400 dark:hover:border-blue-600 disabled:opacity-50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="font-semibold">Victory</div>
|
<div className="font-semibold">Victory</div>
|
||||||
<div className="text-sm opacity-80">Beat the game successfully</div>
|
<div className="text-sm opacity-80">{victoryDescription}</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onConfirm('failed')}
|
onClick={() => onConfirm('failed')}
|
||||||
@@ -33,7 +44,7 @@ export function EndRunModal({ onConfirm, onClose, isPending }: EndRunModalProps)
|
|||||||
className="w-full px-4 py-3 rounded-lg font-medium text-left border-2 border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 hover:border-red-400 dark:hover:border-red-600 disabled:opacity-50 transition-colors"
|
className="w-full px-4 py-3 rounded-lg font-medium text-left border-2 border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 hover:border-red-400 dark:hover:border-red-600 disabled:opacity-50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="font-semibold">Defeat</div>
|
<div className="font-semibold">Defeat</div>
|
||||||
<div className="text-sm opacity-80">All Pokemon fainted or gave up</div>
|
<div className="text-sm opacity-80">{defeatDescription}</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { createGenlocke, getGamesByRegion } from '../api/genlockes'
|
import { advanceLeg, createGenlocke, getGamesByRegion } from '../api/genlockes'
|
||||||
import type { CreateGenlockeInput } from '../types/game'
|
import type { CreateGenlockeInput } from '../types/game'
|
||||||
|
|
||||||
export function useRegions() {
|
export function useRegions() {
|
||||||
@@ -18,3 +18,14 @@ export function useCreateGenlocke() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useAdvanceLeg() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ genlockeId, legOrder }: { genlockeId: number; legOrder: number }) =>
|
||||||
|
advanceLeg(genlockeId, legOrder),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['runs'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useMemo, useEffect, useCallback } from 'react'
|
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 { useRun, useUpdateRun } from '../hooks/useRuns'
|
||||||
|
import { useAdvanceLeg } from '../hooks/useGenlockes'
|
||||||
import { useGameRoutes } from '../hooks/useGames'
|
import { useGameRoutes } from '../hooks/useGames'
|
||||||
import { useCreateEncounter, useUpdateEncounter, useBulkRandomize } from '../hooks/useEncounters'
|
import { useCreateEncounter, useUpdateEncounter, useBulkRandomize } from '../hooks/useEncounters'
|
||||||
import { usePokemonFamilies } from '../hooks/usePokemon'
|
import { usePokemonFamilies } from '../hooks/usePokemon'
|
||||||
@@ -389,8 +390,10 @@ function RouteGroup({
|
|||||||
|
|
||||||
export function RunEncounters() {
|
export function RunEncounters() {
|
||||||
const { runId } = useParams<{ runId: string }>()
|
const { runId } = useParams<{ runId: string }>()
|
||||||
|
const navigate = useNavigate()
|
||||||
const runIdNum = Number(runId)
|
const runIdNum = Number(runId)
|
||||||
const { data: run, isLoading, error } = useRun(runIdNum)
|
const { data: run, isLoading, error } = useRun(runIdNum)
|
||||||
|
const advanceLeg = useAdvanceLeg()
|
||||||
const { data: routes, isLoading: loadingRoutes } = useGameRoutes(
|
const { data: routes, isLoading: loadingRoutes } = useGameRoutes(
|
||||||
run?.gameId ?? null,
|
run?.gameId ?? null,
|
||||||
)
|
)
|
||||||
@@ -745,6 +748,11 @@ export function RunEncounters() {
|
|||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
})}
|
})}
|
||||||
</p>
|
</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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isActive && run.rules?.shinyClause && (
|
{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'
|
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-2xl">{run.status === 'completed' ? '\u{1f3c6}' : '\u{1faa6}'}</span>
|
<div className="flex items-center gap-3">
|
||||||
<div>
|
<span className="text-2xl">{run.status === 'completed' ? '\u{1f3c6}' : '\u{1faa6}'}</span>
|
||||||
<p
|
<div>
|
||||||
className={`font-semibold ${
|
<p
|
||||||
run.status === 'completed'
|
className={`font-semibold ${
|
||||||
? 'text-blue-800 dark:text-blue-200'
|
run.status === 'completed'
|
||||||
: 'text-red-800 dark:text-red-200'
|
? 'text-blue-800 dark:text-blue-200'
|
||||||
}`}
|
: 'text-red-800 dark:text-red-200'
|
||||||
>
|
}`}
|
||||||
{run.status === 'completed' ? 'Victory!' : 'Defeat'}
|
>
|
||||||
</p>
|
{run.status === 'completed'
|
||||||
<p
|
? run.genlocke?.isFinalLeg
|
||||||
className={`text-sm ${
|
? 'Genlocke Complete!'
|
||||||
run.status === 'completed'
|
: 'Victory!'
|
||||||
? 'text-blue-600 dark:text-blue-400'
|
: run.genlocke
|
||||||
: 'text-red-600 dark:text-red-400'
|
? 'Genlocke Failed'
|
||||||
}`}
|
: 'Defeat'}
|
||||||
>
|
</p>
|
||||||
{run.completedAt && (
|
<p
|
||||||
<>
|
className={`text-sm ${
|
||||||
Ended{' '}
|
run.status === 'completed'
|
||||||
{new Date(run.completedAt).toLocaleDateString(undefined, {
|
? 'text-blue-600 dark:text-blue-400'
|
||||||
year: 'numeric',
|
: 'text-red-600 dark:text-red-400'
|
||||||
month: 'short',
|
}`}
|
||||||
day: 'numeric',
|
>
|
||||||
})}
|
{run.completedAt && (
|
||||||
{' \u00b7 '}
|
<>
|
||||||
Duration: {formatDuration(run.startedAt, run.completedAt)}
|
Ended{' '}
|
||||||
</>
|
{new Date(run.completedAt).toLocaleDateString(undefined, {
|
||||||
)}
|
year: 'numeric',
|
||||||
</p>
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
{' \u00b7 '}
|
||||||
|
Duration: {formatDuration(run.startedAt, run.completedAt)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1323,6 +1362,7 @@ export function RunEncounters() {
|
|||||||
}}
|
}}
|
||||||
onClose={() => setShowEndRun(false)}
|
onClose={() => setShowEndRun(false)}
|
||||||
isPending={updateRun.isPending}
|
isPending={updateRun.isPending}
|
||||||
|
genlockeContext={run.genlocke}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,9 +93,18 @@ export interface NuzlockeRun {
|
|||||||
completedAt: string | null
|
completedAt: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RunGenlockeContext {
|
||||||
|
genlockeId: number
|
||||||
|
genlockeName: string
|
||||||
|
legOrder: number
|
||||||
|
totalLegs: number
|
||||||
|
isFinalLeg: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface RunDetail extends NuzlockeRun {
|
export interface RunDetail extends NuzlockeRun {
|
||||||
game: Game
|
game: Game
|
||||||
encounters: EncounterDetail[]
|
encounters: EncounterDetail[]
|
||||||
|
genlocke: RunGenlockeContext | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EncounterDetail extends Encounter {
|
export interface EncounterDetail extends Encounter {
|
||||||
|
|||||||
Reference in New Issue
Block a user