Remove unused nuzlocke rules, reorganize into core and playstyle
All checks were successful
CI / backend-lint (push) Successful in 9s
CI / actions-lint (push) Successful in 15s
CI / frontend-lint (push) Successful in 21s

Remove firstEncounterOnly, permadeath, nicknameRequired, and
postGameCompletion from the rules system — they are either implicit
(it's a nuzlocke tracker) or not enforced. Move levelCaps to core
(it's displayed in the sticky bar). Create a new "playstyle" category
for hardcoreMode and setModeOnly — informational rules useful for
stats but not enforced by the tracker. Remove the completion category
entirely. Add sub-task beans for the rules overhaul epic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 21:20:23 +01:00
parent 4fbfcf9b29
commit e25d1cf24c
11 changed files with 204 additions and 124 deletions

View File

@@ -36,37 +36,18 @@ These are boolean flags with real tracker logic:
- `randomizer` — the run uses a randomized ROM. Same behavior: encounter Pokemon selection allows picking from ALL Pokemon since the dex is randomized. - `randomizer` — the run uses a randomized ROM. Same behavior: encounter Pokemon selection allows picking from ALL Pokemon since the dex is randomized.
- `giftClause` — in-game gift Pokemon are free and do not count against the area's encounter limit. When enabled, gift-origin encounters should bypass the route-lock check (similar to how shinyClause bypasses it for shinies). - `giftClause` — in-game gift Pokemon are free and do not count against the area's encounter limit. When enabled, gift-origin encounters should bypass the route-lock check (similar to how shinyClause bypasses it for shinies).
### Follow-up beans to CREATE ### Complex rules (need design work)
Rules that need more complex logic, tracked separately: These need more complex logic and are tracked as draft sub-tasks:
- Type Restrictions (Monolocke) — restrict team to specific types - Type Restrictions (Monolocke) — bs0y
- Team Size Limit — cap active party size with warnings - Team Size Limit — fv7w
- Static/Legendary Clause — whether static encounters count or are banned - Static/Legendary Clause — knnc
## Checklist ## Children
### Cleanup: remove unused rules Work is tracked in sub-tasks:
- [ ] Remove `firstEncounterOnly`, `permadeath`, `nicknameRequired`, `setModeOnly`, `postGameCompletion` from `NuzlockeRules` interface and `DEFAULT_RULES` - **o7r8** — Remove unused nuzlocke rules
- [ ] Remove their entries from `RULE_DEFINITIONS` - **fitk** — Add egglocke, wonderlocke, and randomizer rules
- **sij8** — Add gift clause rule
### Add new rules: frontend types - **bs0y** — Add type restriction rules (monolocke) *(draft)*
- [ ] Add `egglocke`, `wonderlocke`, `randomizer`, `giftClause` to `NuzlockeRules` interface and `DEFAULT_RULES` (default: false) - **fv7w** — Add team size limit rule *(draft)*
- [ ] Add `RuleDefinition` entries for the new rules with appropriate categories - **knnc** — Add static/legendary clause rule *(draft)*
### Add new rules: egglocke / wonderlocke / randomizer logic
- [ ] When any of `egglocke`, `wonderlocke`, or `randomizer` is enabled, the encounter Pokemon selector should allow picking from ALL Pokemon (not just the game's regional dex)
- [ ] Reuse the existing "all Pokemon" selector pattern used in admin panel encounter creation and boss team creation
### Add new rules: giftClause logic
- [ ] When `giftClause` is enabled, gift-origin encounters should bypass the route-lock check in the backend (similar to shinyClause bypass)
- [ ] Update the encounter creation endpoint to check for giftClause when origin is "gift"
### Update components and pages
- [ ] Update `RulesConfiguration`, `RuleToggle`, and `RuleBadges` components as needed
- [ ] Update `NewRun.tsx` and `NewGenlocke.tsx` if they reference removed rules
### Backend and data
- [ ] Verify backend encounter logic still works for removed rules (uses `.get()` with defaults)
- [ ] Update backend test seed data if it references removed rules
### Follow-ups
- [ ] Create follow-up beans for: Type Restrictions, Team Size Limit, Static/Legendary Clause

View File

@@ -0,0 +1,33 @@
---
# nuzlocke-tracker-bs0y
title: Add type restriction rules (monolocke)
status: todo
type: feature
priority: normal
created_at: 2026-02-20T19:56:16Z
updated_at: 2026-02-20T20:01:40Z
parent: nuzlocke-tracker-49xj
---
Restrict team composition to specific types (monolocke and similar variants).
## Design Decisions
**Type selection:** Multi-select from the 18 standard Pokemon types. A monolocke selects one type; multi-type variants (e.g., "fire and water only") select multiple.
**Dual-type matching:** A Pokemon qualifies if at least one of its types is in the allowed set. This matches the community standard for monolocke — e.g., in a Fire monolocke, Charizard (Fire/Flying) is allowed because it has Fire.
**Enforcement:** Soft enforcement via UI warnings, not hard blocks. The tracker warns when a caught Pokemon doesn't match the allowed types but doesn't prevent logging it. Reason: players sometimes need to use HM slaves or have edge cases the tracker shouldn't block.
**Data model:** Add `allowedTypes: string[]` to `NuzlockeRules`. Empty array means no restriction (disabled). This keeps it in the existing JSONB rules blob on the run.
**UI:** Add a "Type Restrictions" section to `RulesConfiguration` with a multi-select type picker (reuse the type badge styling from `TypeBadge`). Show a warning badge on encounters that don't match.
## Checklist
- [ ] Add `allowedTypes: string[]` to `NuzlockeRules` interface (default: `[]`)
- [ ] Add a new `'variant'` category to `RuleDefinition` for variant rules
- [ ] Add type multi-select UI to `RulesConfiguration` (shown when allowedTypes toggle is on)
- [ ] Show warning indicator on `PokemonCard` and encounter list for Pokemon that don't match allowed types
- [ ] Add `RuleBadge` display for active type restriction (e.g., "Monolocke: Fire")
- [ ] Update `RuleBadges` color mapping for the new `'variant'` category

View File

@@ -0,0 +1,22 @@
---
# nuzlocke-tracker-fitk
title: Add egglocke, wonderlocke, and randomizer rules
status: todo
type: feature
created_at: 2026-02-20T19:56:05Z
updated_at: 2026-02-20T19:56:05Z
parent: nuzlocke-tracker-49xj
---
Add three new boolean rules that all share the same tracker logic: when enabled, the encounter Pokemon selector allows picking from ALL Pokemon (not just the game's regional dex).
- `egglocke` — all caught Pokemon are replaced with traded eggs
- `wonderlocke` — all caught Pokemon are Wonder Traded away
- `randomizer` — the run uses a randomized ROM
## Checklist
- [ ] Add `egglocke`, `wonderlocke`, `randomizer` to `NuzlockeRules` interface and `DEFAULT_RULES` (default: false)
- [ ] Add `RuleDefinition` entries with appropriate categories
- [ ] When any of these is enabled, encounter Pokemon selector should allow picking from ALL Pokemon
- [ ] Reuse the existing "all Pokemon" selector pattern used in admin panel encounter creation and boss team creation

View File

@@ -0,0 +1,33 @@
---
# nuzlocke-tracker-fv7w
title: Add team size limit rule
status: todo
type: feature
priority: normal
created_at: 2026-02-20T19:56:22Z
updated_at: 2026-02-20T20:01:53Z
parent: nuzlocke-tracker-49xj
---
Cap the active party size with warnings when the limit is exceeded.
## Design Decisions
**Configurable limit:** Add `teamSizeLimit: number | null` to `NuzlockeRules`. `null` means no limit (disabled). Default Pokemon party size is 6, but variants like "trio-locke" use 3.
**What counts:** "Active team" = encounters with status `caught` and not fainted. The tracker already tracks this — alive Pokemon are shown in the team section on RunEncounters.
**No PC box tracking:** The tracker doesn't model a PC box. Excess catches beyond the team limit are still logged normally. The tracker just warns that the team is over capacity.
**Enforcement:** Soft enforcement. Show a warning banner on the encounters page when alive count exceeds the limit. Highlight the count in the team section header. Don't block new catches.
**UI:** Add a numeric input to `RulesConfiguration` (shown when team size toggle is on, min 1, max 6). Display the limit in the sticky bar alongside level caps if enabled.
## Checklist
- [ ] Add `teamSizeLimit: number | null` to `NuzlockeRules` interface (default: `null`)
- [ ] Add `RuleDefinition` entry under `'difficulty'` category
- [ ] Add numeric input to `RulesConfiguration` (shown when enabled, min 1, max 6)
- [ ] Show warning banner on RunEncounters when alive team count exceeds limit
- [ ] Display team size limit in sticky bar alongside level caps
- [ ] Show count in team section header (e.g., "Team (4/3)" in red when over)

View File

@@ -0,0 +1,32 @@
---
# nuzlocke-tracker-knnc
title: Add static/legendary clause rule
status: todo
type: feature
priority: normal
created_at: 2026-02-20T19:56:27Z
updated_at: 2026-02-20T20:02:07Z
parent: nuzlocke-tracker-49xj
---
Control whether static/legendary encounters count against the area's encounter limit.
## Design Decisions
**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).
**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.
**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.
**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.
**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.
## 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

View File

@@ -0,0 +1,26 @@
---
# nuzlocke-tracker-o7r8
title: Remove unused nuzlocke rules
status: completed
type: feature
priority: normal
created_at: 2026-02-20T19:55:59Z
updated_at: 2026-02-20T20:04:33Z
parent: nuzlocke-tracker-49xj
---
Remove 5 rules that either define what a nuzlocke is (always true) or don't affect tracker behavior:
- `firstEncounterOnly` — implicit; it's a nuzlocke tracker
- `permadeath` — implicit; it's a nuzlocke tracker
- `nicknameRequired` — not enforced or tracked
- `setModeOnly` — not enforced or tracked
- `postGameCompletion` — not enforced or tracked
## Checklist
- [x] Remove from `NuzlockeRules` interface and `DEFAULT_RULES`
- [x] Remove their entries from `RULE_DEFINITIONS`
- [x] Update `RulesConfiguration`, `RuleToggle`, and `RuleBadges` components as needed
- [x] Update `NewRun.tsx` and `NewGenlocke.tsx` if they reference removed rules
- [x] Verify backend encounter logic still works (uses `.get()` with defaults)
- [x] Update backend test seed data if it references removed rules

View File

@@ -0,0 +1,18 @@
---
# nuzlocke-tracker-sij8
title: Add gift clause rule
status: todo
type: feature
created_at: 2026-02-20T19:56:10Z
updated_at: 2026-02-20T19:56:10Z
parent: nuzlocke-tracker-49xj
---
Add a new `giftClause` boolean rule: in-game gift Pokemon are free and do not count against the area's encounter limit.
## Checklist
- [ ] Add `giftClause` to `NuzlockeRules` interface and `DEFAULT_RULES` (default: false)
- [ ] Add `RuleDefinition` entry with appropriate category
- [ ] When enabled, gift-origin encounters bypass the route-lock check in the backend (similar to shinyClause bypass)
- [ ] Update the encounter creation endpoint to check for giftClause when origin is "gift"

View File

@@ -142,14 +142,11 @@ RUN_DEFS = [
# Default rules (matches frontend DEFAULT_RULES) # Default rules (matches frontend DEFAULT_RULES)
DEFAULT_RULES = { DEFAULT_RULES = {
"firstEncounterOnly": True,
"permadeath": True,
"nicknameRequired": True,
"duplicatesClause": True, "duplicatesClause": True,
"shinyClause": True, "shinyClause": True,
"pinwheelClause": True, "pinwheelClause": True,
"hardcoreMode": False,
"levelCaps": False, "levelCaps": False,
"hardcoreMode": False,
"setModeOnly": False, "setModeOnly": False,
} }

View File

@@ -21,9 +21,7 @@ export function RuleBadges({ rules }: RuleBadgesProps) {
className={`px-2 py-0.5 rounded-full text-xs font-medium ${ className={`px-2 py-0.5 rounded-full text-xs font-medium ${
def.category === 'core' def.category === 'core'
? 'bg-blue-900/40 text-blue-300 light:bg-blue-100 light:text-blue-700' ? 'bg-blue-900/40 text-blue-300 light:bg-blue-100 light:text-blue-700'
: def.category === 'completion' : 'bg-purple-900/40 text-purple-300 light:bg-purple-100 light:text-purple-700'
? 'bg-green-900/40 text-green-300 light:bg-green-100 light:text-green-700'
: 'bg-amber-900/40 text-amber-300 light:bg-amber-100 light:text-amber-800'
}`} }`}
> >
{def.name} {def.name}

View File

@@ -19,8 +19,7 @@ export function RulesConfiguration({
? RULE_DEFINITIONS.filter((r) => !hiddenRules.has(r.key)) ? RULE_DEFINITIONS.filter((r) => !hiddenRules.has(r.key))
: RULE_DEFINITIONS : RULE_DEFINITIONS
const coreRules = visibleRules.filter((r) => r.category === 'core') const coreRules = visibleRules.filter((r) => r.category === 'core')
const difficultyRules = visibleRules.filter((r) => r.category === 'difficulty') const playstyleRules = visibleRules.filter((r) => r.category === 'playstyle')
const completionRules = visibleRules.filter((r) => r.category === 'completion')
const handleRuleChange = (key: keyof NuzlockeRules, value: boolean) => { const handleRuleChange = (key: keyof NuzlockeRules, value: boolean) => {
onChange({ ...rules, [key]: value }) onChange({ ...rules, [key]: value })
@@ -74,11 +73,13 @@ export function RulesConfiguration({
<div className="bg-surface-1 rounded-lg shadow"> <div className="bg-surface-1 rounded-lg shadow">
<div className="px-4 py-3 border-b border-border-default"> <div className="px-4 py-3 border-b border-border-default">
<h3 className="text-lg font-medium text-text-primary">Difficulty Modifiers</h3> <h3 className="text-lg font-medium text-text-primary">Playstyle</h3>
<p className="text-sm text-text-tertiary">Optional rules to increase the challenge</p> <p className="text-sm text-text-tertiary">
Describe how you're playing — doesn't affect tracker behavior
</p>
</div> </div>
<div className="px-4"> <div className="px-4">
{difficultyRules.map((rule) => ( {playstyleRules.map((rule) => (
<RuleToggle <RuleToggle
key={rule.key} key={rule.key}
name={rule.name} name={rule.name}
@@ -89,26 +90,6 @@ export function RulesConfiguration({
))} ))}
</div> </div>
</div> </div>
{completionRules.length > 0 && (
<div className="bg-surface-1 rounded-lg shadow">
<div className="px-4 py-3 border-b border-border-default">
<h3 className="text-lg font-medium text-text-primary">Completion</h3>
<p className="text-sm text-text-tertiary">When is the run considered complete</p>
</div>
<div className="px-4">
{completionRules.map((rule) => (
<RuleToggle
key={rule.key}
name={rule.name}
description={rule.description}
enabled={rules[rule.key]}
onChange={(value) => handleRuleChange(rule.key, value)}
/>
))}
</div>
</div>
)}
</div> </div>
) )
} }

View File

@@ -1,68 +1,36 @@
export interface NuzlockeRules { export interface NuzlockeRules {
// Core rules // Core rules (affect tracker behavior)
firstEncounterOnly: boolean
permadeath: boolean
nicknameRequired: boolean
duplicatesClause: boolean duplicatesClause: boolean
shinyClause: boolean shinyClause: boolean
pinwheelClause: boolean pinwheelClause: boolean
// Difficulty modifiers
hardcoreMode: boolean
levelCaps: boolean levelCaps: boolean
setModeOnly: boolean
// Completion // Playstyle (informational, for stats/categorization)
postGameCompletion: boolean hardcoreMode: boolean
setModeOnly: boolean
} }
export const DEFAULT_RULES: NuzlockeRules = { export const DEFAULT_RULES: NuzlockeRules = {
// Core rules - standard Nuzlocke // Core rules
firstEncounterOnly: true,
permadeath: true,
nicknameRequired: true,
duplicatesClause: true, duplicatesClause: true,
shinyClause: true, shinyClause: true,
pinwheelClause: true, pinwheelClause: true,
// Difficulty modifiers - off by default
hardcoreMode: false,
levelCaps: false, levelCaps: false,
setModeOnly: false,
// Completion // Playstyle - off by default
postGameCompletion: false, hardcoreMode: false,
setModeOnly: false,
} }
export interface RuleDefinition { export interface RuleDefinition {
key: keyof NuzlockeRules key: keyof NuzlockeRules
name: string name: string
description: string description: string
category: 'core' | 'difficulty' | 'completion' category: 'core' | 'playstyle'
} }
export const RULE_DEFINITIONS: RuleDefinition[] = [ export const RULE_DEFINITIONS: RuleDefinition[] = [
// Core rules // Core rules
{
key: 'firstEncounterOnly',
name: 'First Encounter Only',
description:
'You may only catch the first Pokémon encountered in each area. If you fail to catch it, you get nothing from that area.',
category: 'core',
},
{
key: 'permadeath',
name: 'Permadeath',
description:
'If a Pokémon faints, it is considered dead and must be released or permanently boxed.',
category: 'core',
},
{
key: 'nicknameRequired',
name: 'Nickname Required',
description: 'All caught Pokémon must be given a nickname to form a stronger bond.',
category: 'core',
},
{ {
key: 'duplicatesClause', key: 'duplicatesClause',
name: 'Duplicates Clause', name: 'Duplicates Clause',
@@ -84,35 +52,26 @@ export const RULE_DEFINITIONS: RuleDefinition[] = [
'Sub-zones within a location group each get their own encounter instead of sharing one.', 'Sub-zones within a location group each get their own encounter instead of sharing one.',
category: 'core', category: 'core',
}, },
// Difficulty modifiers
{
key: 'hardcoreMode',
name: 'Hardcore Mode',
description: 'No items may be used during battle. Held items are still allowed.',
category: 'difficulty',
},
{ {
key: 'levelCaps', key: 'levelCaps',
name: 'Level Caps', name: 'Level Caps',
description: description:
"Your Pokémon cannot exceed the level of the next Gym Leader's highest-level Pokémon before challenging them.", "Your Pokémon cannot exceed the level of the next Gym Leader's highest-level Pokémon before challenging them.",
category: 'difficulty', category: 'core',
},
// Playstyle
{
key: 'hardcoreMode',
name: 'Hardcore Mode',
description: 'No items may be used during battle. Held items are still allowed.',
category: 'playstyle',
}, },
{ {
key: 'setModeOnly', key: 'setModeOnly',
name: 'Set Mode Only', name: 'Set Mode Only',
description: description:
'The game must be played in "Set" battle style, meaning you cannot switch Pokémon after knocking out an opponent.', 'The game must be played in "Set" battle style, meaning you cannot switch Pokémon after knocking out an opponent.',
category: 'difficulty', category: 'playstyle',
},
// Completion
{
key: 'postGameCompletion',
name: 'Post-Game Completion',
description:
'The run continues into post-game content instead of ending after the Champion is defeated.',
category: 'completion',
}, },
] ]