Replace playstyle rules with free-text custom rules markdown field
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>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { type FormEvent, useState, useMemo } from 'react'
|
||||
import { type FormEvent, useMemo, useState } from 'react'
|
||||
import type { BossBattle, CreateBossResultInput } from '../types/game'
|
||||
import { ConditionBadge } from './ConditionBadge'
|
||||
|
||||
@@ -7,7 +7,6 @@ interface BossDefeatModalProps {
|
||||
onSubmit: (data: CreateBossResultInput) => void
|
||||
onClose: () => void
|
||||
isPending?: boolean
|
||||
hardcoreMode?: boolean
|
||||
starterName?: string | null
|
||||
}
|
||||
|
||||
@@ -23,11 +22,8 @@ export function BossDefeatModal({
|
||||
onSubmit,
|
||||
onClose,
|
||||
isPending,
|
||||
hardcoreMode,
|
||||
starterName,
|
||||
}: BossDefeatModalProps) {
|
||||
const [result, setResult] = useState<'won' | 'lost'>('won')
|
||||
const [attempts, setAttempts] = useState('1')
|
||||
|
||||
const variantLabels = useMemo(() => {
|
||||
const labels = new Set<string>()
|
||||
@@ -58,8 +54,8 @@ export function BossDefeatModal({
|
||||
e.preventDefault()
|
||||
onSubmit({
|
||||
bossBattleId: boss.id,
|
||||
result: hardcoreMode ? 'won' : result,
|
||||
attempts: hardcoreMode ? 1 : Number(attempts) || 1,
|
||||
result: 'won',
|
||||
attempts: 1,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,51 +109,6 @@ export function BossDefeatModal({
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Result</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setResult('won')}
|
||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md border transition-colors ${
|
||||
result === 'won'
|
||||
? 'bg-green-600 text-white border-green-600'
|
||||
: 'border-border-default hover:bg-surface-2'
|
||||
}`}
|
||||
>
|
||||
Won
|
||||
</button>
|
||||
{!hardcoreMode && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setResult('lost')}
|
||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md border transition-colors ${
|
||||
result === 'lost'
|
||||
? 'bg-red-600 text-white border-red-600'
|
||||
: 'border-border-default hover:bg-surface-2'
|
||||
}`}
|
||||
>
|
||||
Lost
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!hardcoreMode && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Attempts</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={attempts}
|
||||
onChange={(e) => setAttempts(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-t border-border-default flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
40
frontend/src/components/CustomRulesDisplay.tsx
Normal file
40
frontend/src/components/CustomRulesDisplay.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useState } from 'react'
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
interface CustomRulesDisplayProps {
|
||||
customRules: string
|
||||
}
|
||||
|
||||
export function CustomRulesDisplay({ customRules }: CustomRulesDisplayProps) {
|
||||
const trimmed = customRules.trim()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
if (!trimmed) return null
|
||||
|
||||
return (
|
||||
<div className="mt-3 rounded-lg border border-border-default bg-surface-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="w-full flex items-center justify-between px-4 py-2.5 text-xs font-medium text-text-tertiary hover:text-text-secondary transition-colors"
|
||||
>
|
||||
<span>Custom Rules</span>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform ${open ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="px-4 pb-3 prose prose-sm prose-invert light:prose text-text-secondary max-w-none">
|
||||
<Markdown remarkPlugins={[remarkGfm]}>{trimmed}</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -9,8 +9,9 @@ interface RuleBadgesProps {
|
||||
export function RuleBadges({ rules }: RuleBadgesProps) {
|
||||
const enabledRules = RULE_DEFINITIONS.filter((def) => rules[def.key])
|
||||
const allowedTypes = rules.allowedTypes ?? []
|
||||
const hasCustomRules = (rules.customRules ?? '').trim().length > 0
|
||||
|
||||
if (enabledRules.length === 0 && allowedTypes.length === 0) {
|
||||
if (enabledRules.length === 0 && allowedTypes.length === 0 && !hasCustomRules) {
|
||||
return <span className="text-sm text-text-tertiary">No rules enabled</span>
|
||||
}
|
||||
|
||||
@@ -23,9 +24,7 @@ export function RuleBadges({ rules }: RuleBadgesProps) {
|
||||
className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
def.category === 'core'
|
||||
? 'bg-blue-900/40 text-blue-300 light:bg-blue-100 light:text-blue-700'
|
||||
: def.category === 'variant'
|
||||
? 'bg-amber-900/40 text-amber-300 light:bg-amber-100 light:text-amber-700'
|
||||
: 'bg-purple-900/40 text-purple-300 light:bg-purple-100 light:text-purple-700'
|
||||
: 'bg-amber-900/40 text-amber-300 light:bg-amber-100 light:text-amber-700'
|
||||
}`}
|
||||
>
|
||||
{def.name}
|
||||
@@ -42,6 +41,14 @@ export function RuleBadges({ rules }: RuleBadgesProps) {
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
{hasCustomRules && (
|
||||
<span
|
||||
title={rules.customRules}
|
||||
className="px-2 py-0.5 rounded-full text-xs font-medium bg-purple-900/40 text-purple-300 light:bg-purple-100 light:text-purple-700"
|
||||
>
|
||||
Custom Rules
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ export function RulesConfiguration({
|
||||
? RULE_DEFINITIONS.filter((r) => !hiddenRules.has(r.key))
|
||||
: RULE_DEFINITIONS
|
||||
const coreRules = visibleRules.filter((r) => r.category === 'core')
|
||||
const playstyleRules = visibleRules.filter((r) => r.category === 'playstyle')
|
||||
const variantRules = visibleRules.filter((r) => r.category === 'variant')
|
||||
|
||||
const handleRuleChange = (key: keyof NuzlockeRules, value: boolean) => {
|
||||
@@ -62,9 +61,13 @@ export function RulesConfiguration({
|
||||
onChange({ ...rules, allowedTypes: next })
|
||||
}
|
||||
|
||||
const customRules = rules.customRules ?? ''
|
||||
|
||||
const enabledCount =
|
||||
visibleRules.filter((r) => rules[r.key]).length + (allowedTypes.length > 0 ? 1 : 0)
|
||||
const totalCount = visibleRules.length + 1 // +1 for type restriction
|
||||
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">
|
||||
@@ -106,21 +109,19 @@ export function RulesConfiguration({
|
||||
|
||||
<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">Playstyle</h3>
|
||||
<h3 className="text-lg font-medium text-text-primary">Custom Rules</h3>
|
||||
<p className="text-sm text-text-tertiary">
|
||||
Describe how you're playing — doesn't affect tracker behavior
|
||||
Track your own rules — supports markdown. Doesn't affect tracker behavior.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
{playstyleRules.map((rule) => (
|
||||
<RuleToggle
|
||||
key={rule.key}
|
||||
name={rule.name}
|
||||
description={rule.description}
|
||||
enabled={rules[rule.key]}
|
||||
onChange={(value) => handleRuleChange(rule.key, value)}
|
||||
/>
|
||||
))}
|
||||
<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>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { CustomRulesDisplay } from './CustomRulesDisplay'
|
||||
export { EggEncounterModal } from './EggEncounterModal'
|
||||
export { EncounterMethodBadge } from './EncounterMethodBadge'
|
||||
export { EncounterModal } from './EncounterModal'
|
||||
|
||||
Reference in New Issue
Block a user