2026-02-04 17:13:58 +01:00
|
|
|
import type { NuzlockeRules } from '../types/rules'
|
|
|
|
|
import { RULE_DEFINITIONS, DEFAULT_RULES } from '../types/rules'
|
|
|
|
|
import { RuleToggle } from './RuleToggle'
|
2026-02-21 12:22:05 +01:00
|
|
|
import { TypeBadge } from './TypeBadge'
|
|
|
|
|
|
|
|
|
|
const POKEMON_TYPES = [
|
|
|
|
|
'bug',
|
|
|
|
|
'dark',
|
|
|
|
|
'dragon',
|
|
|
|
|
'electric',
|
|
|
|
|
'fairy',
|
|
|
|
|
'fighting',
|
|
|
|
|
'fire',
|
|
|
|
|
'flying',
|
|
|
|
|
'ghost',
|
|
|
|
|
'grass',
|
|
|
|
|
'ground',
|
|
|
|
|
'ice',
|
|
|
|
|
'normal',
|
|
|
|
|
'poison',
|
|
|
|
|
'psychic',
|
|
|
|
|
'rock',
|
|
|
|
|
'steel',
|
|
|
|
|
'water',
|
|
|
|
|
] as const
|
2026-02-04 17:13:58 +01:00
|
|
|
|
|
|
|
|
interface RulesConfigurationProps {
|
|
|
|
|
rules: NuzlockeRules
|
|
|
|
|
onChange: (rules: NuzlockeRules) => void
|
2026-02-16 20:39:41 +01:00
|
|
|
onReset?: (() => void) | undefined
|
|
|
|
|
hiddenRules?: Set<keyof NuzlockeRules> | undefined
|
2026-02-04 17:13:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function RulesConfiguration({
|
|
|
|
|
rules,
|
|
|
|
|
onChange,
|
|
|
|
|
onReset,
|
2026-02-08 10:40:18 +01:00
|
|
|
hiddenRules,
|
2026-02-04 17:13:58 +01:00
|
|
|
}: RulesConfigurationProps) {
|
2026-02-08 10:40:18 +01:00
|
|
|
const visibleRules = hiddenRules
|
|
|
|
|
? RULE_DEFINITIONS.filter((r) => !hiddenRules.has(r.key))
|
|
|
|
|
: RULE_DEFINITIONS
|
|
|
|
|
const coreRules = visibleRules.filter((r) => r.category === 'core')
|
2026-02-20 21:20:23 +01:00
|
|
|
const playstyleRules = visibleRules.filter((r) => r.category === 'playstyle')
|
2026-02-20 21:33:01 +01:00
|
|
|
const variantRules = visibleRules.filter((r) => r.category === 'variant')
|
2026-02-04 17:13:58 +01:00
|
|
|
|
|
|
|
|
const handleRuleChange = (key: keyof NuzlockeRules, value: boolean) => {
|
|
|
|
|
onChange({ ...rules, [key]: value })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleResetToDefault = () => {
|
|
|
|
|
onChange(DEFAULT_RULES)
|
|
|
|
|
onReset?.()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 12:22:05 +01:00
|
|
|
const allowedTypes = rules.allowedTypes ?? []
|
|
|
|
|
|
|
|
|
|
const toggleType = (type: string) => {
|
|
|
|
|
const next = allowedTypes.includes(type)
|
|
|
|
|
? allowedTypes.filter((t) => t !== type)
|
|
|
|
|
: [...allowedTypes, type]
|
|
|
|
|
onChange({ ...rules, allowedTypes: next })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const enabledCount =
|
|
|
|
|
visibleRules.filter((r) => rules[r.key]).length + (allowedTypes.length > 0 ? 1 : 0)
|
|
|
|
|
const totalCount = visibleRules.length + 1 // +1 for type restriction
|
2026-02-04 17:13:58 +01:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
2026-02-17 20:48:42 +01:00
|
|
|
<h2 className="text-xl font-semibold text-text-primary">Rules Configuration</h2>
|
|
|
|
|
<p className="text-sm text-text-tertiary">
|
2026-02-04 17:13:58 +01:00
|
|
|
{enabledCount} of {totalCount} rules enabled
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleResetToDefault}
|
2026-02-17 21:08:53 +01:00
|
|
|
className="text-sm text-text-link hover:text-accent-300"
|
2026-02-04 17:13:58 +01:00
|
|
|
>
|
|
|
|
|
Reset to Default
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-17 20:48:42 +01:00
|
|
|
<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">Core Rules</h3>
|
|
|
|
|
<p className="text-sm text-text-tertiary">
|
2026-02-04 17:13:58 +01:00
|
|
|
The fundamental rules of a Nuzlocke challenge
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="px-4">
|
|
|
|
|
{coreRules.map((rule) => (
|
|
|
|
|
<RuleToggle
|
|
|
|
|
key={rule.key}
|
|
|
|
|
name={rule.name}
|
|
|
|
|
description={rule.description}
|
|
|
|
|
enabled={rules[rule.key]}
|
|
|
|
|
onChange={(value) => handleRuleChange(rule.key, value)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-17 20:48:42 +01:00
|
|
|
<div className="bg-surface-1 rounded-lg shadow">
|
|
|
|
|
<div className="px-4 py-3 border-b border-border-default">
|
2026-02-20 21:20:23 +01:00
|
|
|
<h3 className="text-lg font-medium text-text-primary">Playstyle</h3>
|
|
|
|
|
<p className="text-sm text-text-tertiary">
|
|
|
|
|
Describe how you're playing — doesn't affect tracker behavior
|
|
|
|
|
</p>
|
2026-02-04 17:13:58 +01:00
|
|
|
</div>
|
|
|
|
|
<div className="px-4">
|
2026-02-20 21:20:23 +01:00
|
|
|
{playstyleRules.map((rule) => (
|
2026-02-04 17:13:58 +01:00
|
|
|
<RuleToggle
|
|
|
|
|
key={rule.key}
|
|
|
|
|
name={rule.name}
|
|
|
|
|
description={rule.description}
|
|
|
|
|
enabled={rules[rule.key]}
|
|
|
|
|
onChange={(value) => handleRuleChange(rule.key, value)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-20 21:33:01 +01:00
|
|
|
|
|
|
|
|
<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">Run Variant</h3>
|
|
|
|
|
<p className="text-sm text-text-tertiary">
|
|
|
|
|
Changes which Pokémon can appear — affects the encounter selector
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="px-4">
|
|
|
|
|
{variantRules.map((rule) => (
|
|
|
|
|
<RuleToggle
|
|
|
|
|
key={rule.key}
|
|
|
|
|
name={rule.name}
|
|
|
|
|
description={rule.description}
|
|
|
|
|
enabled={rules[rule.key]}
|
|
|
|
|
onChange={(value) => handleRuleChange(rule.key, value)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-21 12:22:05 +01:00
|
|
|
|
|
|
|
|
<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">Type Restriction</h3>
|
|
|
|
|
<p className="text-sm text-text-tertiary">
|
|
|
|
|
Monolocke and variants. Select allowed types — a Pokémon qualifies if it shares at least
|
|
|
|
|
one type. Leave all deselected to disable.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="px-4 py-4">
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
{POKEMON_TYPES.map((type) => (
|
|
|
|
|
<button
|
|
|
|
|
key={type}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => toggleType(type)}
|
|
|
|
|
title={type.charAt(0).toUpperCase() + type.slice(1)}
|
|
|
|
|
className={`p-1.5 rounded-lg border-2 transition-colors ${
|
|
|
|
|
allowedTypes.includes(type)
|
|
|
|
|
? 'border-accent-400 bg-accent-900/20'
|
|
|
|
|
: 'border-transparent opacity-40 hover:opacity-70'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<TypeBadge type={type} size="md" />
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
{allowedTypes.length > 0 && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onChange({ ...rules, allowedTypes: [] })}
|
|
|
|
|
className="mt-3 text-xs text-text-tertiary hover:text-text-secondary"
|
|
|
|
|
>
|
|
|
|
|
Clear selection
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-04 17:13:58 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|