diff --git a/packages/chess/src/presets/berolina-pawns-2.ts b/packages/chess/src/presets/berolina-pawns-2.ts index 595f102..bbfaec1 100644 --- a/packages/chess/src/presets/berolina-pawns-2.ts +++ b/packages/chess/src/presets/berolina-pawns-2.ts @@ -94,6 +94,7 @@ function emitMove( } PRESET_REGISTRY.register({ + category: "movement", id: PRESET_ID, name: "Berolina Pawns (Extended)", description: diff --git a/packages/chess/src/presets/berolina-pawns.ts b/packages/chess/src/presets/berolina-pawns.ts index 4994744..ba81b5d 100644 --- a/packages/chess/src/presets/berolina-pawns.ts +++ b/packages/chess/src/presets/berolina-pawns.ts @@ -134,6 +134,7 @@ function emitMove( } PRESET_REGISTRY.register({ + category: "movement", id: PRESET_ID, name: "Berolina Pawns", description: diff --git a/packages/chess/src/presets/bishops-ignore-color.ts b/packages/chess/src/presets/bishops-ignore-color.ts index 43f53e7..1e59a64 100644 --- a/packages/chess/src/presets/bishops-ignore-color.ts +++ b/packages/chess/src/presets/bishops-ignore-color.ts @@ -18,6 +18,7 @@ const ORTHOGONAL_DELTAS: ReadonlyArray = [ ]; PRESET_REGISTRY.register({ + category: "movement", id: "bishops-ignore-color", name: "Colour-Blind Bishops", description: "Bishops may additionally step one square orthogonally to an empty or enemy-occupied square.", diff --git a/packages/chess/src/presets/bouncing-pieces-2.ts b/packages/chess/src/presets/bouncing-pieces-2.ts index 89cf786..abdd6c3 100644 --- a/packages/chess/src/presets/bouncing-pieces-2.ts +++ b/packages/chess/src/presets/bouncing-pieces-2.ts @@ -192,6 +192,7 @@ function walkBouncingRay( } PRESET_REGISTRY.register({ + category: "movement", id: "bouncing-pieces-2", name: "Bouncing Pieces (Extended)", description: diff --git a/packages/chess/src/presets/bouncing-pieces.ts b/packages/chess/src/presets/bouncing-pieces.ts index 232932c..4875bcd 100644 --- a/packages/chess/src/presets/bouncing-pieces.ts +++ b/packages/chess/src/presets/bouncing-pieces.ts @@ -203,6 +203,7 @@ function walkBouncingRay( } PRESET_REGISTRY.register({ + category: "movement", id: "bouncing-pieces", name: "Bouncing Pieces", description: diff --git a/packages/chess/src/presets/capture-all.ts b/packages/chess/src/presets/capture-all.ts index eaa68ae..55fff74 100644 --- a/packages/chess/src/presets/capture-all.ts +++ b/packages/chess/src/presets/capture-all.ts @@ -94,6 +94,7 @@ import { PRESET_REGISTRY } from "./registry.js"; import type { GameResult } from "../engine.js"; PRESET_REGISTRY.register({ + category: "objective", id: "capture-all", name: "Capture All", description: "Capture every enemy piece to win. Kings are not royal.", diff --git a/packages/chess/src/presets/capture-to-win.ts b/packages/chess/src/presets/capture-to-win.ts index b811ee8..4815644 100644 --- a/packages/chess/src/presets/capture-to-win.ts +++ b/packages/chess/src/presets/capture-to-win.ts @@ -34,6 +34,7 @@ interface CaptureToWinState extends Record { } PRESET_REGISTRY.register({ + category: "objective", id: "capture-to-win", name: "First Blood", description: diff --git a/packages/chess/src/presets/coregal.ts b/packages/chess/src/presets/coregal.ts index 88ce927..d1ec102 100644 --- a/packages/chess/src/presets/coregal.ts +++ b/packages/chess/src/presets/coregal.ts @@ -64,6 +64,7 @@ import { PRESET_REGISTRY } from "./registry.js"; import type { EntityId } from "@paratype/rete"; PRESET_REGISTRY.register({ + category: "king", id: "coregal", name: "Coregal", description: diff --git a/packages/chess/src/presets/double-move.ts b/packages/chess/src/presets/double-move.ts index a70879a..76f31c2 100644 --- a/packages/chess/src/presets/double-move.ts +++ b/packages/chess/src/presets/double-move.ts @@ -75,6 +75,7 @@ import { PRESET_REGISTRY } from "./registry.js"; PRESET_REGISTRY.register({ + category: "multi-move", id: "double-move", name: "Double Move", description: diff --git a/packages/chess/src/presets/double-pawn-sprint.ts b/packages/chess/src/presets/double-pawn-sprint.ts index 8ac3829..1a45ce9 100644 --- a/packages/chess/src/presets/double-pawn-sprint.ts +++ b/packages/chess/src/presets/double-pawn-sprint.ts @@ -54,6 +54,7 @@ function getDoubleSprintMove(engine: ChessEngine, pieceId: EntityId): LegalMove[ } PRESET_REGISTRY.register({ + category: "movement", id: "double-pawn-sprint", name: "Perpetual Sprint", description: diff --git a/packages/chess/src/presets/dual-king.ts b/packages/chess/src/presets/dual-king.ts index 10f7cc0..4473982 100644 --- a/packages/chess/src/presets/dual-king.ts +++ b/packages/chess/src/presets/dual-king.ts @@ -70,6 +70,7 @@ import { PRESET_REGISTRY } from "./registry.js"; import type { EntityId } from "@paratype/rete"; PRESET_REGISTRY.register({ + category: "king", id: "dual-king", name: "Dual King", description: diff --git a/packages/chess/src/presets/explosive-rook.ts b/packages/chess/src/presets/explosive-rook.ts index 76540c2..2582935 100644 --- a/packages/chess/src/presets/explosive-rook.ts +++ b/packages/chess/src/presets/explosive-rook.ts @@ -39,6 +39,7 @@ function pieceAtSquare(session: Session, sq: Square): EntityId | null { } PRESET_REGISTRY.register({ + category: "pieces", id: "explosive-rook", name: "Detonating Rook", description: diff --git a/packages/chess/src/presets/extinction-chess.ts b/packages/chess/src/presets/extinction-chess.ts index ed70f4e..8b4a07c 100644 --- a/packages/chess/src/presets/extinction-chess.ts +++ b/packages/chess/src/presets/extinction-chess.ts @@ -144,6 +144,7 @@ interface ExtinctionChessState extends Record { const DEFAULT_TARGET_TYPE: PieceType = "pawn"; PRESET_REGISTRY.register({ + category: "objective", id: "extinction-chess", name: "Extinction", description: diff --git a/packages/chess/src/presets/first-promotion-wins.ts b/packages/chess/src/presets/first-promotion-wins.ts index 89ba501..76773fb 100644 --- a/packages/chess/src/presets/first-promotion-wins.ts +++ b/packages/chess/src/presets/first-promotion-wins.ts @@ -118,6 +118,7 @@ interface FirstPromotionWinsState extends Record { } PRESET_REGISTRY.register({ + category: "objective", id: "first-promotion-wins", name: "First to Promote Wins", description: diff --git a/packages/chess/src/presets/king-heals.ts b/packages/chess/src/presets/king-heals.ts index 15e788f..a478d6b 100644 --- a/packages/chess/src/presets/king-heals.ts +++ b/packages/chess/src/presets/king-heals.ts @@ -35,6 +35,7 @@ function findKing(session: Session, color: PieceColor): EntityId | null { } PRESET_REGISTRY.register({ + category: "pieces", id: "king-heals", name: "Regenerating King", description: diff --git a/packages/chess/src/presets/knight-immunity.ts b/packages/chess/src/presets/knight-immunity.ts index b27deb7..a6aa33b 100644 --- a/packages/chess/src/presets/knight-immunity.ts +++ b/packages/chess/src/presets/knight-immunity.ts @@ -1,6 +1,7 @@ import { PRESET_REGISTRY } from "./registry.js"; PRESET_REGISTRY.register({ + category: "pieces", id: "knight-immunity", name: "Bishop-Proof Knights", description: "Knights are immune to capture by Bishops. Bishops cannot make moves that would capture a Knight.", diff --git a/packages/chess/src/presets/knightmate-rules.ts b/packages/chess/src/presets/knightmate-rules.ts index 4dbead6..573fedf 100644 --- a/packages/chess/src/presets/knightmate-rules.ts +++ b/packages/chess/src/presets/knightmate-rules.ts @@ -46,6 +46,7 @@ import { PRESET_REGISTRY } from "./registry.js"; import type { EntityId } from "@paratype/rete"; PRESET_REGISTRY.register({ + category: "king", id: "knightmate-rules", name: "Knightmate", description: diff --git a/packages/chess/src/presets/knights-leap-twice.ts b/packages/chess/src/presets/knights-leap-twice.ts index 30968ca..730bf30 100644 --- a/packages/chess/src/presets/knights-leap-twice.ts +++ b/packages/chess/src/presets/knights-leap-twice.ts @@ -12,6 +12,7 @@ import { isAllyAt, isPieceAt } from "../rules/board-queries.js"; import { PRESET_REGISTRY } from "./registry.js"; PRESET_REGISTRY.register({ + category: "movement", id: "knights-leap-twice", name: "Double-Leap Knights", description: "Knights move in two consecutive L-shapes in a single turn; the intermediate square must be empty.", diff --git a/packages/chess/src/presets/last-piece-standing.ts b/packages/chess/src/presets/last-piece-standing.ts index 2a58b43..412eb6a 100644 --- a/packages/chess/src/presets/last-piece-standing.ts +++ b/packages/chess/src/presets/last-piece-standing.ts @@ -29,6 +29,7 @@ import { PRESET_REGISTRY } from "./registry.js"; import type { GameResult } from "../engine.js"; PRESET_REGISTRY.register({ + category: "objective", id: "last-piece-standing", name: "Annihilation", description: diff --git a/packages/chess/src/presets/monster-rules.ts b/packages/chess/src/presets/monster-rules.ts index 66d6a1a..b243cea 100644 --- a/packages/chess/src/presets/monster-rules.ts +++ b/packages/chess/src/presets/monster-rules.ts @@ -63,6 +63,7 @@ import { PRESET_REGISTRY } from "./registry.js"; const PRESET_ID = "monster-rules"; PRESET_REGISTRY.register({ + category: "multi-move", id: PRESET_ID, name: "Monster", description: diff --git a/packages/chess/src/presets/pawn-diagonal-no-capture.ts b/packages/chess/src/presets/pawn-diagonal-no-capture.ts index 9217553..18e8cc6 100644 --- a/packages/chess/src/presets/pawn-diagonal-no-capture.ts +++ b/packages/chess/src/presets/pawn-diagonal-no-capture.ts @@ -40,6 +40,7 @@ function getDiagonalQuietMoves(engine: ChessEngine, pieceId: EntityId): LegalMov } PRESET_REGISTRY.register({ + category: "movement", id: "pawn-diagonal-no-capture", name: "Slanting Pawns", description: diff --git a/packages/chess/src/presets/pawns-move-backward.ts b/packages/chess/src/presets/pawns-move-backward.ts index 1f12999..88061dc 100644 --- a/packages/chess/src/presets/pawns-move-backward.ts +++ b/packages/chess/src/presets/pawns-move-backward.ts @@ -40,6 +40,7 @@ function getPawnBackwardMove(engine: ChessEngine, pieceId: EntityId): LegalMove[ } PRESET_REGISTRY.register({ + category: "movement", id: "pawns-move-backward", name: "Backward-Marching Pawns", description: diff --git a/packages/chess/src/presets/piece-hp.ts b/packages/chess/src/presets/piece-hp.ts index 0421322..446393e 100644 --- a/packages/chess/src/presets/piece-hp.ts +++ b/packages/chess/src/presets/piece-hp.ts @@ -74,6 +74,7 @@ function iteratePieceIds(session: Session): EntityId[] { } PRESET_REGISTRY.register({ + category: "pieces", id: "piece-hp", name: "Hit Points", description: diff --git a/packages/chess/src/presets/poisoned-squares.ts b/packages/chess/src/presets/poisoned-squares.ts index 931211b..3353234 100644 --- a/packages/chess/src/presets/poisoned-squares.ts +++ b/packages/chess/src/presets/poisoned-squares.ts @@ -25,6 +25,7 @@ import type { EntityId } from "@paratype/rete"; export const POISONED_SQUARES: ReadonlySet = new Set([27, 28, 35, 36]); PRESET_REGISTRY.register({ + category: "pieces", id: "poisoned-squares", name: "Poisoned Centre", description: diff --git a/packages/chess/src/presets/queen-splits.ts b/packages/chess/src/presets/queen-splits.ts index 2646dc7..80f6285 100644 --- a/packages/chess/src/presets/queen-splits.ts +++ b/packages/chess/src/presets/queen-splits.ts @@ -82,6 +82,7 @@ function pieceAtSquare(session: Session, sq: Square): EntityId | null { // on fission-spawned pieces automatically. PRESET_REGISTRY.register({ + category: "movement", id: "queen-splits", name: "Queen Fission", description: diff --git a/packages/chess/src/presets/registry.ts b/packages/chess/src/presets/registry.ts index e7dc15b..d855c31 100644 --- a/packages/chess/src/presets/registry.ts +++ b/packages/chess/src/presets/registry.ts @@ -328,6 +328,11 @@ export interface PresetDef { readonly id: string; readonly name: string; readonly description: string; + /** + * The category this preset belongs to for UI grouping. + * Defaults to "misc" if not specified. + */ + readonly category?: "king" | "objective" | "movement" | "multi-move" | "pieces" | "misc"; /** Preset IDs whose activation must NOT overlap with this one. */ readonly incompatibleWith: readonly string[]; /** Preset IDs that must also be active for this one to be valid. */ diff --git a/packages/chess/src/presets/rook-warp.ts b/packages/chess/src/presets/rook-warp.ts index a3197b6..edb7689 100644 --- a/packages/chess/src/presets/rook-warp.ts +++ b/packages/chess/src/presets/rook-warp.ts @@ -36,6 +36,7 @@ function warpDest( } PRESET_REGISTRY.register({ + category: "movement", id: "rook-warp", name: "Rook Warp", description: "After a rook's normal slide, it may warp around the board edge to the opposite end of its rank or file.", diff --git a/packages/chess/src/presets/suicide-chess.ts b/packages/chess/src/presets/suicide-chess.ts index 090c827..7f41074 100644 --- a/packages/chess/src/presets/suicide-chess.ts +++ b/packages/chess/src/presets/suicide-chess.ts @@ -126,6 +126,7 @@ import { PRESET_REGISTRY } from "./registry.js"; import type { ChessEngine, GameResult } from "../engine.js"; PRESET_REGISTRY.register({ + category: "objective", id: "suicide-chess", name: "Suicide Chess (Antichess)", description: diff --git a/packages/chess/src/presets/weak-dual-king.ts b/packages/chess/src/presets/weak-dual-king.ts index 3fab211..8d8dc9e 100644 --- a/packages/chess/src/presets/weak-dual-king.ts +++ b/packages/chess/src/presets/weak-dual-king.ts @@ -219,6 +219,7 @@ function hasSavingMove(engine: ChessEngine): boolean { } PRESET_REGISTRY.register({ + category: "king", id: "weak-dual-king", name: "Weak Dual King", description: diff --git a/packages/chess/src/presets/wrap-board.ts b/packages/chess/src/presets/wrap-board.ts index ca355d1..17661dc 100644 --- a/packages/chess/src/presets/wrap-board.ts +++ b/packages/chess/src/presets/wrap-board.ts @@ -202,6 +202,7 @@ function computeCylinderMoves( } PRESET_REGISTRY.register({ + category: "movement", id: "wrap-board", name: "Cylindrical Board", description: diff --git a/packages/chess/src/ui/LayoutPicker.tsx b/packages/chess/src/ui/LayoutPicker.tsx index 1f7aaf1..1553613 100644 --- a/packages/chess/src/ui/LayoutPicker.tsx +++ b/packages/chess/src/ui/LayoutPicker.tsx @@ -22,15 +22,21 @@ import { useMemo } from 'react'; import { LAYOUT_REGISTRY, + PRESET_REGISTRY, buildChess960Layout, type StartingLayout, } from '@paratype/chess'; +import type { PresetActivation } from '../net/types.js'; export interface LayoutPickerProps { /** Currently selected layout. Parents own the state. */ value: StartingLayout; /** Called when the user picks a new premade. */ onChange: (layout: StartingLayout) => void; + /** Current authoritative preset activations (for toggling suggested rules). */ + activations?: readonly PresetActivation[]; + /** Replace the active set on the engine / server. */ + setPresets?: (activations: PresetActivation[]) => void; /** Called when the user picks the "Custom…" entry. */ onCustomRequested?: () => void; /** Disables the control (during network requests, etc). */ @@ -40,6 +46,8 @@ export interface LayoutPickerProps { export function LayoutPicker({ value, onChange, + activations = [], + setPresets, onCustomRequested, disabled = false, }: LayoutPickerProps) { @@ -75,6 +83,16 @@ export function LayoutPicker({ } } + const togglePreset = (presetId: string) => { + if (!setPresets) return; + const isActive = activations.some(a => a.id === presetId); + if (isActive) { + setPresets(activations.filter(a => a.id !== presetId)); + } else { + setPresets([...activations, { id: presetId, scope: 'both', turnsRemaining: null }]); + } + }; + return (
); diff --git a/packages/chess/src/ui/Lobby.tsx b/packages/chess/src/ui/Lobby.tsx index 652bef1..0bd34a4 100644 --- a/packages/chess/src/ui/Lobby.tsx +++ b/packages/chess/src/ui/Lobby.tsx @@ -12,6 +12,7 @@ import { validateLayout, type StartingLayout, } from '@paratype/chess'; +import type { PresetActivation } from '../net/types.js'; import type { LayoutRequest } from '../net/types'; import { LayoutPicker } from './LayoutPicker'; import { LayoutEditor } from './LayoutEditor'; @@ -90,6 +91,9 @@ export function Lobby({ chessState }: LobbyProps = {}) { * wire — the server protocol carries one ModifierProfile per room * (T2). Multi-stack on the wire is a future T27+ extension. */ + // Suggested rules driven by LayoutPicker + const [presets, setPresets] = useState([]); + const [additionalProfiles, setAdditionalProfiles] = useState< ModifierProfile[] >([]); @@ -301,6 +305,9 @@ export function Lobby({ chessState }: LobbyProps = {}) { if (selectedProfile !== null) { createPayload.profile = selectedProfile; } + if (presets.length > 0) { + createPayload.rulesetIds = presets.map(p => p.id); + } const { code, token, color, layout: resolvedLayout, profile: echoedProfile } = await oneShotRoomRequest('room.create', createPayload); @@ -428,6 +435,8 @@ export function Lobby({ chessState }: LobbyProps = {}) { setEditorOpen(true)} disabled={loading} /> diff --git a/packages/chess/src/ui/RulesDrawer.tsx b/packages/chess/src/ui/RulesDrawer.tsx index 6cf29ea..68b636d 100644 --- a/packages/chess/src/ui/RulesDrawer.tsx +++ b/packages/chess/src/ui/RulesDrawer.tsx @@ -223,7 +223,7 @@ export function RulesDrawer({ data-testid="rules-drawer" className="fixed top-0 right-0 h-full w-full max-w-md bg-white shadow-2xl z-50 flex flex-col overflow-hidden border-l border-neutral-200/50" > -
+

Live Rules @@ -243,272 +243,293 @@ export function RulesDrawer({

-
- {presets.map((preset) => { - const active = activeById.get(preset.id); - const isOn = active !== undefined; - - // Compute whether this preset CAN be toggled right now - // given the currently-active set. Pre-checking mirrors - // the engine's own validation in - // `ActivePresetSet.replaceAll` so we don't round-trip - // to the server just to get an error back (which - // would produce a misleading "Preset enabled" toast - // followed by an error banner). Scope defaults to - // 'both', so overlap is always true for this quick - // check. - // - // Enable block: a currently-active preset conflicts, - // OR a required preset isn't active. - // Disable block: another active preset REQUIRES this - // one. (Turning off piece-hp while - // king-heals is on would fail.) - // - // Returns null when the toggle action would succeed, - // otherwise a structured reason used to drive the - // disabled toggle + tooltip. - let blockReason: - | null - | { kind: 'conflicts'; withName: string } - | { kind: 'missing'; needsName: string } - | { kind: 'depended-on-by'; dependentName: string } = null; - if (!isOn) { - for (const otherId of preset.incompatibleWith) { - if (activeById.has(otherId)) { - blockReason = { - kind: 'conflicts', - withName: nameById(otherId), - }; - break; - } - } - if (blockReason === null) { - for (const depId of preset.requires) { - if (!activeById.has(depId)) { - blockReason = { - kind: 'missing', - needsName: nameById(depId), - }; - break; - } - } - } - } else { - // Disable path: check whether any OTHER active - // preset requires this one. If so, disabling would - // leave that preset with an unmet dependency and - // the server / engine would reject the whole set. - for (const other of presets) { - if (!activeById.has(other.id)) continue; - if (other.id === preset.id) continue; - if (other.requires.includes(preset.id)) { - blockReason = { - kind: 'depended-on-by', - dependentName: other.name, - }; - break; - } - } - } - const toggleDisabled = blockReason !== null; - const blockMessage = - blockReason === null - ? undefined - : blockReason.kind === 'conflicts' - ? `Disable "${blockReason.withName}" first — it conflicts with this rule` - : blockReason.kind === 'missing' - ? `Enable "${blockReason.needsName}" first — this rule requires it` - : `Disable "${blockReason.dependentName}" first — it requires this rule`; +
+ {['king', 'objective', 'movement', 'multi-move', 'pieces', 'misc'].map((categoryId) => { + const categoryPresets = presets.filter(p => (p.category ?? 'misc') === categoryId); + if (categoryPresets.length === 0) return null; + + const categoryName = { + 'king': 'King Variants', + 'objective': 'Objectives', + 'movement': 'Movement', + 'multi-move': 'Multi-move', + 'pieces': 'Pieces', + 'misc': 'Misc' + }[categoryId]; return ( -
-
-
-

- {preset.name} -

-

- {preset.description} -

- {(preset.requires.length > 0 || - preset.incompatibleWith.length > 0) && ( -
- {preset.requires.map((depId) => { - const depActive = activeById.has(depId); - return ( - - - Requires {nameById(depId)} - - ); - })} - {preset.incompatibleWith.map((otherId) => { - const otherActive = activeById.has(otherId); - return ( - - - Conflicts with {nameById(otherId)} - - ); - })} -
- )} -
- -
+
+

{categoryName}

+
+ {categoryPresets.map((preset) => { + const active = activeById.get(preset.id); + const isOn = active !== undefined; - {isOn && ( -
- {/* Scope radios */} -
- + // Compute whether this preset CAN be toggled right now + // given the currently-active set. Pre-checking mirrors + // the engine's own validation in + // `ActivePresetSet.replaceAll` so we don't round-trip + // to the server just to get an error back (which + // would produce a misleading "Preset enabled" toast + // followed by an error banner). Scope defaults to + // 'both', so overlap is always true for this quick + // check. + // + // Enable block: a currently-active preset conflicts, + // OR a required preset isn't active. + // Disable block: another active preset REQUIRES this + // one. (Turning off piece-hp while + // king-heals is on would fail.) + // + // Returns null when the toggle action would succeed, + // otherwise a structured reason used to drive the + // disabled toggle + tooltip. + let blockReason: + | null + | { kind: 'conflicts'; withName: string } + | { kind: 'missing'; needsName: string } + | { kind: 'depended-on-by'; dependentName: string } = null; + if (!isOn) { + for (const otherId of preset.incompatibleWith) { + if (activeById.has(otherId)) { + blockReason = { + kind: 'conflicts', + withName: nameById(otherId), + }; + break; + } + } + if (blockReason === null) { + for (const depId of preset.requires) { + if (!activeById.has(depId)) { + blockReason = { + kind: 'missing', + needsName: nameById(depId), + }; + break; + } + } + } + } else { + // Disable path: check whether any OTHER active + // preset requires this one. If so, disabling would + // leave that preset with an unmet dependency and + // the server / engine would reject the whole set. + for (const other of presets) { + if (!activeById.has(other.id)) continue; + if (other.id === preset.id) continue; + if (other.requires.includes(preset.id)) { + blockReason = { + kind: 'depended-on-by', + dependentName: other.name, + }; + break; + } + } + } + const toggleDisabled = blockReason !== null; + const blockMessage = + blockReason === null + ? undefined + : blockReason.kind === 'conflicts' + ? `Disable "${blockReason.withName}" first — it conflicts with this rule` + : blockReason.kind === 'missing' + ? `Enable "${blockReason.needsName}" first — this rule requires it` + : `Disable "${blockReason.dependentName}" first — it requires this rule`; + + return (
- {(['both', 'white', 'black'] as const).map( - (scope) => { - const selected = active.scope === scope; - return ( - - ); - }, + {preset.requires.map((depId) => { + const depActive = activeById.has(depId); + return ( + + + Requires {nameById(depId)} + + ); + })} + {preset.incompatibleWith.map((otherId) => { + const otherActive = activeById.has(otherId); + return ( + + + Conflicts with {nameById(otherId)} + + ); + })} +
+ )} +
+ +
+ + {isOn && ( +
+ {/* Scope radios */} +
+ +
+ {(['both', 'white', 'black'] as const).map( + (scope) => { + const selected = active.scope === scope; + return ( + + ); + }, + )} +
+
+ + {/* Duration input */} +
+ +
+ setDuration(preset.id, e)} + className="w-20 px-2 py-1 border border-neutral-300 rounded-md text-sm text-right focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:border-transparent" + /> + + {active.turnsRemaining === null + ? 'permanent' + : 'turns'} + +
+
+
)}
-
- - {/* Duration input */} -
- -
- setDuration(preset.id, e)} - className="w-20 px-2 py-1 border border-neutral-300 rounded-md text-sm text-right focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:border-transparent" - /> - - {active.turnsRemaining === null - ? 'permanent' - : 'turns'} - -
-
-
- )} + ); + })} +
); })} -