develop #45
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
# nuzlocke-tracker-wb85
|
||||||
|
title: Replace playstyle rules with custom rules markdown field
|
||||||
|
status: completed
|
||||||
|
type: feature
|
||||||
|
priority: normal
|
||||||
|
created_at: 2026-03-20T13:48:50Z
|
||||||
|
updated_at: 2026-03-20T13:53:08Z
|
||||||
|
---
|
||||||
|
|
||||||
|
Remove hardcoreMode, setModeOnly, bossTeamMatch playstyle rules. Add a free-text markdown customRules field so users can track their own rules (especially useful for genlockes). Also: remove 'Lost' result and attempts from BossDefeatModal, always show boss team size.
|
||||||
@@ -87,7 +87,7 @@ RUN_DEFS = [
|
|||||||
"name": "Kanto Heartbreak",
|
"name": "Kanto Heartbreak",
|
||||||
"status": "failed",
|
"status": "failed",
|
||||||
"progress": 0.45,
|
"progress": 0.45,
|
||||||
"rules": {"hardcoreMode": True, "setModeOnly": True},
|
"rules": {"customRules": "- Hardcore mode: no items in battle\n- Set mode only"},
|
||||||
"started_days_ago": 30,
|
"started_days_ago": 30,
|
||||||
"ended_days_ago": 20,
|
"ended_days_ago": 20,
|
||||||
},
|
},
|
||||||
@@ -134,7 +134,7 @@ RUN_DEFS = [
|
|||||||
"name": "Crystal Nuzlocke",
|
"name": "Crystal Nuzlocke",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"progress": 0.20,
|
"progress": 0.20,
|
||||||
"rules": {"hardcoreMode": True, "levelCaps": True, "setModeOnly": True},
|
"rules": {"levelCaps": True, "customRules": "- Hardcore mode\n- Set mode only"},
|
||||||
"started_days_ago": 2,
|
"started_days_ago": 2,
|
||||||
"ended_days_ago": None,
|
"ended_days_ago": None,
|
||||||
},
|
},
|
||||||
@@ -148,13 +148,11 @@ DEFAULT_RULES = {
|
|||||||
"staticClause": True,
|
"staticClause": True,
|
||||||
"pinwheelClause": True,
|
"pinwheelClause": True,
|
||||||
"levelCaps": False,
|
"levelCaps": False,
|
||||||
"hardcoreMode": False,
|
|
||||||
"setModeOnly": False,
|
|
||||||
"bossTeamMatch": False,
|
|
||||||
"egglocke": False,
|
"egglocke": False,
|
||||||
"wonderlocke": False,
|
"wonderlocke": False,
|
||||||
"randomizer": False,
|
"randomizer": False,
|
||||||
"allowedTypes": [],
|
"allowedTypes": [],
|
||||||
|
"customRules": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1514
frontend/package-lock.json
generated
1514
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,10 +19,13 @@
|
|||||||
"@dnd-kit/core": "6.3.1",
|
"@dnd-kit/core": "6.3.1",
|
||||||
"@dnd-kit/sortable": "10.0.0",
|
"@dnd-kit/sortable": "10.0.0",
|
||||||
"@dnd-kit/utilities": "3.2.2",
|
"@dnd-kit/utilities": "3.2.2",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/react-query": "5.90.20",
|
"@tanstack/react-query": "5.90.20",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "7.13.0",
|
"react-router-dom": "7.13.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "2.0.7"
|
"sonner": "2.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -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 type { BossBattle, CreateBossResultInput } from '../types/game'
|
||||||
import { ConditionBadge } from './ConditionBadge'
|
import { ConditionBadge } from './ConditionBadge'
|
||||||
|
|
||||||
@@ -7,7 +7,6 @@ interface BossDefeatModalProps {
|
|||||||
onSubmit: (data: CreateBossResultInput) => void
|
onSubmit: (data: CreateBossResultInput) => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
isPending?: boolean
|
isPending?: boolean
|
||||||
hardcoreMode?: boolean
|
|
||||||
starterName?: string | null
|
starterName?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,11 +22,8 @@ export function BossDefeatModal({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onClose,
|
onClose,
|
||||||
isPending,
|
isPending,
|
||||||
hardcoreMode,
|
|
||||||
starterName,
|
starterName,
|
||||||
}: BossDefeatModalProps) {
|
}: BossDefeatModalProps) {
|
||||||
const [result, setResult] = useState<'won' | 'lost'>('won')
|
|
||||||
const [attempts, setAttempts] = useState('1')
|
|
||||||
|
|
||||||
const variantLabels = useMemo(() => {
|
const variantLabels = useMemo(() => {
|
||||||
const labels = new Set<string>()
|
const labels = new Set<string>()
|
||||||
@@ -58,8 +54,8 @@ export function BossDefeatModal({
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onSubmit({
|
onSubmit({
|
||||||
bossBattleId: boss.id,
|
bossBattleId: boss.id,
|
||||||
result: hardcoreMode ? 'won' : result,
|
result: 'won',
|
||||||
attempts: hardcoreMode ? 1 : Number(attempts) || 1,
|
attempts: 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,51 +109,6 @@ export function BossDefeatModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<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">
|
<div className="px-6 py-4 border-t border-border-default flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
type="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) {
|
export function RuleBadges({ rules }: RuleBadgesProps) {
|
||||||
const enabledRules = RULE_DEFINITIONS.filter((def) => rules[def.key])
|
const enabledRules = RULE_DEFINITIONS.filter((def) => rules[def.key])
|
||||||
const allowedTypes = rules.allowedTypes ?? []
|
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>
|
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 ${
|
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 === 'variant'
|
: 'bg-amber-900/40 text-amber-300 light:bg-amber-100 light:text-amber-700'
|
||||||
? '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'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{def.name}
|
{def.name}
|
||||||
@@ -42,6 +41,14 @@ export function RuleBadges({ rules }: RuleBadgesProps) {
|
|||||||
))}
|
))}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ describe('RulesConfiguration', () => {
|
|||||||
it('renders all rule section headings', () => {
|
it('renders all rule section headings', () => {
|
||||||
setup()
|
setup()
|
||||||
expect(screen.getByText('Core Rules')).toBeInTheDocument()
|
expect(screen.getByText('Core Rules')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Playstyle')).toBeInTheDocument()
|
|
||||||
expect(screen.getByText('Run Variant')).toBeInTheDocument()
|
expect(screen.getByText('Run Variant')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Type Restriction')).toBeInTheDocument()
|
expect(screen.getByText('Type Restriction')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ 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 playstyleRules = visibleRules.filter((r) => r.category === 'playstyle')
|
|
||||||
const variantRules = visibleRules.filter((r) => r.category === 'variant')
|
const variantRules = visibleRules.filter((r) => r.category === 'variant')
|
||||||
|
|
||||||
const handleRuleChange = (key: keyof NuzlockeRules, value: boolean) => {
|
const handleRuleChange = (key: keyof NuzlockeRules, value: boolean) => {
|
||||||
@@ -62,9 +61,13 @@ export function RulesConfiguration({
|
|||||||
onChange({ ...rules, allowedTypes: next })
|
onChange({ ...rules, allowedTypes: next })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customRules = rules.customRules ?? ''
|
||||||
|
|
||||||
const enabledCount =
|
const enabledCount =
|
||||||
visibleRules.filter((r) => rules[r.key]).length + (allowedTypes.length > 0 ? 1 : 0)
|
visibleRules.filter((r) => rules[r.key]).length +
|
||||||
const totalCount = visibleRules.length + 1 // +1 for type restriction
|
(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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -106,21 +109,19 @@ 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">Playstyle</h3>
|
<h3 className="text-lg font-medium text-text-primary">Custom Rules</h3>
|
||||||
<p className="text-sm text-text-tertiary">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4">
|
<div className="px-4 py-4">
|
||||||
{playstyleRules.map((rule) => (
|
<textarea
|
||||||
<RuleToggle
|
value={customRules}
|
||||||
key={rule.key}
|
onChange={(e) => onChange({ ...rules, customRules: e.target.value })}
|
||||||
name={rule.name}
|
placeholder="e.g. No items in battle, Set mode only, must match boss team size..."
|
||||||
description={rule.description}
|
rows={4}
|
||||||
enabled={rules[rule.key]}
|
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"
|
||||||
onChange={(value) => handleRuleChange(rule.key, value)}
|
/>
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { CustomRulesDisplay } from './CustomRulesDisplay'
|
||||||
export { EggEncounterModal } from './EggEncounterModal'
|
export { EggEncounterModal } from './EggEncounterModal'
|
||||||
export { EncounterMethodBadge } from './EncounterMethodBadge'
|
export { EncounterMethodBadge } from './EncounterMethodBadge'
|
||||||
export { EncounterModal } from './EncounterModal'
|
export { EncounterModal } from './EncounterModal'
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
@plugin '@tailwindcss/typography';
|
||||||
|
|
||||||
/* ── Geist font family (variable, self-hosted) ─────────────────── */
|
/* ── Geist font family (variable, self-hosted) ─────────────────── */
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { useGenlocke } from '../hooks/useGenlockes'
|
import { useGenlocke } from '../hooks/useGenlockes'
|
||||||
import { usePokemonFamilies } from '../hooks/usePokemon'
|
import { usePokemonFamilies } from '../hooks/usePokemon'
|
||||||
import { GenlockeGraveyard, GenlockeLineage, StatCard, RuleBadges } from '../components'
|
import { CustomRulesDisplay, GenlockeGraveyard, GenlockeLineage, StatCard, RuleBadges } from '../components'
|
||||||
import type { GenlockeLegDetail, RetiredPokemon, RunStatus } from '../types'
|
import type { GenlockeLegDetail, RetiredPokemon, RunStatus } from '../types'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
@@ -226,6 +226,7 @@ export function GenlockeDetail() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-text-tertiary mb-2">Nuzlocke Rules</h3>
|
<h3 className="text-sm font-medium text-text-tertiary mb-2">Nuzlocke Rules</h3>
|
||||||
<RuleBadges rules={genlocke.nuzlockeRules} />
|
<RuleBadges rules={genlocke.nuzlockeRules} />
|
||||||
|
<CustomRulesDisplay customRules={genlocke.nuzlockeRules?.customRules ?? ''} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useParams, Link } from 'react-router-dom'
|
|||||||
import { useRun, useUpdateRun, useNamingCategories } from '../hooks/useRuns'
|
import { useRun, useUpdateRun, useNamingCategories } from '../hooks/useRuns'
|
||||||
import { useGameRoutes } from '../hooks/useGames'
|
import { useGameRoutes } from '../hooks/useGames'
|
||||||
import { useCreateEncounter, useUpdateEncounter } from '../hooks/useEncounters'
|
import { useCreateEncounter, useUpdateEncounter } from '../hooks/useEncounters'
|
||||||
import { StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components'
|
import { CustomRulesDisplay, StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components'
|
||||||
import type { RunStatus, EncounterDetail } from '../types'
|
import type { RunStatus, EncounterDetail } from '../types'
|
||||||
|
|
||||||
type TeamSortKey = 'route' | 'level' | 'species' | 'dex'
|
type TeamSortKey = 'route' | 'level' | 'species' | 'dex'
|
||||||
@@ -187,6 +187,7 @@ export function RunDashboard() {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-sm font-medium text-text-tertiary mb-2">Active Rules</h2>
|
<h2 className="text-sm font-medium text-text-tertiary mb-2">Active Rules</h2>
|
||||||
<RuleBadges rules={run.rules} />
|
<RuleBadges rules={run.rules} />
|
||||||
|
<CustomRulesDisplay customRules={run.rules?.customRules ?? ''} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Naming Scheme */}
|
{/* Naming Scheme */}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useCreateEncounter, useUpdateEncounter, useBulkRandomize } from '../hoo
|
|||||||
import { usePokemonFamilies } from '../hooks/usePokemon'
|
import { usePokemonFamilies } from '../hooks/usePokemon'
|
||||||
import { useGameBosses, useBossResults, useCreateBossResult } from '../hooks/useBosses'
|
import { useGameBosses, useBossResults, useCreateBossResult } from '../hooks/useBosses'
|
||||||
import {
|
import {
|
||||||
|
CustomRulesDisplay,
|
||||||
EggEncounterModal,
|
EggEncounterModal,
|
||||||
EncounterModal,
|
EncounterModal,
|
||||||
EncounterMethodBadge,
|
EncounterMethodBadge,
|
||||||
@@ -1077,12 +1078,10 @@ export function RunEncounters() {
|
|||||||
{nextBoss && (
|
{nextBoss && (
|
||||||
<span className="text-sm text-text-tertiary">
|
<span className="text-sm text-text-tertiary">
|
||||||
Next: {nextBoss.name}
|
Next: {nextBoss.name}
|
||||||
{run.rules?.bossTeamMatch && (
|
<span className="text-text-muted">
|
||||||
<span className="text-text-muted">
|
{' '}
|
||||||
{' '}
|
({getBossTeamSize(nextBoss.pokemon, starterName)} Pokémon)
|
||||||
({getBossTeamSize(nextBoss.pokemon, starterName)} Pokémon — match their team)
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!nextBoss && (
|
{!nextBoss && (
|
||||||
@@ -1135,6 +1134,7 @@ export function RunEncounters() {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-sm font-medium text-text-tertiary mb-2">Active Rules</h2>
|
<h2 className="text-sm font-medium text-text-tertiary mb-2">Active Rules</h2>
|
||||||
<RuleBadges rules={run.rules} />
|
<RuleBadges rules={run.rules} />
|
||||||
|
<CustomRulesDisplay customRules={run.rules?.customRules ?? ''} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Team Section */}
|
{/* Team Section */}
|
||||||
@@ -1602,7 +1602,6 @@ export function RunEncounters() {
|
|||||||
}}
|
}}
|
||||||
onClose={() => setSelectedBoss(null)}
|
onClose={() => setSelectedBoss(null)}
|
||||||
isPending={createBossResult.isPending}
|
isPending={createBossResult.isPending}
|
||||||
hardcoreMode={run?.rules?.hardcoreMode}
|
|
||||||
starterName={starterName}
|
starterName={starterName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ export interface NuzlockeRules {
|
|||||||
pinwheelClause: boolean
|
pinwheelClause: boolean
|
||||||
levelCaps: boolean
|
levelCaps: boolean
|
||||||
|
|
||||||
// Playstyle (informational, for stats/categorization)
|
|
||||||
hardcoreMode: boolean
|
|
||||||
setModeOnly: boolean
|
|
||||||
bossTeamMatch: boolean
|
|
||||||
|
|
||||||
// Variant (changes which Pokemon can appear)
|
// Variant (changes which Pokemon can appear)
|
||||||
egglocke: boolean
|
egglocke: boolean
|
||||||
wonderlocke: boolean
|
wonderlocke: boolean
|
||||||
@@ -19,10 +14,13 @@ export interface NuzlockeRules {
|
|||||||
|
|
||||||
// Type restriction (monolocke and variants)
|
// Type restriction (monolocke and variants)
|
||||||
allowedTypes: string[]
|
allowedTypes: string[]
|
||||||
|
|
||||||
|
// Free-text rules (markdown, for tracking custom/personal rules)
|
||||||
|
customRules: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Keys of NuzlockeRules that are boolean toggles (excludes array fields) */
|
/** Keys of NuzlockeRules that are boolean toggles (excludes array and string fields) */
|
||||||
type BooleanRuleKeys = Exclude<keyof NuzlockeRules, 'allowedTypes'>
|
type BooleanRuleKeys = Exclude<keyof NuzlockeRules, 'allowedTypes' | 'customRules'>
|
||||||
|
|
||||||
export const DEFAULT_RULES: NuzlockeRules = {
|
export const DEFAULT_RULES: NuzlockeRules = {
|
||||||
// Core rules
|
// Core rules
|
||||||
@@ -33,11 +31,6 @@ export const DEFAULT_RULES: NuzlockeRules = {
|
|||||||
pinwheelClause: true,
|
pinwheelClause: true,
|
||||||
levelCaps: false,
|
levelCaps: false,
|
||||||
|
|
||||||
// Playstyle - off by default
|
|
||||||
hardcoreMode: false,
|
|
||||||
setModeOnly: false,
|
|
||||||
bossTeamMatch: false,
|
|
||||||
|
|
||||||
// Variant - off by default
|
// Variant - off by default
|
||||||
egglocke: false,
|
egglocke: false,
|
||||||
wonderlocke: false,
|
wonderlocke: false,
|
||||||
@@ -45,13 +38,16 @@ export const DEFAULT_RULES: NuzlockeRules = {
|
|||||||
|
|
||||||
// Type restriction - no restriction by default
|
// Type restriction - no restriction by default
|
||||||
allowedTypes: [],
|
allowedTypes: [],
|
||||||
|
|
||||||
|
// Custom rules - empty by default
|
||||||
|
customRules: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuleDefinition {
|
export interface RuleDefinition {
|
||||||
key: BooleanRuleKeys
|
key: BooleanRuleKeys
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
category: 'core' | 'playstyle' | 'variant'
|
category: 'core' | 'variant'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RULE_DEFINITIONS: RuleDefinition[] = [
|
export const RULE_DEFINITIONS: RuleDefinition[] = [
|
||||||
@@ -99,28 +95,6 @@ export const RULE_DEFINITIONS: RuleDefinition[] = [
|
|||||||
category: 'core',
|
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',
|
|
||||||
name: 'Set Mode Only',
|
|
||||||
description:
|
|
||||||
'The game must be played in "Set" battle style, meaning you cannot switch Pokémon after knocking out an opponent.',
|
|
||||||
category: 'playstyle',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'bossTeamMatch',
|
|
||||||
name: 'Boss Team Match',
|
|
||||||
description:
|
|
||||||
'Limit your active party to the same number of Pokémon as the boss you are challenging.',
|
|
||||||
category: 'playstyle',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Variant
|
// Variant
|
||||||
{
|
{
|
||||||
key: 'egglocke',
|
key: 'egglocke',
|
||||||
|
|||||||
Reference in New Issue
Block a user