diff --git a/.beans/nuzlocke-tracker-knnc--add-staticlegendary-clause-rule.md b/.beans/nuzlocke-tracker-knnc--add-staticlegendary-clause-rule.md index 6bfc401..d80c090 100644 --- a/.beans/nuzlocke-tracker-knnc--add-staticlegendary-clause-rule.md +++ b/.beans/nuzlocke-tracker-knnc--add-staticlegendary-clause-rule.md @@ -1,32 +1,38 @@ --- # nuzlocke-tracker-knnc -title: Add static/legendary clause rule -status: todo +title: Add static encounter filter rule +status: in-progress type: feature priority: normal created_at: 2026-02-20T19:56:27Z -updated_at: 2026-02-20T20:02:07Z +updated_at: 2026-02-21T11:03:12Z parent: nuzlocke-tracker-49xj --- -Control whether static/legendary encounters count against the area's encounter limit. +Control whether static encounters are available in the encounter selector. Static encounters already exist in the route encounter tables (e.g., Zapdos in Power Plant, Snorlax on Route 7 in X/Y). This rule acts as a display filter, not a route-lock bypass like gift clause. -## Design Decisions +## Motivation -**Scope:** This rule covers overworld Pokemon that are always available (legendaries, Snorlax blocking the road, Sudowoodo, Voltorb in the power plant, etc.). These are distinct from gifts (given by NPCs) which are covered by giftClause (sij8). +Static encounters can feel unfair in nuzlockes because they are deterministic — the player is forced to pick a specific Pokemon rather than getting the randomness that makes nuzlockes fun. Example: Snorlax blocks Route 7 in X/Y. By definition it is the first encounter, but being forced to take it reduces variety. -**Encounter method:** The existing encounter method list (walk, surf, gift, fossil, etc.) doesn't have a "static" method. Add `static` as a new encounter method in the seed data and `METHOD_CONFIG`. Static encounters are one-time overworld Pokemon the player walks up to and battles. +Some static encounters are also overpowered (legendaries), which some players want to avoid. -**Rule behavior:** `staticClause: boolean` (default: false). When enabled, encounters with method `static` bypass the route-lock check (same pattern as shinyClause and giftClause). This means static Pokemon are "free" and don't consume the area's encounter. +## Design -**No legendary ban:** Rather than banning legendaries outright, the community standard is to let the player choose. The tracker just needs to support logging static encounters correctly. Players who want to ban legendaries simply don't catch them. +**Rule:** `staticClause: boolean` (default: true — static encounters enabled by default). When disabled, encounters with a `static` encounter method are hidden or grayed out in the encounter selector, so the player skips them and gets a different first encounter. -**Interaction with giftClause:** These are separate rules. `giftClause` covers NPC gifts (method: `gift`). `staticClause` covers overworld statics (method: `static`). A player can enable both, one, or neither. +**This is NOT like gift clause.** There is no dual-encounter per route. Disabling static encounters simply filters them out of the available encounter pool for a location. The player still gets one encounter per area — just not the static one. + +**Encounter method:** The existing encounter tables already include static encounters (e.g., Zapdos in Power Plant). The `static` encounter method may already exist in seed data — verify before adding. If not present, add it to seed data and `METHOD_CONFIG` / `METHOD_ORDER`. + +**Frontend behavior:** +- When `staticClause` is **enabled** (default): static encounters appear normally in the encounter selector +- When `staticClause` is **disabled**: static encounters are hidden or visually grayed out in the encounter selector, preventing the player from selecting them ## Checklist -- [ ] Add `static` encounter method to seed data and `METHOD_CONFIG` / `METHOD_ORDER` -- [ ] Add `staticClause` to `NuzlockeRules` interface and `DEFAULT_RULES` (default: false) -- [ ] Add `RuleDefinition` entry under `'core'` category -- [ ] When enabled, encounters with method `static` bypass route-lock check in backend (add to `skip_route_lock` condition alongside shiny/egg/shed/transfer) -- [ ] Update encounter creation frontend to show `static` as a selectable method where appropriate \ No newline at end of file +- [x] Verify `static` encounter method exists in seed data; add to `METHOD_CONFIG` / `METHOD_ORDER` if missing +- [x] Add `staticClause` to `NuzlockeRules` interface and `DEFAULT_RULES` (default: true) +- [x] Add `RuleDefinition` entry under `core` category +- [x] Frontend: filter or gray out static encounters in encounter selector when `staticClause` is disabled +- [x] Backend seed data: add `staticClause` to `DEFAULT_RULES` in `inject_test_data.py` diff --git a/backend/src/app/seeds/inject_test_data.py b/backend/src/app/seeds/inject_test_data.py index 25c1693..833cc06 100644 --- a/backend/src/app/seeds/inject_test_data.py +++ b/backend/src/app/seeds/inject_test_data.py @@ -145,6 +145,7 @@ DEFAULT_RULES = { "duplicatesClause": True, "shinyClause": True, "giftClause": False, + "staticClause": True, "pinwheelClause": True, "levelCaps": False, "hardcoreMode": False, diff --git a/frontend/src/components/EncounterMethodBadge.tsx b/frontend/src/components/EncounterMethodBadge.tsx index 7660796..35b1929 100644 --- a/frontend/src/components/EncounterMethodBadge.tsx +++ b/frontend/src/components/EncounterMethodBadge.tsx @@ -15,6 +15,10 @@ export const METHOD_CONFIG: Record = { label: 'Trade', color: 'bg-emerald-900/40 text-emerald-300 light:bg-emerald-100 light:text-emerald-700', }, + static: { + label: 'Static', + color: 'bg-teal-900/40 text-teal-300 light:bg-teal-100 light:text-teal-700', + }, walk: { label: 'Grass', color: 'bg-green-900/40 text-green-300 light:bg-green-100 light:text-green-700', @@ -59,6 +63,7 @@ export const METHOD_ORDER = [ 'gift', 'fossil', 'trade', + 'static', 'walk', 'headbutt', 'surf', diff --git a/frontend/src/components/EncounterModal.tsx b/frontend/src/components/EncounterModal.tsx index 88410bf..2c64f75 100644 --- a/frontend/src/components/EncounterModal.tsx +++ b/frontend/src/components/EncounterModal.tsx @@ -42,6 +42,7 @@ interface EncounterModalProps { onClose: () => void isPending: boolean useAllPokemon?: boolean | undefined + staticClause?: boolean | undefined } const statusOptions: { @@ -132,7 +133,8 @@ function groupByMethod( } else { // Determine the display rate let displayRate: number | null = null - const isSpecial = SPECIAL_METHODS.includes(rp.encounterMethod) + const isSpecial = + SPECIAL_METHODS.includes(rp.encounterMethod) || rp.encounterMethod === 'static' if (!isSpecial) { if (selectedCondition) { const key = `${rp.pokemonId}:${rp.encounterMethod}` @@ -198,6 +200,7 @@ export function EncounterModal({ onClose, isPending, useAllPokemon, + staticClause = true, }: EncounterModalProps) { const { data: routePokemon, isLoading: loadingPokemon } = useRoutePokemon( useAllPokemon ? null : route.id, @@ -443,7 +446,10 @@ export function EncounterModal({ } onClick={() => { if (routePokemon) { - setSelectedPokemon(pickRandomPokemon(routePokemon, dupedPokemonIds)) + const eligible = staticClause + ? routePokemon + : routePokemon.filter((rp) => rp.encounterMethod !== 'static') + setSelectedPokemon(pickRandomPokemon(eligible, dupedPokemonIds)) } }} className="px-2.5 py-1 text-xs font-medium rounded-lg border border-purple-600 text-purple-400 light:text-purple-700 light:border-purple-500 hover:bg-purple-900/20 light:hover:bg-purple-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors" @@ -508,6 +514,9 @@ export function EncounterModal({
{pokemon.map(({ encounter: rp, conditions, displayRate }) => { const isDuped = dupedPokemonIds?.has(rp.pokemonId) ?? false + const isStaticDisabled = + !staticClause && rp.encounterMethod === 'static' + const isDisabled = isDuped || isStaticDisabled const isSelected = selectedPokemon?.pokemonId === rp.pokemonId && selectedPokemon?.encounterMethod === rp.encounterMethod @@ -515,10 +524,10 @@ export function EncounterModal({