Implement Retire HoF (Gauntlet) rule enforcement for genlockes
When retireHoF is enabled, surviving HoF Pokemon and their evolutionary families are retired at leg advancement and treated as duplicates in all subsequent legs — both in the encounter modal and bulk randomize. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
# nuzlocke-tracker-8w9s
|
# nuzlocke-tracker-8w9s
|
||||||
title: Gauntlet rule option for genlockes
|
title: Gauntlet rule option for genlockes
|
||||||
status: todo
|
status: in-progress
|
||||||
type: feature
|
type: feature
|
||||||
priority: normal
|
priority: normal
|
||||||
created_at: 2026-02-08T19:15:43Z
|
created_at: 2026-02-08T19:15:43Z
|
||||||
updated_at: 2026-02-09T07:46:32Z
|
updated_at: 2026-02-09T08:56:55Z
|
||||||
parent: nuzlocke-tracker-25mh
|
parent: nuzlocke-tracker-25mh
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -31,10 +31,10 @@ Add the **Retire HoF** (aka Gauntlet) rule as a genlocke-specific rule option. W
|
|||||||
- The dupe list should be visible somewhere in the genlocke dashboard so the player knows which families are off-limits
|
- The dupe list should be visible somewhere in the genlocke dashboard so the player knows which families are off-limits
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
- [ ] Add a `retireHoF` boolean (or equivalent) to the genlocke rules JSONB schema
|
- [x] Add a `retireHoF` boolean (or equivalent) to the genlocke rules JSONB schema
|
||||||
- [ ] On leg completion with Retire HoF enabled: resolve the full evolutionary families of all surviving HoF Pokemon
|
- [x] On leg completion with Retire HoF enabled: resolve the full evolutionary families of all surviving HoF Pokemon
|
||||||
- [ ] Store the cumulative retired families list (could be a JSONB field on the Genlocke, or derived from completed legs)
|
- [x] Store the cumulative retired families list (could be a JSONB field on the Genlocke, or derived from completed legs)
|
||||||
- [ ] Implement `GET /api/v1/genlockes/{id}/retired-families` — return the list of retired evolutionary families with which leg they were retired in
|
- [x] Implement `GET /api/v1/genlockes/{id}/retired-families` — return the list of retired evolutionary families with which leg they were retired in
|
||||||
- [ ] Integrate with the encounter system's duplicates clause: when logging an encounter in a genlocke leg, check the cumulative retired list and flag duplicates
|
- [x] Integrate with the encounter system's duplicates clause: when logging an encounter in a genlocke leg, check the cumulative retired list and flag duplicates
|
||||||
- [ ] Build a "Retired Families" display on the genlocke overview page showing all off-limits Pokemon with their sprites
|
- [ ] Build a "Retired Families" display on the genlocke overview page showing all off-limits Pokemon with their sprites
|
||||||
- [ ] Ensure the creation wizard's genlocke rules step correctly toggles between Keep HoF and Retire HoF
|
- [x] Ensure the creation wizard's genlocke rules step correctly toggles between Keep HoF and Retire HoF
|
||||||
@@ -55,5 +55,6 @@ A dedicated dashboard page for a genlocke, showing progress, configuration, and
|
|||||||
- [ ] Build the configuration section: display genlocke rules and nuzlocke rules
|
- [ ] Build the configuration section: display genlocke rules and nuzlocke rules
|
||||||
- [ ] Build the cumulative stats section: aggregate encounters, deaths, and completions across legs
|
- [ ] Build the cumulative stats section: aggregate encounters, deaths, and completions across legs
|
||||||
- [ ] Add quick action links (active leg, graveyard, lineage — placeholder links for unimplemented features)
|
- [ ] Add quick action links (active leg, graveyard, lineage — placeholder links for unimplemented features)
|
||||||
|
- [ ] Build "Retired Families" section: when retireHoF is enabled, display cumulative retired Pokemon with sprites grouped by leg (data available via `GET /api/v1/genlockes/{id}/retired-families`)
|
||||||
- [ ] Add "Genlockes" navigation item to the app nav bar
|
- [ ] Add "Genlockes" navigation item to the app nav bar
|
||||||
- [ ] Add `/genlockes` and `/genlockes/:genlockeId` routes to the React Router config
|
- [ ] Add `/genlockes` and `/genlockes/:genlockeId` routes to the React Router config
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""add retired_pokemon_ids to genlocke_legs
|
||||||
|
|
||||||
|
Revision ID: c3d4e5f6a7b9
|
||||||
|
Revises: b2c3d4e5f6a8
|
||||||
|
Create Date: 2026-02-09 18:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'c3d4e5f6a7b9'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = 'b2c3d4e5f6a8'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
'genlocke_legs',
|
||||||
|
sa.Column('retired_pokemon_ids', JSONB(), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('genlocke_legs', 'retired_pokemon_ids')
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import random
|
import random
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response
|
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -9,6 +8,7 @@ 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.evolution import Evolution
|
from app.models.evolution import Evolution
|
||||||
|
from app.models.genlocke import GenlockeLeg
|
||||||
from app.models.nuzlocke_run import NuzlockeRun
|
from app.models.nuzlocke_run import NuzlockeRun
|
||||||
from app.models.pokemon import Pokemon
|
from app.models.pokemon import Pokemon
|
||||||
from app.models.route import Route
|
from app.models.route import Route
|
||||||
@@ -20,6 +20,7 @@ from app.schemas.encounter import (
|
|||||||
EncounterResponse,
|
EncounterResponse,
|
||||||
EncounterUpdate,
|
EncounterUpdate,
|
||||||
)
|
)
|
||||||
|
from app.services.families import build_families
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -159,34 +160,6 @@ async def delete_encounter(
|
|||||||
return Response(status_code=204)
|
return Response(status_code=204)
|
||||||
|
|
||||||
|
|
||||||
def _build_families(evolutions: list[Evolution]) -> dict[int, list[int]]:
|
|
||||||
"""Build pokemon_id → family members mapping using BFS on evolution graph."""
|
|
||||||
adj: dict[int, set[int]] = {}
|
|
||||||
for evo in evolutions:
|
|
||||||
adj.setdefault(evo.from_pokemon_id, set()).add(evo.to_pokemon_id)
|
|
||||||
adj.setdefault(evo.to_pokemon_id, set()).add(evo.from_pokemon_id)
|
|
||||||
|
|
||||||
visited: set[int] = set()
|
|
||||||
pokemon_to_family: dict[int, list[int]] = {}
|
|
||||||
for node in adj:
|
|
||||||
if node in visited:
|
|
||||||
continue
|
|
||||||
component: list[int] = []
|
|
||||||
queue = deque([node])
|
|
||||||
while queue:
|
|
||||||
current = queue.popleft()
|
|
||||||
if current in visited:
|
|
||||||
continue
|
|
||||||
visited.add(current)
|
|
||||||
component.append(current)
|
|
||||||
for neighbor in adj.get(current, set()):
|
|
||||||
if neighbor not in visited:
|
|
||||||
queue.append(neighbor)
|
|
||||||
for member in component:
|
|
||||||
pokemon_to_family[member] = component
|
|
||||||
return pokemon_to_family
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/runs/{run_id}/encounters/bulk-randomize",
|
"/runs/{run_id}/encounters/bulk-randomize",
|
||||||
response_model=BulkRandomizeResponse,
|
response_model=BulkRandomizeResponse,
|
||||||
@@ -247,7 +220,7 @@ async def bulk_randomize_encounters(
|
|||||||
if dupes_clause_on:
|
if dupes_clause_on:
|
||||||
evo_result = await session.execute(select(Evolution))
|
evo_result = await session.execute(select(Evolution))
|
||||||
evolutions = evo_result.scalars().all()
|
evolutions = evo_result.scalars().all()
|
||||||
pokemon_to_family = _build_families(evolutions)
|
pokemon_to_family = build_families(evolutions)
|
||||||
|
|
||||||
# 7. Build initial duped set from existing caught encounters
|
# 7. Build initial duped set from existing caught encounters
|
||||||
duped: set[int] = set()
|
duped: set[int] = set()
|
||||||
@@ -260,6 +233,23 @@ async def bulk_randomize_encounters(
|
|||||||
for member in family:
|
for member in family:
|
||||||
duped.add(member)
|
duped.add(member)
|
||||||
|
|
||||||
|
# Seed duped set with retired Pokemon IDs from prior genlocke legs
|
||||||
|
leg_result = await session.execute(
|
||||||
|
select(GenlockeLeg).where(GenlockeLeg.run_id == run_id)
|
||||||
|
)
|
||||||
|
leg = leg_result.scalar_one_or_none()
|
||||||
|
if leg:
|
||||||
|
genlocke_result = await session.execute(
|
||||||
|
select(GenlockeLeg.retired_pokemon_ids)
|
||||||
|
.where(
|
||||||
|
GenlockeLeg.genlocke_id == leg.genlocke_id,
|
||||||
|
GenlockeLeg.leg_order < leg.leg_order,
|
||||||
|
GenlockeLeg.retired_pokemon_ids.isnot(None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for (retired_ids,) in genlocke_result:
|
||||||
|
duped.update(retired_ids)
|
||||||
|
|
||||||
# 8. Organize routes: identify top-level and children
|
# 8. Organize routes: identify top-level and children
|
||||||
routes_by_id = {r.id: r for r in all_routes}
|
routes_by_id = {r.id: r for r in all_routes}
|
||||||
top_level = [r for r in all_routes if r.parent_route_id is None]
|
top_level = [r for r in all_routes if r.parent_route_id is None]
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from app.core.database import get_session
|
from app.core.database import get_session
|
||||||
|
from app.models.encounter import Encounter
|
||||||
|
from app.models.evolution import Evolution
|
||||||
from app.models.game import Game
|
from app.models.game import Game
|
||||||
from app.models.genlocke import Genlocke, GenlockeLeg
|
from app.models.genlocke import Genlocke, GenlockeLeg
|
||||||
from app.models.nuzlocke_run import NuzlockeRun
|
from app.models.nuzlocke_run import NuzlockeRun
|
||||||
from app.schemas.genlocke import GenlockeCreate, GenlockeResponse
|
from app.schemas.genlocke import GenlockeCreate, GenlockeResponse
|
||||||
|
from app.services.families import build_families
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -140,6 +144,37 @@ async def advance_leg(
|
|||||||
status_code=400, detail="Next leg already has a run"
|
status_code=400, detail="Next leg already has a run"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Compute retired Pokemon families if retireHoF is enabled
|
||||||
|
if genlocke.genlocke_rules.get("retireHoF", False):
|
||||||
|
# Query surviving caught Pokemon from the completed run
|
||||||
|
# "Surviving HoF" = caught, not fainted, not shiny
|
||||||
|
survivors_result = await session.execute(
|
||||||
|
select(Encounter.pokemon_id).where(
|
||||||
|
Encounter.run_id == current_leg.run_id,
|
||||||
|
Encounter.status == "caught",
|
||||||
|
Encounter.faint_level.is_(None),
|
||||||
|
Encounter.is_shiny.is_(False),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
survivor_ids = [row[0] for row in survivors_result]
|
||||||
|
|
||||||
|
if survivor_ids:
|
||||||
|
# Build family map from evolution data
|
||||||
|
evo_result = await session.execute(select(Evolution))
|
||||||
|
evolutions = evo_result.scalars().all()
|
||||||
|
pokemon_to_family = build_families(evolutions)
|
||||||
|
|
||||||
|
# Collect all family members of surviving Pokemon
|
||||||
|
retired: set[int] = set()
|
||||||
|
for pid in survivor_ids:
|
||||||
|
retired.add(pid)
|
||||||
|
for member in pokemon_to_family.get(pid, []):
|
||||||
|
retired.add(member)
|
||||||
|
|
||||||
|
current_leg.retired_pokemon_ids = sorted(retired)
|
||||||
|
else:
|
||||||
|
current_leg.retired_pokemon_ids = []
|
||||||
|
|
||||||
# Create a new run for the next leg
|
# Create a new run for the next leg
|
||||||
new_run = NuzlockeRun(
|
new_run = NuzlockeRun(
|
||||||
game_id=next_leg.game_id,
|
game_id=next_leg.game_id,
|
||||||
@@ -162,3 +197,56 @@ async def advance_leg(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return result.scalar_one()
|
return result.scalar_one()
|
||||||
|
|
||||||
|
|
||||||
|
class RetiredLegResponse(BaseModel):
|
||||||
|
leg_order: int
|
||||||
|
retired_pokemon_ids: list[int]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class RetiredFamiliesResponse(BaseModel):
|
||||||
|
retired_pokemon_ids: list[int]
|
||||||
|
by_leg: list[RetiredLegResponse]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{genlocke_id}/retired-families",
|
||||||
|
response_model=RetiredFamiliesResponse,
|
||||||
|
)
|
||||||
|
async def get_retired_families(
|
||||||
|
genlocke_id: int,
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
# Verify genlocke exists
|
||||||
|
genlocke = await session.get(Genlocke, genlocke_id)
|
||||||
|
if genlocke is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Genlocke not found")
|
||||||
|
|
||||||
|
# Query all legs with retired_pokemon_ids
|
||||||
|
result = await session.execute(
|
||||||
|
select(GenlockeLeg)
|
||||||
|
.where(
|
||||||
|
GenlockeLeg.genlocke_id == genlocke_id,
|
||||||
|
GenlockeLeg.retired_pokemon_ids.isnot(None),
|
||||||
|
)
|
||||||
|
.order_by(GenlockeLeg.leg_order)
|
||||||
|
)
|
||||||
|
legs = result.scalars().all()
|
||||||
|
|
||||||
|
cumulative: set[int] = set()
|
||||||
|
by_leg: list[RetiredLegResponse] = []
|
||||||
|
for leg in legs:
|
||||||
|
ids = leg.retired_pokemon_ids or []
|
||||||
|
cumulative.update(ids)
|
||||||
|
by_leg.append(RetiredLegResponse(
|
||||||
|
leg_order=leg.leg_order,
|
||||||
|
retired_pokemon_ids=ids,
|
||||||
|
))
|
||||||
|
|
||||||
|
return RetiredFamiliesResponse(
|
||||||
|
retired_pokemon_ids=sorted(cumulative),
|
||||||
|
by_leg=by_leg,
|
||||||
|
)
|
||||||
|
|||||||
@@ -78,12 +78,29 @@ async def get_run(run_id: int, session: AsyncSession = Depends(get_session)):
|
|||||||
.where(GenlockeLeg.genlocke_id == leg.genlocke_id)
|
.where(GenlockeLeg.genlocke_id == leg.genlocke_id)
|
||||||
)
|
)
|
||||||
total_legs = total_legs_result.scalar_one()
|
total_legs = total_legs_result.scalar_one()
|
||||||
|
|
||||||
|
# Aggregate retired Pokemon IDs from prior legs (retireHoF rule)
|
||||||
|
retired_pokemon_ids: list[int] = []
|
||||||
|
if leg.genlocke.genlocke_rules.get("retireHoF", False) and leg.leg_order > 1:
|
||||||
|
prior_result = await session.execute(
|
||||||
|
select(GenlockeLeg.retired_pokemon_ids).where(
|
||||||
|
GenlockeLeg.genlocke_id == leg.genlocke_id,
|
||||||
|
GenlockeLeg.leg_order < leg.leg_order,
|
||||||
|
GenlockeLeg.retired_pokemon_ids.isnot(None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cumulative: set[int] = set()
|
||||||
|
for (ids,) in prior_result:
|
||||||
|
cumulative.update(ids)
|
||||||
|
retired_pokemon_ids = sorted(cumulative)
|
||||||
|
|
||||||
genlocke_context = RunGenlockeContext(
|
genlocke_context = RunGenlockeContext(
|
||||||
genlocke_id=leg.genlocke_id,
|
genlocke_id=leg.genlocke_id,
|
||||||
genlocke_name=leg.genlocke.name,
|
genlocke_name=leg.genlocke.name,
|
||||||
leg_order=leg.leg_order,
|
leg_order=leg.leg_order,
|
||||||
total_legs=total_legs,
|
total_legs=total_legs,
|
||||||
is_final_leg=leg.leg_order == total_legs,
|
is_final_leg=leg.leg_order == total_legs,
|
||||||
|
retired_pokemon_ids=retired_pokemon_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = RunDetailResponse.model_validate(run)
|
response = RunDetailResponse.model_validate(run)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class GenlockeLeg(Base):
|
|||||||
ForeignKey("nuzlocke_runs.id"), index=True
|
ForeignKey("nuzlocke_runs.id"), index=True
|
||||||
)
|
)
|
||||||
leg_order: Mapped[int] = mapped_column(SmallInteger)
|
leg_order: Mapped[int] = mapped_column(SmallInteger)
|
||||||
|
retired_pokemon_ids: Mapped[list[int] | None] = mapped_column(JSONB, default=None)
|
||||||
|
|
||||||
genlocke: Mapped["Genlocke"] = relationship(back_populates="legs")
|
genlocke: Mapped["Genlocke"] = relationship(back_populates="legs")
|
||||||
game: Mapped["Game"] = relationship()
|
game: Mapped["Game"] = relationship()
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class RunGenlockeContext(CamelModel):
|
|||||||
leg_order: int
|
leg_order: int
|
||||||
total_legs: int
|
total_legs: int
|
||||||
is_final_leg: bool
|
is_final_leg: bool
|
||||||
|
retired_pokemon_ids: list[int] = []
|
||||||
|
|
||||||
|
|
||||||
class RunDetailResponse(RunResponse):
|
class RunDetailResponse(RunResponse):
|
||||||
|
|||||||
31
backend/src/app/services/families.py
Normal file
31
backend/src/app/services/families.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from app.models.evolution import Evolution
|
||||||
|
|
||||||
|
|
||||||
|
def build_families(evolutions: list[Evolution]) -> dict[int, list[int]]:
|
||||||
|
"""Build pokemon_id -> family members mapping using BFS on evolution graph."""
|
||||||
|
adj: dict[int, set[int]] = {}
|
||||||
|
for evo in evolutions:
|
||||||
|
adj.setdefault(evo.from_pokemon_id, set()).add(evo.to_pokemon_id)
|
||||||
|
adj.setdefault(evo.to_pokemon_id, set()).add(evo.from_pokemon_id)
|
||||||
|
|
||||||
|
visited: set[int] = set()
|
||||||
|
pokemon_to_family: dict[int, list[int]] = {}
|
||||||
|
for node in adj:
|
||||||
|
if node in visited:
|
||||||
|
continue
|
||||||
|
component: list[int] = []
|
||||||
|
queue = deque([node])
|
||||||
|
while queue:
|
||||||
|
current = queue.popleft()
|
||||||
|
if current in visited:
|
||||||
|
continue
|
||||||
|
visited.add(current)
|
||||||
|
component.append(current)
|
||||||
|
for neighbor in adj.get(current, set()):
|
||||||
|
if neighbor not in visited:
|
||||||
|
queue.append(neighbor)
|
||||||
|
for member in component:
|
||||||
|
pokemon_to_family[member] = component
|
||||||
|
return pokemon_to_family
|
||||||
@@ -17,6 +17,7 @@ interface EncounterModalProps {
|
|||||||
gameId: number
|
gameId: number
|
||||||
existing?: EncounterDetail
|
existing?: EncounterDetail
|
||||||
dupedPokemonIds?: Set<number>
|
dupedPokemonIds?: Set<number>
|
||||||
|
retiredPokemonIds?: Set<number>
|
||||||
onSubmit: (data: {
|
onSubmit: (data: {
|
||||||
routeId: number
|
routeId: number
|
||||||
pokemonId: number
|
pokemonId: number
|
||||||
@@ -93,6 +94,7 @@ export function EncounterModal({
|
|||||||
gameId,
|
gameId,
|
||||||
existing,
|
existing,
|
||||||
dupedPokemonIds,
|
dupedPokemonIds,
|
||||||
|
retiredPokemonIds,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -285,7 +287,7 @@ export function EncounterModal({
|
|||||||
</span>
|
</span>
|
||||||
{isDuped && (
|
{isDuped && (
|
||||||
<span className="text-[10px] text-gray-400 italic">
|
<span className="text-[10px] text-gray-400 italic">
|
||||||
already caught
|
{retiredPokemonIds?.has(rp.pokemonId) ? 'retired (HoF)' : 'already caught'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isDuped && SPECIAL_METHODS.includes(rp.encounterMethod) && (
|
{!isDuped && SPECIAL_METHODS.includes(rp.encounterMethod) && (
|
||||||
|
|||||||
@@ -469,6 +469,13 @@ export function RunEncounters() {
|
|||||||
return map
|
return map
|
||||||
}, [normalEncounters])
|
}, [normalEncounters])
|
||||||
|
|
||||||
|
// Build set of retired Pokemon IDs from genlocke context
|
||||||
|
const retiredPokemonIds = useMemo(() => {
|
||||||
|
const ids = run?.genlocke?.retiredPokemonIds
|
||||||
|
if (!ids || ids.length === 0) return undefined
|
||||||
|
return new Set(ids)
|
||||||
|
}, [run])
|
||||||
|
|
||||||
// Build set of duped Pokemon IDs (for duplicates clause)
|
// Build set of duped Pokemon IDs (for duplicates clause)
|
||||||
const dupedPokemonIds = useMemo(() => {
|
const dupedPokemonIds = useMemo(() => {
|
||||||
const dupesClauseOn = run?.rules?.duplicatesClause ?? true
|
const dupesClauseOn = run?.rules?.duplicatesClause ?? true
|
||||||
@@ -483,6 +490,14 @@ export function RunEncounters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const duped = new Set<number>()
|
const duped = new Set<number>()
|
||||||
|
|
||||||
|
// Seed with retired Pokemon IDs from prior genlocke legs
|
||||||
|
if (retiredPokemonIds) {
|
||||||
|
for (const id of retiredPokemonIds) {
|
||||||
|
duped.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const enc of normalEncounters) {
|
for (const enc of normalEncounters) {
|
||||||
if (enc.status !== 'caught') continue
|
if (enc.status !== 'caught') continue
|
||||||
const pokemonId = enc.currentPokemonId ?? enc.pokemonId
|
const pokemonId = enc.currentPokemonId ?? enc.pokemonId
|
||||||
@@ -504,7 +519,7 @@ export function RunEncounters() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return duped.size > 0 ? duped : undefined
|
return duped.size > 0 ? duped : undefined
|
||||||
}, [run, normalEncounters, familiesData])
|
}, [run, normalEncounters, familiesData, retiredPokemonIds])
|
||||||
|
|
||||||
// Find starter Pokemon name for auto-matching variant boss teams
|
// Find starter Pokemon name for auto-matching variant boss teams
|
||||||
// Note: enc.route from the run detail doesn't include encounterMethods
|
// Note: enc.route from the run detail doesn't include encounterMethods
|
||||||
@@ -1286,6 +1301,7 @@ export function RunEncounters() {
|
|||||||
gameId={run!.gameId}
|
gameId={run!.gameId}
|
||||||
existing={editingEncounter ?? undefined}
|
existing={editingEncounter ?? undefined}
|
||||||
dupedPokemonIds={dupedPokemonIds}
|
dupedPokemonIds={dupedPokemonIds}
|
||||||
|
retiredPokemonIds={retiredPokemonIds}
|
||||||
onSubmit={handleCreate}
|
onSubmit={handleCreate}
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export interface RunGenlockeContext {
|
|||||||
legOrder: number
|
legOrder: number
|
||||||
totalLegs: number
|
totalLegs: number
|
||||||
isFinalLeg: boolean
|
isFinalLeg: boolean
|
||||||
|
retiredPokemonIds: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RunDetail extends NuzlockeRun {
|
export interface RunDetail extends NuzlockeRun {
|
||||||
|
|||||||
Reference in New Issue
Block a user