Remove hardcoreMode, setModeOnly, and bossTeamMatch toggles which had no mechanical impact on the tracker. Replace them with a customRules markdown field so users can document their own rules (especially useful for genlockes). Add react-markdown + remark-gfm for rendering and @tailwindcss/typography for prose styling. The custom rules display is collapsible and hidden by default. Also simplifies the BossDefeatModal by removing the Lost result and attempts counter, and always shows boss team size in the level cap bar. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
188 lines
6.1 KiB
TypeScript
188 lines
6.1 KiB
TypeScript
import type { NuzlockeRules } from '../types/rules'
|
|
import { RULE_DEFINITIONS, DEFAULT_RULES } from '../types/rules'
|
|
import { RuleToggle } from './RuleToggle'
|
|
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
|
|
|
|
interface RulesConfigurationProps {
|
|
rules: NuzlockeRules
|
|
onChange: (rules: NuzlockeRules) => void
|
|
onReset?: (() => void) | undefined
|
|
hiddenRules?: Set<keyof NuzlockeRules> | undefined
|
|
}
|
|
|
|
export function RulesConfiguration({
|
|
rules,
|
|
onChange,
|
|
onReset,
|
|
hiddenRules,
|
|
}: RulesConfigurationProps) {
|
|
const visibleRules = hiddenRules
|
|
? RULE_DEFINITIONS.filter((r) => !hiddenRules.has(r.key))
|
|
: RULE_DEFINITIONS
|
|
const coreRules = visibleRules.filter((r) => r.category === 'core')
|
|
const variantRules = visibleRules.filter((r) => r.category === 'variant')
|
|
|
|
const handleRuleChange = (key: keyof NuzlockeRules, value: boolean) => {
|
|
onChange({ ...rules, [key]: value })
|
|
}
|
|
|
|
const handleResetToDefault = () => {
|
|
onChange(DEFAULT_RULES)
|
|
onReset?.()
|
|
}
|
|
|
|
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 customRules = rules.customRules ?? ''
|
|
|
|
const enabledCount =
|
|
visibleRules.filter((r) => rules[r.key]).length +
|
|
(allowedTypes.length > 0 ? 1 : 0) +
|
|
(customRules.trim().length > 0 ? 1 : 0)
|
|
const totalCount = visibleRules.length + 2 // +1 for type restriction, +1 for custom rules
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xl font-semibold text-text-primary">Rules Configuration</h2>
|
|
<p className="text-sm text-text-tertiary">
|
|
{enabledCount} of {totalCount} rules enabled
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={handleResetToDefault}
|
|
className="text-sm text-text-link hover:text-accent-300"
|
|
>
|
|
Reset to Default
|
|
</button>
|
|
</div>
|
|
|
|
<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">
|
|
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>
|
|
|
|
<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">Custom Rules</h3>
|
|
<p className="text-sm text-text-tertiary">
|
|
Track your own rules — supports markdown. Doesn't affect tracker behavior.
|
|
</p>
|
|
</div>
|
|
<div className="px-4 py-4">
|
|
<textarea
|
|
value={customRules}
|
|
onChange={(e) => onChange({ ...rules, customRules: e.target.value })}
|
|
placeholder="e.g. No items in battle, Set mode only, must match boss team size..."
|
|
rows={4}
|
|
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default text-text-primary placeholder:text-text-muted text-sm resize-y"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
</div>
|
|
)
|
|
}
|