Add Pinwheel Clause support for zone-based encounters in route groups
Allows each sub-zone within a route group to have its own independent encounter when the Pinwheel Clause rule is enabled (default on), instead of the entire group sharing a single encounter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
# nuzlocke-tracker-i08l
|
||||||
|
title: Implement Pinwheel Clause support
|
||||||
|
status: completed
|
||||||
|
type: feature
|
||||||
|
priority: normal
|
||||||
|
created_at: 2026-02-07T19:18:34Z
|
||||||
|
updated_at: 2026-02-07T19:21:45Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Add pinwheel_zone column to routes, pinwheelClause toggle to NuzlockeRules, zone-aware encounter locking on frontend and backend.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [x] Alembic migration for pinwheel_zone column
|
||||||
|
- [x] SQLAlchemy model update
|
||||||
|
- [x] Pydantic schema updates
|
||||||
|
- [x] Route list API helper update
|
||||||
|
- [x] Encounter creation API zone-aware sibling check
|
||||||
|
- [x] Seed loader update
|
||||||
|
- [x] Seed data for Pinwheel Forest zones
|
||||||
|
- [x] NuzlockeRules per-run toggle
|
||||||
|
- [x] Frontend types (game.ts, admin.ts)
|
||||||
|
- [x] Admin route form pinwheelZone input
|
||||||
|
- [x] Encounter page zone-aware locking, counts, and filtering
|
||||||
|
- [x] getZoneEncounters helper
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""add pinwheel clause support
|
||||||
|
|
||||||
|
Revision ID: a1b2c3d4e5f7
|
||||||
|
Revises: f6a7b8c9d0e1
|
||||||
|
Create Date: 2026-02-07 12:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'a1b2c3d4e5f7'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = 'f6a7b8c9d0e1'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
'routes',
|
||||||
|
sa.Column('pinwheel_zone', sa.SmallInteger(), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('routes', 'pinwheel_zone')
|
||||||
@@ -50,15 +50,30 @@ async def create_encounter(
|
|||||||
detail="Cannot create encounter on a parent route. Use a child route instead.",
|
detail="Cannot create encounter on a parent route. Use a child route instead.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# If this route has a parent, check if any sibling already has an encounter
|
# If this route has a parent, check if sibling already has an encounter
|
||||||
if route.parent_route_id is not None:
|
if route.parent_route_id is not None:
|
||||||
# Get all sibling route IDs (routes with same parent, including this one)
|
# Get all sibling routes (routes with same parent, including this one)
|
||||||
siblings_result = await session.execute(
|
siblings_result = await session.execute(
|
||||||
select(Route.id).where(Route.parent_route_id == route.parent_route_id)
|
select(Route).where(Route.parent_route_id == route.parent_route_id)
|
||||||
)
|
)
|
||||||
sibling_ids = [r for r in siblings_result.scalars().all()]
|
siblings = siblings_result.scalars().all()
|
||||||
|
|
||||||
# Check if any sibling already has an encounter in this run
|
# Determine which siblings to check based on pinwheel clause
|
||||||
|
pinwheel_on = run.rules.get("pinwheelClause", True) if run.rules else True
|
||||||
|
any_has_zone = any(s.pinwheel_zone is not None for s in siblings)
|
||||||
|
|
||||||
|
if pinwheel_on and any_has_zone:
|
||||||
|
# Zone-aware: only check siblings in the same zone (null treated as 0)
|
||||||
|
my_zone = route.pinwheel_zone if route.pinwheel_zone is not None else 0
|
||||||
|
sibling_ids = [
|
||||||
|
s.id for s in siblings
|
||||||
|
if (s.pinwheel_zone if s.pinwheel_zone is not None else 0) == my_zone
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
# No pinwheel clause or no zones defined: all siblings share
|
||||||
|
sibling_ids = [s.id for s in siblings]
|
||||||
|
|
||||||
|
# Check if any relevant sibling already has an encounter in this run
|
||||||
existing_encounter = await session.execute(
|
existing_encounter = await session.execute(
|
||||||
select(Encounter)
|
select(Encounter)
|
||||||
.where(
|
.where(
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ async def list_game_routes(
|
|||||||
"game_id": route.game_id,
|
"game_id": route.game_id,
|
||||||
"order": route.order,
|
"order": route.order,
|
||||||
"parent_route_id": route.parent_route_id,
|
"parent_route_id": route.parent_route_id,
|
||||||
|
"pinwheel_zone": route.pinwheel_zone,
|
||||||
"encounter_methods": methods,
|
"encounter_methods": methods,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class Route(Base):
|
|||||||
parent_route_id: Mapped[int | None] = mapped_column(
|
parent_route_id: Mapped[int | None] = mapped_column(
|
||||||
ForeignKey("routes.id", ondelete="CASCADE"), index=True, default=None
|
ForeignKey("routes.id", ondelete="CASCADE"), index=True, default=None
|
||||||
)
|
)
|
||||||
|
pinwheel_zone: Mapped[int | None] = mapped_column(SmallInteger, default=None)
|
||||||
|
|
||||||
game: Mapped["Game"] = relationship(back_populates="routes")
|
game: Mapped["Game"] = relationship(back_populates="routes")
|
||||||
route_encounters: Mapped[list["RouteEncounter"]] = relationship(
|
route_encounters: Mapped[list["RouteEncounter"]] = relationship(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class RouteResponse(CamelModel):
|
|||||||
game_id: int
|
game_id: int
|
||||||
order: int
|
order: int
|
||||||
parent_route_id: int | None = None
|
parent_route_id: int | None = None
|
||||||
|
pinwheel_zone: int | None = None
|
||||||
encounter_methods: list[str] = []
|
encounter_methods: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
@@ -56,12 +57,14 @@ class RouteCreate(CamelModel):
|
|||||||
name: str
|
name: str
|
||||||
order: int
|
order: int
|
||||||
parent_route_id: int | None = None
|
parent_route_id: int | None = None
|
||||||
|
pinwheel_zone: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class RouteUpdate(CamelModel):
|
class RouteUpdate(CamelModel):
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
order: int | None = None
|
order: int | None = None
|
||||||
parent_route_id: int | None = None
|
parent_route_id: int | None = None
|
||||||
|
pinwheel_zone: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class RouteReorderItem(CamelModel):
|
class RouteReorderItem(CamelModel):
|
||||||
|
|||||||
@@ -1333,6 +1333,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Pinwheel Forest (Outside)",
|
"name": "Pinwheel Forest (Outside)",
|
||||||
"order": 23,
|
"order": 23,
|
||||||
|
"pinwheel_zone": 1,
|
||||||
"encounters": [
|
"encounters": [
|
||||||
{
|
{
|
||||||
"pokeapi_id": 533,
|
"pokeapi_id": 533,
|
||||||
@@ -1379,6 +1380,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Pinwheel Forest (Inside)",
|
"name": "Pinwheel Forest (Inside)",
|
||||||
"order": 24,
|
"order": 24,
|
||||||
|
"pinwheel_zone": 2,
|
||||||
"encounters": [
|
"encounters": [
|
||||||
{
|
{
|
||||||
"pokeapi_id": 550,
|
"pokeapi_id": 550,
|
||||||
|
|||||||
@@ -1321,6 +1321,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Pinwheel Forest (Outside)",
|
"name": "Pinwheel Forest (Outside)",
|
||||||
"order": 25,
|
"order": 25,
|
||||||
|
"pinwheel_zone": 1,
|
||||||
"encounters": [
|
"encounters": [
|
||||||
{
|
{
|
||||||
"pokeapi_id": 535,
|
"pokeapi_id": 535,
|
||||||
@@ -1359,6 +1360,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Pinwheel Forest (Inside)",
|
"name": "Pinwheel Forest (Inside)",
|
||||||
"order": 26,
|
"order": 26,
|
||||||
|
"pinwheel_zone": 2,
|
||||||
"encounters": [
|
"encounters": [
|
||||||
{
|
{
|
||||||
"pokeapi_id": 550,
|
"pokeapi_id": 550,
|
||||||
|
|||||||
@@ -1333,6 +1333,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Pinwheel Forest (Outside)",
|
"name": "Pinwheel Forest (Outside)",
|
||||||
"order": 23,
|
"order": 23,
|
||||||
|
"pinwheel_zone": 1,
|
||||||
"encounters": [
|
"encounters": [
|
||||||
{
|
{
|
||||||
"pokeapi_id": 533,
|
"pokeapi_id": 533,
|
||||||
@@ -1379,6 +1380,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Pinwheel Forest (Inside)",
|
"name": "Pinwheel Forest (Inside)",
|
||||||
"order": 24,
|
"order": 24,
|
||||||
|
"pinwheel_zone": 2,
|
||||||
"encounters": [
|
"encounters": [
|
||||||
{
|
{
|
||||||
"pokeapi_id": 10016,
|
"pokeapi_id": 10016,
|
||||||
|
|||||||
@@ -1321,6 +1321,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Pinwheel Forest (Outside)",
|
"name": "Pinwheel Forest (Outside)",
|
||||||
"order": 25,
|
"order": 25,
|
||||||
|
"pinwheel_zone": 1,
|
||||||
"encounters": [
|
"encounters": [
|
||||||
{
|
{
|
||||||
"pokeapi_id": 535,
|
"pokeapi_id": 535,
|
||||||
@@ -1359,6 +1360,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Pinwheel Forest (Inside)",
|
"name": "Pinwheel Forest (Inside)",
|
||||||
"order": 26,
|
"order": 26,
|
||||||
|
"pinwheel_zone": 2,
|
||||||
"encounters": [
|
"encounters": [
|
||||||
{
|
{
|
||||||
"pokeapi_id": 10016,
|
"pokeapi_id": 10016,
|
||||||
|
|||||||
@@ -109,9 +109,14 @@ async def upsert_routes(
|
|||||||
game_id=game_id,
|
game_id=game_id,
|
||||||
order=child["order"],
|
order=child["order"],
|
||||||
parent_route_id=parent_id,
|
parent_route_id=parent_id,
|
||||||
|
pinwheel_zone=child.get("pinwheel_zone"),
|
||||||
).on_conflict_do_update(
|
).on_conflict_do_update(
|
||||||
constraint="uq_routes_game_name",
|
constraint="uq_routes_game_name",
|
||||||
set_={"order": child["order"], "parent_route_id": parent_id},
|
set_={
|
||||||
|
"order": child["order"],
|
||||||
|
"parent_route_id": parent_id,
|
||||||
|
"pinwheel_zone": child.get("pinwheel_zone"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
await session.execute(stmt)
|
await session.execute(stmt)
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,17 @@ interface RouteFormModalProps {
|
|||||||
export function RouteFormModal({ route, nextOrder, onSubmit, onClose, isSubmitting }: RouteFormModalProps) {
|
export function RouteFormModal({ route, nextOrder, onSubmit, onClose, isSubmitting }: RouteFormModalProps) {
|
||||||
const [name, setName] = useState(route?.name ?? '')
|
const [name, setName] = useState(route?.name ?? '')
|
||||||
const [order, setOrder] = useState(String(route?.order ?? nextOrder ?? 0))
|
const [order, setOrder] = useState(String(route?.order ?? nextOrder ?? 0))
|
||||||
|
const [pinwheelZone, setPinwheelZone] = useState(
|
||||||
|
route?.pinwheelZone != null ? String(route.pinwheelZone) : ''
|
||||||
|
)
|
||||||
|
|
||||||
const handleSubmit = (e: FormEvent) => {
|
const handleSubmit = (e: FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onSubmit({ name, order: Number(order) })
|
onSubmit({
|
||||||
|
name,
|
||||||
|
order: Number(order),
|
||||||
|
pinwheelZone: pinwheelZone !== '' ? Number(pinwheelZone) : null,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,6 +54,20 @@ export function RouteFormModal({ route, nextOrder, onSubmit, onClose, isSubmitti
|
|||||||
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Pinwheel Zone</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={pinwheelZone}
|
||||||
|
onChange={(e) => setPinwheelZone(e.target.value)}
|
||||||
|
placeholder="None"
|
||||||
|
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Routes in the same zone share an encounter when the Pinwheel Clause is active
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,40 @@ function getGroupEncounter(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether any child in this group has a pinwheelZone set. */
|
||||||
|
function groupHasZones(group: RouteWithChildren): boolean {
|
||||||
|
return group.children.some((c) => c.pinwheelZone != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the effective zone for a route (null treated as 0). */
|
||||||
|
function effectiveZone(route: Route): number {
|
||||||
|
return route.pinwheelZone ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get encounters grouped by zone within a route group.
|
||||||
|
* Returns a Map from zone number to the encounter in that zone.
|
||||||
|
*/
|
||||||
|
function getZoneEncounters(
|
||||||
|
group: RouteWithChildren,
|
||||||
|
encounterByRoute: Map<number, EncounterDetail>,
|
||||||
|
): Map<number, EncounterDetail> {
|
||||||
|
const zoneMap = new Map<number, EncounterDetail>()
|
||||||
|
for (const child of group.children) {
|
||||||
|
const enc = encounterByRoute.get(child.id)
|
||||||
|
if (enc) {
|
||||||
|
zoneMap.set(effectiveZone(child), enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return zoneMap
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Count distinct zones in a group. */
|
||||||
|
function countDistinctZones(group: RouteWithChildren): number {
|
||||||
|
const zones = new Set(group.children.map(effectiveZone))
|
||||||
|
return zones.size
|
||||||
|
}
|
||||||
|
|
||||||
interface RouteGroupProps {
|
interface RouteGroupProps {
|
||||||
group: RouteWithChildren
|
group: RouteWithChildren
|
||||||
encounterByRoute: Map<number, EncounterDetail>
|
encounterByRoute: Map<number, EncounterDetail>
|
||||||
@@ -110,6 +144,7 @@ interface RouteGroupProps {
|
|||||||
onToggleExpand: () => void
|
onToggleExpand: () => void
|
||||||
onRouteClick: (route: Route) => void
|
onRouteClick: (route: Route) => void
|
||||||
filter: 'all' | RouteStatus
|
filter: 'all' | RouteStatus
|
||||||
|
pinwheelClause: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function RouteGroup({
|
function RouteGroup({
|
||||||
@@ -119,14 +154,37 @@ function RouteGroup({
|
|||||||
onToggleExpand,
|
onToggleExpand,
|
||||||
onRouteClick,
|
onRouteClick,
|
||||||
filter,
|
filter,
|
||||||
|
pinwheelClause,
|
||||||
}: RouteGroupProps) {
|
}: RouteGroupProps) {
|
||||||
const groupEncounter = getGroupEncounter(group, encounterByRoute)
|
const groupEncounter = getGroupEncounter(group, encounterByRoute)
|
||||||
const groupStatus = groupEncounter ? groupEncounter.status : 'none'
|
const usePinwheel = pinwheelClause && groupHasZones(group)
|
||||||
|
const zoneEncounters = usePinwheel
|
||||||
|
? getZoneEncounters(group, encounterByRoute)
|
||||||
|
: null
|
||||||
|
|
||||||
|
// For pinwheel groups, determine status from all zone statuses
|
||||||
|
let groupStatus: RouteStatus
|
||||||
|
if (usePinwheel && zoneEncounters && zoneEncounters.size > 0) {
|
||||||
|
// Use the first encounter's status as representative for the header
|
||||||
|
groupStatus = groupEncounter ? groupEncounter.status : 'none'
|
||||||
|
} else {
|
||||||
|
groupStatus = groupEncounter ? groupEncounter.status : 'none'
|
||||||
|
}
|
||||||
const si = statusIndicator[groupStatus]
|
const si = statusIndicator[groupStatus]
|
||||||
|
|
||||||
// For groups, check if it matches the filter
|
// For groups, check if it matches the filter
|
||||||
if (filter !== 'all' && groupStatus !== filter) {
|
if (filter !== 'all') {
|
||||||
return null
|
if (usePinwheel) {
|
||||||
|
// Show group if any zone matches the filter
|
||||||
|
const anyChildMatches = group.children.some((child) => {
|
||||||
|
const enc = encounterByRoute.get(child.id)
|
||||||
|
return getRouteStatus(enc) === filter
|
||||||
|
})
|
||||||
|
// Also check children without encounters (for 'none' filter)
|
||||||
|
if (!anyChildMatches) return null
|
||||||
|
} else if (groupStatus !== filter) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasGroupEncounter = groupEncounter !== null
|
const hasGroupEncounter = groupEncounter !== null
|
||||||
@@ -192,7 +250,16 @@ function RouteGroup({
|
|||||||
const childEncounter = encounterByRoute.get(child.id)
|
const childEncounter = encounterByRoute.get(child.id)
|
||||||
const childStatus = getRouteStatus(childEncounter)
|
const childStatus = getRouteStatus(childEncounter)
|
||||||
const childSi = statusIndicator[childStatus]
|
const childSi = statusIndicator[childStatus]
|
||||||
const isDisabled = hasGroupEncounter && !childEncounter
|
|
||||||
|
let isDisabled: boolean
|
||||||
|
if (usePinwheel && zoneEncounters) {
|
||||||
|
// Zone-aware: only lock if this child's zone already has an encounter
|
||||||
|
const myZone = effectiveZone(child)
|
||||||
|
isDisabled = zoneEncounters.has(myZone) && !childEncounter
|
||||||
|
} else {
|
||||||
|
// Classic: whole group shares one encounter
|
||||||
|
isDisabled = hasGroupEncounter && !childEncounter
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -334,17 +401,32 @@ export function RunEncounters() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count completed locations (groups count as 1, standalone routes count as 1)
|
const pinwheelClause = run.rules?.pinwheelClause ?? true
|
||||||
const completedCount = organizedRoutes.filter((r) => {
|
|
||||||
if (r.children.length > 0) {
|
|
||||||
// It's a group - check if any child has an encounter
|
|
||||||
return getGroupEncounter(r, encounterByRoute) !== null
|
|
||||||
}
|
|
||||||
// Standalone route
|
|
||||||
return encounterByRoute.has(r.id)
|
|
||||||
}).length
|
|
||||||
|
|
||||||
const totalLocations = organizedRoutes.length
|
// Count completed locations (zone-aware when pinwheel clause is on)
|
||||||
|
let completedCount = 0
|
||||||
|
let totalLocations = 0
|
||||||
|
for (const r of organizedRoutes) {
|
||||||
|
if (r.children.length > 0) {
|
||||||
|
const usePinwheel = pinwheelClause && groupHasZones(r)
|
||||||
|
if (usePinwheel) {
|
||||||
|
const distinctZones = countDistinctZones(r)
|
||||||
|
const zoneEncs = getZoneEncounters(r, encounterByRoute)
|
||||||
|
totalLocations += distinctZones
|
||||||
|
completedCount += zoneEncs.size
|
||||||
|
} else {
|
||||||
|
totalLocations += 1
|
||||||
|
if (getGroupEncounter(r, encounterByRoute) !== null) {
|
||||||
|
completedCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
totalLocations += 1
|
||||||
|
if (encounterByRoute.has(r.id)) {
|
||||||
|
completedCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isActive = run.status === 'active'
|
const isActive = run.status === 'active'
|
||||||
const alive = run.encounters.filter(
|
const alive = run.encounters.filter(
|
||||||
@@ -413,7 +495,15 @@ export function RunEncounters() {
|
|||||||
if (filter === 'all') return true
|
if (filter === 'all') return true
|
||||||
|
|
||||||
if (r.children.length > 0) {
|
if (r.children.length > 0) {
|
||||||
// It's a group
|
const usePinwheel = pinwheelClause && groupHasZones(r)
|
||||||
|
if (usePinwheel) {
|
||||||
|
// Show group if any child/zone matches the filter
|
||||||
|
return r.children.some((child) => {
|
||||||
|
const enc = encounterByRoute.get(child.id)
|
||||||
|
return getRouteStatus(enc) === filter
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Classic: single status for whole group
|
||||||
const groupEnc = getGroupEncounter(r, encounterByRoute)
|
const groupEnc = getGroupEncounter(r, encounterByRoute)
|
||||||
return getRouteStatus(groupEnc ?? undefined) === filter
|
return getRouteStatus(groupEnc ?? undefined) === filter
|
||||||
}
|
}
|
||||||
@@ -665,6 +755,7 @@ export function RunEncounters() {
|
|||||||
onToggleExpand={() => toggleGroup(route.id)}
|
onToggleExpand={() => toggleGroup(route.id)}
|
||||||
onRouteClick={handleRouteClick}
|
onRouteClick={handleRouteClick}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
pinwheelClause={pinwheelClause}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ export interface UpdateGameInput {
|
|||||||
export interface CreateRouteInput {
|
export interface CreateRouteInput {
|
||||||
name: string
|
name: string
|
||||||
order: number
|
order: number
|
||||||
|
pinwheelZone?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateRouteInput {
|
export interface UpdateRouteInput {
|
||||||
name?: string
|
name?: string
|
||||||
order?: number
|
order?: number
|
||||||
|
pinwheelZone?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouteReorderItem {
|
export interface RouteReorderItem {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface Route {
|
|||||||
gameId: number
|
gameId: number
|
||||||
order: number
|
order: number
|
||||||
parentRouteId: number | null
|
parentRouteId: number | null
|
||||||
|
pinwheelZone: number | null
|
||||||
encounterMethods: string[]
|
encounterMethods: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface NuzlockeRules {
|
|||||||
nicknameRequired: boolean
|
nicknameRequired: boolean
|
||||||
duplicatesClause: boolean
|
duplicatesClause: boolean
|
||||||
shinyClause: boolean
|
shinyClause: boolean
|
||||||
|
pinwheelClause: boolean
|
||||||
|
|
||||||
// Difficulty modifiers
|
// Difficulty modifiers
|
||||||
hardcoreMode: boolean
|
hardcoreMode: boolean
|
||||||
@@ -19,6 +20,7 @@ export const DEFAULT_RULES: NuzlockeRules = {
|
|||||||
nicknameRequired: true,
|
nicknameRequired: true,
|
||||||
duplicatesClause: true,
|
duplicatesClause: true,
|
||||||
shinyClause: true,
|
shinyClause: true,
|
||||||
|
pinwheelClause: true,
|
||||||
|
|
||||||
// Difficulty modifiers - off by default
|
// Difficulty modifiers - off by default
|
||||||
hardcoreMode: false,
|
hardcoreMode: false,
|
||||||
@@ -70,6 +72,13 @@ export const RULE_DEFINITIONS: RuleDefinition[] = [
|
|||||||
'Shiny Pokémon may always be caught, regardless of whether they are your first encounter.',
|
'Shiny Pokémon may always be caught, regardless of whether they are your first encounter.',
|
||||||
category: 'core',
|
category: 'core',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'pinwheelClause',
|
||||||
|
name: 'Pinwheel Clause',
|
||||||
|
description:
|
||||||
|
'Sub-zones within a location group each get their own encounter instead of sharing one.',
|
||||||
|
category: 'core',
|
||||||
|
},
|
||||||
|
|
||||||
// Difficulty modifiers
|
// Difficulty modifiers
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user