feat(engine): modifier profile types

This commit is contained in:
Joey Yakimowich-Payne 2026-04-18 22:05:58 -06:00
commit fea511790b
No known key found for this signature in database
2 changed files with 150 additions and 1 deletions

View file

@ -21,7 +21,8 @@
"motion": "^12.38.0",
"react-router-dom": "^7.14.1",
"sonner": "^2.0.7",
"tailwindcss": "^4.2.2"
"tailwindcss": "^4.2.2",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/canvas-confetti": "^1.9.0",

View file

@ -0,0 +1,148 @@
/**
* Type definitions for the piece modifier profile system.
*
* A ModifierProfile is a first-class entity (orthogonal to layouts and
* presets) that attaches rule modifiers to pieces by type ("all white
* knights have +1 HP") or by layout slot ("the piece on b1 has +2 range").
*
* Modifier profiles combine with layouts at game start. They are saved
* independently to a library (houserules:modifier-profiles:v1) and can be
* hot-swapped mid-game at turn boundaries.
*
* ADR-2: per-instance modifiers keyed by algebraic-notation square string
* (e.g. "b1"). Profile may optionally specify a layoutId for validation.
*/
import type { EntityId, Session } from "@paratype/rete";
import type { PieceType, PieceColor, ChessAttrKey } from "../schema.js";
import type { ZodType } from "zod";
/** The six T1 modifier category identifiers. */
export type ModifierKindId =
| "hp-bonus"
| "range-bonus"
| "direction-additions"
| "capture-flags"
| "promotion-override"
| "damage-resistance";
/**
* Named movement directions (from the perspective of the moving piece,
* color-independent — engine interprets "forward" as toward the opponent's
* back rank based on piece color).
*
* Used by DirectionAdditions modifier to specify additional movement directions.
*/
export type Direction =
| "forward" // toward opponent's back rank (1 square)
| "backward" // toward own back rank (1 square)
| "left" // queenside (from white's perspective)
| "right" // kingside (from white's perspective)
| "diagonal-fl" // forward-left
| "diagonal-fr" // forward-right
| "diagonal-bl" // backward-left
| "diagonal-br"; // backward-right
/**
* A modifier that applies to ALL pieces of a given type+color combo.
* `value` is the modifier-kind-specific value (number, string[], etc.)
*/
export interface TypeModifier {
readonly kind: ModifierKindId;
readonly pieceType: PieceType;
readonly color: PieceColor | "both";
readonly value: unknown;
}
/**
* A modifier that applies to the piece at a specific layout square.
* `square` is algebraic notation: "a1" through "h8".
* ADR-2: keyed by layout-slot (square string), not EntityId.
*/
export interface InstanceModifier {
readonly kind: ModifierKindId;
readonly square: string; // algebraic notation, e.g. "b1"
readonly value: unknown;
}
/**
* A complete modifier profile.
*
* Combines with a layout at game start: perType modifiers apply to all
* matching pieces; perInstance modifiers apply to pieces at specific squares.
*
* `layoutId` is optional — only required when perInstance entries are
* present, as per-instance modifiers are layout-slot-bound (ADR-2).
* When layoutId is absent and perInstance is non-empty, the validator
* emits a warning.
*/
export interface ModifierProfile {
readonly id: string;
readonly name: string;
readonly description: string;
/** Layout this profile's per-instance modifiers are bound to. Optional. */
readonly layoutId?: string;
readonly perType: readonly TypeModifier[];
readonly perInstance: readonly InstanceModifier[];
readonly version: 1;
readonly source: "premade" | "custom";
}
/**
* Descriptor for a modifier category. Each T1 modifier registers one
* descriptor into MODIFIER_REGISTRY at module load time (ADR-8).
*
* `V` is the value type (number for HpBonus, string[] for DirectionAdditions, etc.)
*/
export interface ModifierDescriptor<V = unknown> {
/** Matches ModifierKindId — used as the registry key. */
readonly id: ModifierKindId;
/** The ChessAttrMap key this modifier seeds on piece entities. */
readonly attrName: ChessAttrKey;
/** Human-readable display label for UI. */
readonly label: string;
/** Zod schema for the value field — used for validation and UI form generation. */
readonly valueSchema: ZodType<V>;
/** How multiple values from different sources stack (ADR-4). */
readonly stackingRule: "additive" | "union" | "multiplicative" | "priority-wins";
/**
* Apply the modifier's effective value to a piece entity in the session.
* Called once per (pieceId, kind) pair after all sources have been
* collected and the effective value computed via stacking rules.
*
* @param session - The game session to mutate
* @param pieceId - Target piece entity
* @param effectiveValue - The already-stacked value to apply
*/
readonly apply: (session: Session, pieceId: EntityId, effectiveValue: V) => void;
/** Return a human-readable description of this modifier value for UI. */
readonly describe: (value: V) => string;
/**
* Hint for the UI editor about which input widget to render for this modifier.
* Each uiForm maps to a specific React component in the PerTypePanel/PerInstancePanel.
*/
readonly uiForm:
| "number"
| "direction-set"
| "capture-flags"
| "promotion-target"
| "percentage";
}
/** Helper: create a TypeModifier with proper typing. */
export function typeModifier(
kind: ModifierKindId,
pieceType: PieceType,
color: PieceColor | "both",
value: unknown,
): TypeModifier {
return { kind, pieceType, color, value };
}
/** Helper: create an InstanceModifier with proper typing. */
export function instanceModifier(
kind: ModifierKindId,
square: string,
value: unknown,
): InstanceModifier {
return { kind, square, value };
}