Compare commits

...

2 Commits

Author SHA1 Message Date
9b9b189735 Update seed data
All checks were successful
CI / backend-lint (push) Successful in 9s
CI / frontend-lint (push) Successful in 33s
2026-02-14 22:43:44 +01:00
a482b27bca Refine bean oqfo: encounter rate display for time/weather variants
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 22:17:57 +01:00
19 changed files with 136 additions and 85 deletions

View File

@@ -1,32 +1,83 @@
--- ---
# nuzlocke-tracker-oqfo # nuzlocke-tracker-oqfo
title: Improve encounter rate display for time/weather variants title: Improve encounter rate display for time/weather variants
status: draft status: todo
type: feature type: feature
priority: normal
created_at: 2026-02-10T14:04:27Z created_at: 2026-02-10T14:04:27Z
updated_at: 2026-02-10T14:04:27Z updated_at: 2026-02-14T21:17:00Z
--- ---
Improve how encounter rates are displayed in the tracker to support time-of-day, weather, and seasonal variants that exist in many Pokemon games. ## Problem
## Context PokeDB data reveals that encounter rates vary by context across many games:
- **Gen 2/4 (G/S/C, HG/SS, D/P/Pt, BDSP):** morning/day/night
PokeDB.org data reveals that encounter rates vary significantly by context across different games: - **Gen 5 (B/W, B2/W2):** spring/summer/autumn/winter
- **Gen 2 / Gen 4 (G/S/C, HG/SS, D/P/Pt, BDSP):** rates vary by morning/day/night - **Gen 8 (Sw/Sh):** weather (clear, cloudy, rain, thunderstorm, snow, etc.)
- **Gen 5 (B/W, B2/W2):** rates vary by season (spring/summer/autumn/winter)
- **Gen 8 (Sw/Sh):** rates vary by weather (clear, cloudy, rain, thunderstorm, snow, etc.)
- **Gen 8 (Legends Arceus):** time + weather boolean conditions - **Gen 8 (Legends Arceus):** time + weather boolean conditions
- **Gen 9 (Sc/Vi):** overworld probability weights (not traditional encounter rates) - **Gen 9 (Sc/Vi):** overworld probability weights
Currently the seed format has a single `encounter_rate` field per encounter, which doesn't capture these variants. Currently the seed format and `RouteEncounter` model have a single `encounter_rate` field, which flattens all of this into one number.
## Goals ## Goal
- Design a display format that lets users see encounter rates for different conditions (e.g., tabs or tables for morning/day/night)
- Determine how to extend the seed data format to store variant rates Extend the data model and UI to support conditional encounter rates, so users can see which Pokemon appear under which conditions.
- Decide which level of detail is useful for Nuzlocke tracking (do players care about exact weather rates, or is "available during rain" sufficient?)
## Design
### Seed data format
Add an optional `conditions` field to encounter entries. When absent, the encounter has a flat rate (Gen 1/3/6 — no change needed). When present, it replaces `encounter_rate` with per-condition rates:
```json
{
"pokeapi_id": 163,
"pokemon_name": "Hoothoot",
"method": "walk",
"encounter_rate": null,
"conditions": {
"night": 50,
"morning": 10,
"day": 0
},
"min_level": 2,
"max_level": 5
}
```
For games without variant rates, the existing flat `encounter_rate` field is used unchanged.
### Backend changes
1. **New model `RouteEncounterCondition`** (one-to-many from `RouteEncounter`):
- `id`, `route_encounter_id` (FK), `condition` (string), `encounter_rate` (int)
- Conditions are free-form strings: `"morning"`, `"day"`, `"night"`, `"rain"`, `"spring"`, etc.
2. **`RouteEncounter` model**: keep `encounter_rate` as nullable — null when conditions exist, populated when flat.
3. **Seed loader**: detect `conditions` key in JSON, create `RouteEncounterCondition` rows accordingly.
4. **API**: include conditions in route encounter responses (nested array under each encounter).
### Frontend changes
1. **AdminRouteDetail**: show conditions as sub-rows or a tooltip when hovering the rate column.
2. **EncounterModal**: group by condition context when relevant (e.g. tabs for morning/day/night).
3. **Type updates**: extend `RouteEncounter` type with optional `conditions: { condition: string, encounterRate: number }[]`.
## Checklist
- [ ] Update seed JSON schema: add optional `conditions` field to encounter entries
- [ ] Create `RouteEncounterCondition` model with migration
- [ ] Make `RouteEncounter.encounter_rate` nullable
- [ ] Update seed loader to handle `conditions` entries
- [ ] Update API serialization to include conditions
- [ ] Update frontend types (`RouteEncounter`)
- [ ] Update AdminRouteDetail to display condition-based rates
- [ ] Update EncounterModal to show conditions contextually
- [ ] Update seed data for at least one game per variant type (HG/SS, B/W, Sw/Sh) as proof of concept
- [ ] Keep simple display for games with flat rates (no regression)
## Considerations ## Considerations
- Keep it simple for games with single rates (Gen 1, Gen 3, Gen 6)
- For Nuzlockes, the key question is usually "what can I encounter here?" — exact rates are secondary but useful for planning - For Nuzlocke play, availability ("appears during rain") matters more than exact percentages — consider a simplified view option
- The UI should not become cluttered for simple cases - Keep UI uncluttered for simple cases (Gen 1/3/6)
- This may affect the backend encounter model, seed format, and frontend display - Condition strings should use a consistent vocabulary (define an enum or reference list)
- Seed data updates for all games can be done incrementally after the infrastructure is in place

View File

@@ -1486,7 +1486,7 @@
] ]
}, },
{ {
"name": "Relic Castle (Volcaronas Room and Room Outside)", "name": "Relic Castle (Volcarona\u2019s Room and Room Outside)",
"order": 30, "order": 30,
"encounters": [ "encounters": [
{ {

View File

@@ -7689,7 +7689,7 @@
] ]
}, },
{ {
"name": "Pokémon League (Sinnoh)", "name": "Pok\u00e9mon League (Sinnoh)",
"order": 117, "order": 117,
"encounters": [ "encounters": [
{ {

View File

@@ -8028,7 +8028,7 @@
] ]
}, },
{ {
"name": "Pokémon League (Sinnoh)", "name": "Pok\u00e9mon League (Sinnoh)",
"order": 115, "order": 115,
"encounters": [ "encounters": [
{ {

View File

@@ -2423,12 +2423,12 @@
] ]
}, },
{ {
"name": "Pokémon Tower", "name": "Pok\u00e9mon Tower",
"order": 31, "order": 31,
"encounters": [], "encounters": [],
"children": [ "children": [
{ {
"name": "Pokémon Tower (3F)", "name": "Pok\u00e9mon Tower (3F)",
"order": 32, "order": 32,
"encounters": [ "encounters": [
{ {
@@ -2474,7 +2474,7 @@
] ]
}, },
{ {
"name": "Pokémon Tower (4F)", "name": "Pok\u00e9mon Tower (4F)",
"order": 33, "order": 33,
"encounters": [ "encounters": [
{ {
@@ -2520,7 +2520,7 @@
] ]
}, },
{ {
"name": "Pokémon Tower (5F)", "name": "Pok\u00e9mon Tower (5F)",
"order": 34, "order": 34,
"encounters": [ "encounters": [
{ {
@@ -2574,7 +2574,7 @@
] ]
}, },
{ {
"name": "Pokémon Tower (6F)", "name": "Pok\u00e9mon Tower (6F)",
"order": 35, "order": 35,
"encounters": [ "encounters": [
{ {
@@ -4335,12 +4335,12 @@
] ]
}, },
{ {
"name": "Pokémon Mansion (Kanto)", "name": "Pok\u00e9mon Mansion (Kanto)",
"order": 56, "order": 56,
"encounters": [], "encounters": [],
"children": [ "children": [
{ {
"name": "Pokémon Mansion (Kanto - 1F)", "name": "Pok\u00e9mon Mansion (Kanto - 1F)",
"order": 57, "order": 57,
"encounters": [ "encounters": [
{ {
@@ -4418,7 +4418,7 @@
] ]
}, },
{ {
"name": "Pokémon Mansion (Kanto - 2F)", "name": "Pok\u00e9mon Mansion (Kanto - 2F)",
"order": 58, "order": 58,
"encounters": [ "encounters": [
{ {
@@ -4496,7 +4496,7 @@
] ]
}, },
{ {
"name": "Pokémon Mansion (Kanto - 3F)", "name": "Pok\u00e9mon Mansion (Kanto - 3F)",
"order": 59, "order": 59,
"encounters": [ "encounters": [
{ {
@@ -4574,7 +4574,7 @@
] ]
}, },
{ {
"name": "Pokémon Mansion (Kanto - B1F)", "name": "Pok\u00e9mon Mansion (Kanto - B1F)",
"order": 60, "order": 60,
"encounters": [ "encounters": [
{ {

View File

@@ -2375,12 +2375,12 @@
] ]
}, },
{ {
"name": "Pokémon Tower", "name": "Pok\u00e9mon Tower",
"order": 31, "order": 31,
"encounters": [], "encounters": [],
"children": [ "children": [
{ {
"name": "Pokémon Tower (3F)", "name": "Pok\u00e9mon Tower (3F)",
"order": 32, "order": 32,
"encounters": [ "encounters": [
{ {
@@ -2426,7 +2426,7 @@
] ]
}, },
{ {
"name": "Pokémon Tower (4F)", "name": "Pok\u00e9mon Tower (4F)",
"order": 33, "order": 33,
"encounters": [ "encounters": [
{ {
@@ -2472,7 +2472,7 @@
] ]
}, },
{ {
"name": "Pokémon Tower (5F)", "name": "Pok\u00e9mon Tower (5F)",
"order": 34, "order": 34,
"encounters": [ "encounters": [
{ {
@@ -2526,7 +2526,7 @@
] ]
}, },
{ {
"name": "Pokémon Tower (6F)", "name": "Pok\u00e9mon Tower (6F)",
"order": 35, "order": 35,
"encounters": [ "encounters": [
{ {
@@ -4279,12 +4279,12 @@
] ]
}, },
{ {
"name": "Pokémon Mansion (Kanto)", "name": "Pok\u00e9mon Mansion (Kanto)",
"order": 56, "order": 56,
"encounters": [], "encounters": [],
"children": [ "children": [
{ {
"name": "Pokémon Mansion (Kanto - 1F)", "name": "Pok\u00e9mon Mansion (Kanto - 1F)",
"order": 57, "order": 57,
"encounters": [ "encounters": [
{ {
@@ -4346,7 +4346,7 @@
] ]
}, },
{ {
"name": "Pokémon Mansion (Kanto - 2F)", "name": "Pok\u00e9mon Mansion (Kanto - 2F)",
"order": 58, "order": 58,
"encounters": [ "encounters": [
{ {
@@ -4408,7 +4408,7 @@
] ]
}, },
{ {
"name": "Pokémon Mansion (Kanto - 3F)", "name": "Pok\u00e9mon Mansion (Kanto - 3F)",
"order": 59, "order": 59,
"encounters": [ "encounters": [
{ {
@@ -4470,7 +4470,7 @@
] ]
}, },
{ {
"name": "Pokémon Mansion (Kanto - B1F)", "name": "Pok\u00e9mon Mansion (Kanto - B1F)",
"order": 60, "order": 60,
"encounters": [ "encounters": [
{ {

View File

@@ -35,7 +35,7 @@
"encounters": [], "encounters": [],
"children": [ "children": [
{ {
"name": "Alola Route 1 (First two fields east of the players house)", "name": "Alola Route 1 (First two fields east of the player\u2019s house)",
"order": 3, "order": 3,
"encounters": [ "encounters": [
{ {
@@ -368,7 +368,7 @@
] ]
}, },
{ {
"name": "Trainers School (Alola)", "name": "Trainer\u2019s School (Alola)",
"order": 8, "order": 8,
"encounters": [ "encounters": [
{ {

View File

@@ -8028,7 +8028,7 @@
] ]
}, },
{ {
"name": "Pokémon League (Sinnoh)", "name": "Pok\u00e9mon League (Sinnoh)",
"order": 115, "order": 115,
"encounters": [ "encounters": [
{ {

View File

@@ -8356,7 +8356,7 @@
] ]
}, },
{ {
"name": "Pokémon League (Sinnoh)", "name": "Pok\u00e9mon League (Sinnoh)",
"order": 119, "order": 119,
"encounters": [ "encounters": [
{ {

View File

@@ -3875,7 +3875,7 @@
] ]
}, },
{ {
"name": "South Lake Miloch (Northwest of Bridge to Giants Seat)", "name": "South Lake Miloch (Northwest of Bridge to Giant\u2019s Seat)",
"order": 70, "order": 70,
"encounters": [ "encounters": [
{ {
@@ -3913,7 +3913,7 @@
] ]
}, },
{ {
"name": "South Lake Miloch (West of Bridge to Giants Seat)", "name": "South Lake Miloch (West of Bridge to Giant\u2019s Seat)",
"order": 71, "order": 71,
"encounters": [ "encounters": [
{ {
@@ -4133,7 +4133,7 @@
] ]
}, },
{ {
"name": "South Lake Miloch (By Giants Seat, Fishing Spot North of Bridge)", "name": "South Lake Miloch (By Giant\u2019s Seat, Fishing Spot North of Bridge)",
"order": 77, "order": 77,
"encounters": [ "encounters": [
{ {
@@ -12241,7 +12241,7 @@
] ]
}, },
{ {
"name": "Route 9 - Galar (Circhester Bay around icebergs northwest of Pokémon camp)", "name": "Route 9 - Galar (Circhester Bay around icebergs northwest of Pok\u00e9mon camp)",
"order": 212, "order": 212,
"encounters": [ "encounters": [
{ {
@@ -12255,7 +12255,7 @@
] ]
}, },
{ {
"name": "Route 9 - Galar (Circhester Bay in canal southwest of Pokémon camp)", "name": "Route 9 - Galar (Circhester Bay in canal southwest of Pok\u00e9mon camp)",
"order": 213, "order": 213,
"encounters": [ "encounters": [
{ {
@@ -12269,7 +12269,7 @@
] ]
}, },
{ {
"name": "Route 9 - Galar (Circhester Bay around iceberg between Trainer Tips signpost and Circhester Bay", "name": "Route 9 - Galar (Circhester Bay around iceberg between Trainer Tips signpost and Circhester Bay\u2026",
"order": 214, "order": 214,
"encounters": [ "encounters": [
{ {
@@ -12430,7 +12430,7 @@
"encounters": [], "encounters": [],
"children": [ "children": [
{ {
"name": "Axews Eye", "name": "Axew\u2019s Eye",
"order": 218, "order": 218,
"encounters": [ "encounters": [
{ {
@@ -12700,7 +12700,7 @@
] ]
}, },
{ {
"name": "Axews Eye (Southeast of the Big Tree)", "name": "Axew\u2019s Eye (Southeast of the Big Tree)",
"order": 219, "order": 219,
"encounters": [ "encounters": [
{ {
@@ -12730,7 +12730,7 @@
] ]
}, },
{ {
"name": "Axews Eye (Northeast of the Big Tree)", "name": "Axew\u2019s Eye (Northeast of the Big Tree)",
"order": 220, "order": 220,
"encounters": [ "encounters": [
{ {
@@ -13959,7 +13959,7 @@
] ]
}, },
{ {
"name": "Route 10 - Galar (East of Pokémon camp)", "name": "Route 10 - Galar (East of Pok\u00e9mon camp)",
"order": 235, "order": 235,
"encounters": [ "encounters": [
{ {
@@ -14966,7 +14966,7 @@
] ]
}, },
{ {
"name": "Soothing Wetlands (In Puddle Near Brawlers Cave Entrance)", "name": "Soothing Wetlands (In Puddle Near Brawler\u2019s Cave Entrance)",
"order": 254, "order": 254,
"encounters": [ "encounters": [
{ {
@@ -14996,7 +14996,7 @@
] ]
}, },
{ {
"name": "Soothing Wetlands (Southwest of Brawlers Cave Entrance in Open Area Near Den)", "name": "Soothing Wetlands (Southwest of Brawler\u2019s Cave Entrance in Open Area Near Den)",
"order": 255, "order": 255,
"encounters": [ "encounters": [
{ {

View File

@@ -7681,7 +7681,7 @@
] ]
}, },
{ {
"name": "Pokémon League (Sinnoh)", "name": "Pok\u00e9mon League (Sinnoh)",
"order": 117, "order": 117,
"encounters": [ "encounters": [
{ {

View File

@@ -51,7 +51,7 @@
"badge_image_url": null, "badge_image_url": null,
"level_cap": 24, "level_cap": 24,
"order": 4, "order": 4,
"after_route_name": "Route 5 (Alola) - Northern half", "after_route_name": "Route 5 (Alola)",
"location": "Brooklet Hill", "location": "Brooklet Hill",
"section": "Akala Island", "section": "Akala Island",
"sprite_url": "/sprites/746.webp", "sprite_url": "/sprites/746.webp",

View File

@@ -35,7 +35,7 @@
"encounters": [], "encounters": [],
"children": [ "children": [
{ {
"name": "Alola Route 1 (First two fields east of the players house)", "name": "Alola Route 1 (First two fields east of the player\u2019s house)",
"order": 3, "order": 3,
"encounters": [ "encounters": [
{ {
@@ -368,7 +368,7 @@
] ]
}, },
{ {
"name": "Trainers School (Alola)", "name": "Trainer\u2019s School (Alola)",
"order": 8, "order": 8,
"encounters": [ "encounters": [
{ {

View File

@@ -3883,7 +3883,7 @@
] ]
}, },
{ {
"name": "South Lake Miloch (Northwest of Bridge to Giants Seat)", "name": "South Lake Miloch (Northwest of Bridge to Giant\u2019s Seat)",
"order": 70, "order": 70,
"encounters": [ "encounters": [
{ {
@@ -3921,7 +3921,7 @@
] ]
}, },
{ {
"name": "South Lake Miloch (West of Bridge to Giants Seat)", "name": "South Lake Miloch (West of Bridge to Giant\u2019s Seat)",
"order": 71, "order": 71,
"encounters": [ "encounters": [
{ {
@@ -4141,7 +4141,7 @@
] ]
}, },
{ {
"name": "South Lake Miloch (By Giants Seat, Fishing Spot North of Bridge)", "name": "South Lake Miloch (By Giant\u2019s Seat, Fishing Spot North of Bridge)",
"order": 77, "order": 77,
"encounters": [ "encounters": [
{ {
@@ -12265,7 +12265,7 @@
] ]
}, },
{ {
"name": "Route 9 - Galar (Circhester Bay around icebergs northwest of Pokémon camp)", "name": "Route 9 - Galar (Circhester Bay around icebergs northwest of Pok\u00e9mon camp)",
"order": 212, "order": 212,
"encounters": [ "encounters": [
{ {
@@ -12279,7 +12279,7 @@
] ]
}, },
{ {
"name": "Route 9 - Galar (Circhester Bay in canal southwest of Pokémon camp)", "name": "Route 9 - Galar (Circhester Bay in canal southwest of Pok\u00e9mon camp)",
"order": 213, "order": 213,
"encounters": [ "encounters": [
{ {
@@ -12293,7 +12293,7 @@
] ]
}, },
{ {
"name": "Route 9 - Galar (Circhester Bay around iceberg between Trainer Tips signpost and Circhester Bay", "name": "Route 9 - Galar (Circhester Bay around iceberg between Trainer Tips signpost and Circhester Bay\u2026",
"order": 214, "order": 214,
"encounters": [ "encounters": [
{ {
@@ -12454,7 +12454,7 @@
"encounters": [], "encounters": [],
"children": [ "children": [
{ {
"name": "Axews Eye", "name": "Axew\u2019s Eye",
"order": 218, "order": 218,
"encounters": [ "encounters": [
{ {
@@ -12724,7 +12724,7 @@
] ]
}, },
{ {
"name": "Axews Eye (Southeast of the Big Tree)", "name": "Axew\u2019s Eye (Southeast of the Big Tree)",
"order": 219, "order": 219,
"encounters": [ "encounters": [
{ {
@@ -12754,7 +12754,7 @@
] ]
}, },
{ {
"name": "Axews Eye (Northeast of the Big Tree)", "name": "Axew\u2019s Eye (Northeast of the Big Tree)",
"order": 220, "order": 220,
"encounters": [ "encounters": [
{ {
@@ -14015,7 +14015,7 @@
] ]
}, },
{ {
"name": "Route 10 - Galar (East of Pokémon camp)", "name": "Route 10 - Galar (East of Pok\u00e9mon camp)",
"order": 235, "order": 235,
"encounters": [ "encounters": [
{ {
@@ -15006,7 +15006,7 @@
] ]
}, },
{ {
"name": "Soothing Wetlands (In Puddle Near Brawlers Cave Entrance)", "name": "Soothing Wetlands (In Puddle Near Brawler\u2019s Cave Entrance)",
"order": 254, "order": 254,
"encounters": [ "encounters": [
{ {
@@ -15036,7 +15036,7 @@
] ]
}, },
{ {
"name": "Soothing Wetlands (Southwest of Brawlers Cave Entrance in Open Area Near Den)", "name": "Soothing Wetlands (Southwest of Brawler\u2019s Cave Entrance in Open Area Near Den)",
"order": 255, "order": 255,
"encounters": [ "encounters": [
{ {

View File

@@ -60,7 +60,7 @@
], ],
"children": [ "children": [
{ {
"name": "Alola Route 1 (First two fields east of the players house)", "name": "Alola Route 1 (First two fields east of the player\u2019s house)",
"order": 3, "order": 3,
"encounters": [ "encounters": [
{ {
@@ -377,7 +377,7 @@
] ]
}, },
{ {
"name": "Trainers School (Alola)", "name": "Trainer\u2019s School (Alola)",
"order": 8, "order": 8,
"encounters": [ "encounters": [
{ {
@@ -773,7 +773,7 @@
] ]
}, },
{ {
"name": "Alola Route 2 (Two patches of grass southwest of the Pokémon Center)", "name": "Alola Route 2 (Two patches of grass southwest of the Pok\u00e9mon Center)",
"order": 15, "order": 15,
"encounters": [ "encounters": [
{ {
@@ -6696,7 +6696,7 @@
] ]
}, },
{ {
"name": "Team Rockets Castle", "name": "Team Rocket\u2019s Castle",
"order": 124, "order": 124,
"encounters": [ "encounters": [
{ {

View File

@@ -60,7 +60,7 @@
], ],
"children": [ "children": [
{ {
"name": "Alola Route 1 (First two fields east of the players house)", "name": "Alola Route 1 (First two fields east of the player\u2019s house)",
"order": 3, "order": 3,
"encounters": [ "encounters": [
{ {
@@ -377,7 +377,7 @@
] ]
}, },
{ {
"name": "Trainers School (Alola)", "name": "Trainer\u2019s School (Alola)",
"order": 8, "order": 8,
"encounters": [ "encounters": [
{ {
@@ -773,7 +773,7 @@
] ]
}, },
{ {
"name": "Alola Route 2 (Two patches of grass southwest of the Pokémon Center)", "name": "Alola Route 2 (Two patches of grass southwest of the Pok\u00e9mon Center)",
"order": 15, "order": 15,
"encounters": [ "encounters": [
{ {
@@ -6705,7 +6705,7 @@
] ]
}, },
{ {
"name": "Team Rockets Castle", "name": "Team Rocket\u2019s Castle",
"order": 124, "order": 124,
"encounters": [ "encounters": [
{ {

View File

@@ -1486,7 +1486,7 @@
] ]
}, },
{ {
"name": "Relic Castle (Volcaronas Room and Room Outside)", "name": "Relic Castle (Volcarona\u2019s Room and Room Outside)",
"order": 30, "order": 30,
"encounters": [ "encounters": [
{ {

View File

@@ -3804,7 +3804,7 @@
] ]
}, },
{ {
"name": "Pokémon Village", "name": "Pok\u00e9mon Village",
"order": 59, "order": 59,
"encounters": [ "encounters": [
{ {

View File

@@ -3796,7 +3796,7 @@
] ]
}, },
{ {
"name": "Pokémon Village", "name": "Pok\u00e9mon Village",
"order": 59, "order": 59,
"encounters": [ "encounters": [
{ {