feature/genlocke-naming-scheme #23
@@ -1,11 +1,66 @@
|
|||||||
---
|
---
|
||||||
# nuzlocke-tracker-5tac
|
# nuzlocke-tracker-5tac
|
||||||
title: Enable naming generator for Genlockes
|
title: Enable naming generator for Genlockes
|
||||||
status: draft
|
status: completed
|
||||||
type: task
|
type: task
|
||||||
priority: normal
|
priority: normal
|
||||||
created_at: 2026-02-11T21:14:21Z
|
created_at: 2026-02-11T21:14:21Z
|
||||||
updated_at: 2026-02-11T21:15:25Z
|
updated_at: 2026-02-14T08:52:16Z
|
||||||
---
|
---
|
||||||
|
|
||||||
Genlockes are just multiple runs in a row. Selecting a naming scheme works just the same and we do not need bigger dictionaries, as the names basically get reset after each team. Only 6 Pokemon at most can be transfered and would take up names, but that is OK.
|
## Overview
|
||||||
|
|
||||||
|
Genlockes are multiple nuzlocke runs played back-to-back. Currently, naming scheme selection is only available per-run, meaning genlocke runs don't get naming schemes at all (they're created automatically during genlocke creation and leg advancement). This task adds genlocke-level naming scheme selection and lineage-aware name suggestions.
|
||||||
|
|
||||||
|
## Key Behaviors
|
||||||
|
|
||||||
|
### 1. Genlocke-Level Naming Scheme
|
||||||
|
- When creating a genlocke, the user selects a naming scheme (same categories as standalone runs)
|
||||||
|
- This scheme is stored on the `Genlocke` model and automatically applied to every leg's `NuzlockeRun`
|
||||||
|
- Both the initial run (created in `create_genlocke`) and subsequent runs (created in `advance_leg`) inherit the genlocke's naming scheme
|
||||||
|
|
||||||
|
### 2. Name Suggestions (Current Leg Only)
|
||||||
|
- Duplicate name checking stays scoped to the current run (already the case)
|
||||||
|
- Transferred pokemon carry their nicknames forward, so they naturally occupy names in the current run's used-name set
|
||||||
|
|
||||||
|
### 3. Lineage-Aware Name Suggestions (Roman Numerals)
|
||||||
|
- When catching a pokemon in a genlocke leg (leg 2+), the system checks if any pokemon from the same **evolution family** was caught in a previous leg
|
||||||
|
- If so, the original nickname is suggested with a roman numeral suffix (e.g., "Heracles II", "Heracles III")
|
||||||
|
- The numeral represents the Nth distinct leg where this evolution family was originally caught (not transferred)
|
||||||
|
- Leg 1: Magikarp → "Heracles" (no numeral, first appearance)
|
||||||
|
- Leg 2: Magikarp or Gyarados caught → suggest "Heracles II"
|
||||||
|
- Leg 3: Magikarp caught again → suggest "Heracles III"
|
||||||
|
- Transferred pokemon don't count as new appearances (they're the same individual)
|
||||||
|
- The "base name" is taken from the first original encounter of that family across all legs
|
||||||
|
- The lineage suggestion appears as a **priority suggestion** alongside regular naming scheme suggestions
|
||||||
|
- The user can always choose a different name
|
||||||
|
|
||||||
|
### 4. How the API Changes
|
||||||
|
- `GET /runs/{run_id}/name-suggestions` gains an optional `pokemon_id` query param
|
||||||
|
- When `pokemon_id` is provided AND the run belongs to a genlocke:
|
||||||
|
- Determine the pokemon's evolution family
|
||||||
|
- Query previous legs' encounters (excluding transfer-target encounters) for matching family members
|
||||||
|
- If matches found: compute the roman numeral and prepend "{base_name} {numeral}" to the suggestions list
|
||||||
|
- Regular naming scheme suggestions are returned as before
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [x] Add `naming_scheme` column to `genlockes` table (Alembic migration)
|
||||||
|
- [x] Update `Genlocke` model with `naming_scheme: Mapped[str | None]`
|
||||||
|
- [x] Update `GenlockeCreate` schema to accept optional `naming_scheme: str | None`
|
||||||
|
- [x] Update `GenlockeResponse` and `GenlockeDetailResponse` to include `naming_scheme`
|
||||||
|
- [x] Update `create_genlocke` endpoint: pass `naming_scheme` to the first leg's `NuzlockeRun`
|
||||||
|
- [x] Update `advance_leg` endpoint: pass the genlocke's `naming_scheme` to the new leg's `NuzlockeRun`
|
||||||
|
- [x] Add roman numeral helper function (e.g., in `services/naming.py`)
|
||||||
|
- [x] Update `get_name_suggestions` endpoint to accept optional `pokemon_id` param
|
||||||
|
- [x] Implement lineage lookup: when in genlocke context with `pokemon_id`, query prior legs for evolution family matches (excluding transfers) and compute suggestion with roman numeral
|
||||||
|
- [ ] Add tests for lineage-aware name suggestions
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [x] Update `CreateGenlockeInput` type to include `namingScheme?: string | null`
|
||||||
|
- [x] Add naming scheme selector to genlocke creation wizard (in the Rules step or as a new step)
|
||||||
|
- [x] Update `GenlockeResponse` / `GenlockeDetailResponse` types to include `namingScheme`
|
||||||
|
- [x] Update `EncounterModal` to pass selected `pokemonId` to name suggestions API when in genlocke context
|
||||||
|
- [x] Update `getNameSuggestions` API client to accept optional `pokemonId` param
|
||||||
|
- [x] Display lineage suggestion prominently in the suggestions UI (e.g., first pill with distinct styling)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""add naming_scheme to genlockes
|
||||||
|
|
||||||
|
Revision ID: f7a8b9c0d1e2
|
||||||
|
Revises: e5f70a1ca323
|
||||||
|
Create Date: 2026-02-14 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "f7a8b9c0d1e2"
|
||||||
|
down_revision: str | Sequence[str] | None = "e5f70a1ca323"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Add naming_scheme column to genlockes table."""
|
||||||
|
op.add_column(
|
||||||
|
"genlockes",
|
||||||
|
sa.Column("naming_scheme", sa.String(50), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Remove naming_scheme column from genlockes table."""
|
||||||
|
op.drop_column("genlockes", "naming_scheme")
|
||||||
@@ -458,6 +458,7 @@ async def create_genlocke(
|
|||||||
status="active",
|
status="active",
|
||||||
genlocke_rules=data.genlocke_rules,
|
genlocke_rules=data.genlocke_rules,
|
||||||
nuzlocke_rules=data.nuzlocke_rules,
|
nuzlocke_rules=data.nuzlocke_rules,
|
||||||
|
naming_scheme=data.naming_scheme,
|
||||||
)
|
)
|
||||||
session.add(genlocke)
|
session.add(genlocke)
|
||||||
await session.flush() # get genlocke.id
|
await session.flush() # get genlocke.id
|
||||||
@@ -480,6 +481,7 @@ async def create_genlocke(
|
|||||||
name=f"{data.name.strip()} \u2014 Leg 1",
|
name=f"{data.name.strip()} \u2014 Leg 1",
|
||||||
status="active",
|
status="active",
|
||||||
rules=data.nuzlocke_rules,
|
rules=data.nuzlocke_rules,
|
||||||
|
naming_scheme=data.naming_scheme,
|
||||||
)
|
)
|
||||||
session.add(first_run)
|
session.add(first_run)
|
||||||
await session.flush() # get first_run.id
|
await session.flush() # get first_run.id
|
||||||
@@ -653,6 +655,7 @@ async def advance_leg(
|
|||||||
name=f"{genlocke.name} \u2014 Leg {next_leg.leg_order}",
|
name=f"{genlocke.name} \u2014 Leg {next_leg.leg_order}",
|
||||||
status="active",
|
status="active",
|
||||||
rules=genlocke.nuzlocke_rules,
|
rules=genlocke.nuzlocke_rules,
|
||||||
|
naming_scheme=genlocke.naming_scheme,
|
||||||
)
|
)
|
||||||
session.add(new_run)
|
session.add(new_run)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|||||||
@@ -8,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.boss_result import BossResult
|
from app.models.boss_result import BossResult
|
||||||
from app.models.encounter import Encounter
|
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 GenlockeLeg
|
from app.models.genlocke import GenlockeLeg
|
||||||
from app.models.genlocke_transfer import GenlockeTransfer
|
from app.models.genlocke_transfer import GenlockeTransfer
|
||||||
@@ -19,7 +20,13 @@ from app.schemas.run import (
|
|||||||
RunResponse,
|
RunResponse,
|
||||||
RunUpdate,
|
RunUpdate,
|
||||||
)
|
)
|
||||||
from app.services.naming import get_naming_categories, suggest_names
|
from app.services.families import build_families
|
||||||
|
from app.services.naming import (
|
||||||
|
get_naming_categories,
|
||||||
|
strip_roman_suffix,
|
||||||
|
suggest_names,
|
||||||
|
to_roman,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -33,6 +40,7 @@ async def list_naming_categories():
|
|||||||
async def get_name_suggestions(
|
async def get_name_suggestions(
|
||||||
run_id: int,
|
run_id: int,
|
||||||
count: int = 10,
|
count: int = 10,
|
||||||
|
pokemon_id: int | None = None,
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
):
|
):
|
||||||
run = await session.get(NuzlockeRun, run_id)
|
run = await session.get(NuzlockeRun, run_id)
|
||||||
@@ -51,7 +59,102 @@ async def get_name_suggestions(
|
|||||||
)
|
)
|
||||||
used_names = {row[0] for row in result}
|
used_names = {row[0] for row in result}
|
||||||
|
|
||||||
return suggest_names(run.naming_scheme, used_names, count)
|
lineage_suggestion: str | None = None
|
||||||
|
|
||||||
|
# Lineage-aware suggestion: check if this run belongs to a genlocke
|
||||||
|
if pokemon_id is not None:
|
||||||
|
lineage_suggestion = await _compute_lineage_suggestion(
|
||||||
|
session, run_id, pokemon_id
|
||||||
|
)
|
||||||
|
|
||||||
|
suggestions = suggest_names(run.naming_scheme, used_names, count)
|
||||||
|
|
||||||
|
if lineage_suggestion and lineage_suggestion not in suggestions:
|
||||||
|
suggestions.insert(0, lineage_suggestion)
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
|
async def _compute_lineage_suggestion(
|
||||||
|
session: AsyncSession,
|
||||||
|
run_id: int,
|
||||||
|
pokemon_id: int,
|
||||||
|
) -> str | None:
|
||||||
|
"""Check previous genlocke legs for the same evolution family and suggest a name with roman numeral."""
|
||||||
|
# Find the genlocke leg for this run
|
||||||
|
leg_result = await session.execute(
|
||||||
|
select(GenlockeLeg).where(GenlockeLeg.run_id == run_id)
|
||||||
|
)
|
||||||
|
current_leg = leg_result.scalar_one_or_none()
|
||||||
|
if current_leg is None or current_leg.leg_order <= 1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Build evolution family map
|
||||||
|
evo_result = await session.execute(select(Evolution))
|
||||||
|
evolutions = evo_result.scalars().all()
|
||||||
|
pokemon_to_family = build_families(evolutions)
|
||||||
|
|
||||||
|
family_ids = set(pokemon_to_family.get(pokemon_id, [pokemon_id]))
|
||||||
|
family_ids.add(pokemon_id)
|
||||||
|
|
||||||
|
# Get run IDs for all previous legs
|
||||||
|
prev_legs_result = await session.execute(
|
||||||
|
select(GenlockeLeg.run_id).where(
|
||||||
|
GenlockeLeg.genlocke_id == current_leg.genlocke_id,
|
||||||
|
GenlockeLeg.leg_order < current_leg.leg_order,
|
||||||
|
GenlockeLeg.run_id.isnot(None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
prev_run_ids = [row[0] for row in prev_legs_result]
|
||||||
|
if not prev_run_ids:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get transfer target encounter IDs (these are not "original" catches)
|
||||||
|
transfer_targets_result = await session.execute(
|
||||||
|
select(GenlockeTransfer.target_encounter_id).where(
|
||||||
|
GenlockeTransfer.genlocke_id == current_leg.genlocke_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
transfer_target_ids = {row[0] for row in transfer_targets_result}
|
||||||
|
|
||||||
|
# Find original (non-transfer) encounters from previous legs matching this family
|
||||||
|
enc_result = await session.execute(
|
||||||
|
select(Encounter.id, Encounter.nickname, Encounter.run_id).where(
|
||||||
|
Encounter.run_id.in_(prev_run_ids),
|
||||||
|
Encounter.pokemon_id.in_(family_ids),
|
||||||
|
Encounter.status == "caught",
|
||||||
|
Encounter.nickname.isnot(None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
matches = [
|
||||||
|
(row[0], row[1], row[2])
|
||||||
|
for row in enc_result
|
||||||
|
if row[0] not in transfer_target_ids
|
||||||
|
]
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Use the nickname from the first encounter (earliest leg)
|
||||||
|
# Build run_id -> leg_order mapping for sorting
|
||||||
|
leg_order_result = await session.execute(
|
||||||
|
select(GenlockeLeg.run_id, GenlockeLeg.leg_order).where(
|
||||||
|
GenlockeLeg.genlocke_id == current_leg.genlocke_id,
|
||||||
|
GenlockeLeg.run_id.in_(prev_run_ids),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
run_to_leg_order = {row[0]: row[1] for row in leg_order_result}
|
||||||
|
|
||||||
|
# Sort by leg order to find the first appearance
|
||||||
|
matches.sort(key=lambda m: run_to_leg_order.get(m[2], 0))
|
||||||
|
base_name = strip_roman_suffix(matches[0][1])
|
||||||
|
|
||||||
|
# Count distinct legs with original encounters for this family
|
||||||
|
legs_with_family = len({run_to_leg_order.get(m[2]) for m in matches})
|
||||||
|
|
||||||
|
# The new one would be the next numeral (legs_with_family + 1)
|
||||||
|
numeral = to_roman(legs_with_family + 1)
|
||||||
|
return f"{base_name} {numeral}"
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=RunResponse, status_code=201)
|
@router.post("", response_model=RunResponse, status_code=201)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class Genlocke(Base):
|
|||||||
) # active, completed, failed
|
) # active, completed, failed
|
||||||
genlocke_rules: Mapped[dict] = mapped_column(JSONB, default=dict)
|
genlocke_rules: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||||
nuzlocke_rules: Mapped[dict] = mapped_column(JSONB, default=dict)
|
nuzlocke_rules: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||||
|
naming_scheme: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), server_default=func.now()
|
DateTime(timezone=True), server_default=func.now()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class GenlockeCreate(CamelModel):
|
|||||||
game_ids: list[int]
|
game_ids: list[int]
|
||||||
genlocke_rules: dict = {}
|
genlocke_rules: dict = {}
|
||||||
nuzlocke_rules: dict = {}
|
nuzlocke_rules: dict = {}
|
||||||
|
naming_scheme: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class GenlockeUpdate(CamelModel):
|
class GenlockeUpdate(CamelModel):
|
||||||
@@ -51,6 +52,7 @@ class GenlockeResponse(CamelModel):
|
|||||||
status: str
|
status: str
|
||||||
genlocke_rules: dict
|
genlocke_rules: dict
|
||||||
nuzlocke_rules: dict
|
nuzlocke_rules: dict
|
||||||
|
naming_scheme: str | None = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
legs: list[GenlockeLegResponse] = []
|
legs: list[GenlockeLegResponse] = []
|
||||||
|
|
||||||
@@ -98,6 +100,7 @@ class GenlockeDetailResponse(CamelModel):
|
|||||||
status: str
|
status: str
|
||||||
genlocke_rules: dict
|
genlocke_rules: dict
|
||||||
nuzlocke_rules: dict
|
nuzlocke_rules: dict
|
||||||
|
naming_scheme: str | None = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
legs: list[GenlockeLegDetailResponse] = []
|
legs: list[GenlockeLegDetailResponse] = []
|
||||||
stats: GenlockeStatsResponse
|
stats: GenlockeStatsResponse
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -26,6 +27,42 @@ def get_words_for_category(category: str) -> list[str]:
|
|||||||
return _load_dictionary().get(category, [])
|
return _load_dictionary().get(category, [])
|
||||||
|
|
||||||
|
|
||||||
|
_ROMAN_NUMERALS = [
|
||||||
|
(1000, "M"),
|
||||||
|
(900, "CM"),
|
||||||
|
(500, "D"),
|
||||||
|
(400, "CD"),
|
||||||
|
(100, "C"),
|
||||||
|
(90, "XC"),
|
||||||
|
(50, "L"),
|
||||||
|
(40, "XL"),
|
||||||
|
(10, "X"),
|
||||||
|
(9, "IX"),
|
||||||
|
(5, "V"),
|
||||||
|
(4, "IV"),
|
||||||
|
(1, "I"),
|
||||||
|
]
|
||||||
|
|
||||||
|
_ROMAN_SUFFIX_RE = re.compile(
|
||||||
|
r"\s+(M{0,3}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3}))$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_roman(n: int) -> str:
|
||||||
|
"""Convert a positive integer to a roman numeral string."""
|
||||||
|
parts: list[str] = []
|
||||||
|
for value, numeral in _ROMAN_NUMERALS:
|
||||||
|
while n >= value:
|
||||||
|
parts.append(numeral)
|
||||||
|
n -= value
|
||||||
|
return "".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_roman_suffix(name: str) -> str:
|
||||||
|
"""Remove a trailing roman numeral suffix from a name (e.g., 'Heracles II' -> 'Heracles')."""
|
||||||
|
return _ROMAN_SUFFIX_RE.sub("", name).strip()
|
||||||
|
|
||||||
|
|
||||||
def suggest_names(
|
def suggest_names(
|
||||||
category: str,
|
category: str,
|
||||||
used_names: set[str],
|
used_names: set[str],
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ export function getNamingCategories(): Promise<string[]> {
|
|||||||
return api.get('/runs/naming-categories')
|
return api.get('/runs/naming-categories')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNameSuggestions(runId: number, count = 10): Promise<string[]> {
|
export function getNameSuggestions(runId: number, count = 10, pokemonId?: number): Promise<string[]> {
|
||||||
return api.get(`/runs/${runId}/name-suggestions?count=${count}`)
|
let url = `/runs/${runId}/name-suggestions?count=${count}`
|
||||||
|
if (pokemonId != null) {
|
||||||
|
url += `&pokemon_id=${pokemonId}`
|
||||||
|
}
|
||||||
|
return api.get(url)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface EncounterModalProps {
|
|||||||
gameId: number
|
gameId: number
|
||||||
runId: number
|
runId: number
|
||||||
namingScheme?: string | null
|
namingScheme?: string | null
|
||||||
|
isGenlocke?: boolean
|
||||||
existing?: EncounterDetail
|
existing?: EncounterDetail
|
||||||
dupedPokemonIds?: Set<number>
|
dupedPokemonIds?: Set<number>
|
||||||
retiredPokemonIds?: Set<number>
|
retiredPokemonIds?: Set<number>
|
||||||
@@ -97,6 +98,7 @@ export function EncounterModal({
|
|||||||
gameId,
|
gameId,
|
||||||
runId,
|
runId,
|
||||||
namingScheme,
|
namingScheme,
|
||||||
|
isGenlocke,
|
||||||
existing,
|
existing,
|
||||||
dupedPokemonIds,
|
dupedPokemonIds,
|
||||||
retiredPokemonIds,
|
retiredPokemonIds,
|
||||||
@@ -126,8 +128,9 @@ export function EncounterModal({
|
|||||||
const isEditing = !!existing
|
const isEditing = !!existing
|
||||||
|
|
||||||
const showSuggestions = !!namingScheme && status === 'caught' && !isEditing
|
const showSuggestions = !!namingScheme && status === 'caught' && !isEditing
|
||||||
|
const lineagePokemonId = isGenlocke && selectedPokemon ? selectedPokemon.pokemonId : null
|
||||||
const { data: suggestions, refetch: regenerate, isFetching: loadingSuggestions } =
|
const { data: suggestions, refetch: regenerate, isFetching: loadingSuggestions } =
|
||||||
useNameSuggestions(showSuggestions ? runId : null)
|
useNameSuggestions(showSuggestions ? runId : null, lineagePokemonId)
|
||||||
|
|
||||||
// Pre-select pokemon when editing
|
// Pre-select pokemon when editing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ export function useNamingCategories() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useNameSuggestions(runId: number | null) {
|
export function useNameSuggestions(runId: number | null, pokemonId?: number | null) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['name-suggestions', runId],
|
queryKey: ['name-suggestions', runId, pokemonId ?? null],
|
||||||
queryFn: () => getNameSuggestions(runId!),
|
queryFn: () => getNameSuggestions(runId!, 10, pokemonId ?? undefined),
|
||||||
enabled: runId !== null,
|
enabled: runId !== null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { RulesConfiguration, StepIndicator } from '../components'
|
import { RulesConfiguration, StepIndicator } from '../components'
|
||||||
import { useRegions, useCreateGenlocke } from '../hooks/useGenlockes'
|
import { useRegions, useCreateGenlocke } from '../hooks/useGenlockes'
|
||||||
|
import { useNamingCategories } from '../hooks/useRuns'
|
||||||
import type { Game, GenlockeRules, Region } from '../types'
|
import type { Game, GenlockeRules, Region } from '../types'
|
||||||
import { DEFAULT_RULES } from '../types'
|
import { DEFAULT_RULES } from '../types'
|
||||||
import type { NuzlockeRules } from '../types/rules'
|
import type { NuzlockeRules } from '../types/rules'
|
||||||
@@ -46,6 +47,8 @@ export function NewGenlocke() {
|
|||||||
const [preset, setPreset] = useState<PresetType>(null)
|
const [preset, setPreset] = useState<PresetType>(null)
|
||||||
const [nuzlockeRules, setNuzlockeRules] = useState<NuzlockeRules>(DEFAULT_RULES)
|
const [nuzlockeRules, setNuzlockeRules] = useState<NuzlockeRules>(DEFAULT_RULES)
|
||||||
const [genlockeRules, setGenlockeRules] = useState<GenlockeRules>({ retireHoF: false })
|
const [genlockeRules, setGenlockeRules] = useState<GenlockeRules>({ retireHoF: false })
|
||||||
|
const [namingScheme, setNamingScheme] = useState<string | null>(null)
|
||||||
|
const { data: namingCategories } = useNamingCategories()
|
||||||
|
|
||||||
const handlePresetSelect = (type: PresetType) => {
|
const handlePresetSelect = (type: PresetType) => {
|
||||||
setPreset(type)
|
setPreset(type)
|
||||||
@@ -91,6 +94,7 @@ export function NewGenlocke() {
|
|||||||
gameIds: legs.map((l) => l.game.id),
|
gameIds: legs.map((l) => l.game.id),
|
||||||
genlockeRules,
|
genlockeRules,
|
||||||
nuzlockeRules,
|
nuzlockeRules,
|
||||||
|
namingScheme,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -323,6 +327,32 @@ export function NewGenlocke() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Naming scheme */}
|
||||||
|
<div className="mt-6 bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||||
|
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
Naming Scheme
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Get nickname suggestions from a themed word list when catching Pokemon. Applied to all legs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-4">
|
||||||
|
<select
|
||||||
|
value={namingScheme ?? ''}
|
||||||
|
onChange={(e) => setNamingScheme(e.target.value || null)}
|
||||||
|
className="w-full max-w-xs px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">None (manual nicknames)</option>
|
||||||
|
{namingCategories?.map((cat) => (
|
||||||
|
<option key={cat} value={cat}>
|
||||||
|
{cat.charAt(0).toUpperCase() + cat.slice(1)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex justify-between">
|
<div className="mt-6 flex justify-between">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -398,6 +428,14 @@ export function NewGenlocke() {
|
|||||||
{genlockeRules.retireHoF ? 'Retire' : 'Keep'}
|
{genlockeRules.retireHoF ? 'Retire' : 'Keep'}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<dt className="text-gray-600 dark:text-gray-400">Naming Scheme</dt>
|
||||||
|
<dd className="text-gray-900 dark:text-gray-100 font-medium">
|
||||||
|
{namingScheme
|
||||||
|
? namingScheme.charAt(0).toUpperCase() + namingScheme.slice(1)
|
||||||
|
: 'None'}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1437,6 +1437,7 @@ export function RunEncounters() {
|
|||||||
gameId={run!.gameId}
|
gameId={run!.gameId}
|
||||||
runId={runIdNum}
|
runId={runIdNum}
|
||||||
namingScheme={run!.namingScheme}
|
namingScheme={run!.namingScheme}
|
||||||
|
isGenlocke={!!run!.genlocke}
|
||||||
existing={editingEncounter ?? undefined}
|
existing={editingEncounter ?? undefined}
|
||||||
dupedPokemonIds={dupedPokemonIds}
|
dupedPokemonIds={dupedPokemonIds}
|
||||||
retiredPokemonIds={retiredPokemonIds}
|
retiredPokemonIds={retiredPokemonIds}
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ export interface Genlocke {
|
|||||||
status: 'active' | 'completed' | 'failed'
|
status: 'active' | 'completed' | 'failed'
|
||||||
genlockeRules: GenlockeRules
|
genlockeRules: GenlockeRules
|
||||||
nuzlockeRules: NuzlockeRules
|
nuzlockeRules: NuzlockeRules
|
||||||
|
namingScheme: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
legs: GenlockeLeg[]
|
legs: GenlockeLeg[]
|
||||||
}
|
}
|
||||||
@@ -239,6 +240,7 @@ export interface CreateGenlockeInput {
|
|||||||
gameIds: number[]
|
gameIds: number[]
|
||||||
genlockeRules: GenlockeRules
|
genlockeRules: GenlockeRules
|
||||||
nuzlockeRules: NuzlockeRules
|
nuzlockeRules: NuzlockeRules
|
||||||
|
namingScheme?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Genlocke list / detail types
|
// Genlocke list / detail types
|
||||||
@@ -283,6 +285,7 @@ export interface GenlockeDetail {
|
|||||||
status: 'active' | 'completed' | 'failed'
|
status: 'active' | 'completed' | 'failed'
|
||||||
genlockeRules: GenlockeRules
|
genlockeRules: GenlockeRules
|
||||||
nuzlockeRules: NuzlockeRules
|
nuzlockeRules: NuzlockeRules
|
||||||
|
namingScheme: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
legs: GenlockeLegDetail[]
|
legs: GenlockeLegDetail[]
|
||||||
stats: GenlockeStats
|
stats: GenlockeStats
|
||||||
|
|||||||
Reference in New Issue
Block a user