# Custom Modifiers — User Guide Houserules ships with six built-in modifiers (HP Bonus, Range Bonus, Direction Additions, Capture Flags, Promotion Override, Damage Resistance). Custom modifiers let you author your own from a fixed catalog of 15 reusable effect primitives — no programming required. A custom modifier is a named, reusable bundle of primitive effects that any modifier profile can reference, exactly the same way it references a built-in. --- ## Opening the editor 1. Open the **Modifier Profile** editor (`+ Modifier Profile` from the rules drawer, or via the layout's modifier-profile picker). 2. In the editor's header, click **+ Custom Modifier**. The Custom Modifier editor opens as a nested modal. 3. The Custom Modifier editor is a 3-column workspace: - **Left**: primitive palette, grouped by category. Hover any entry for a full tooltip with the primitive's long description and a worked example. - **Center**: the descriptor's primitive tree (the composition you're building). - **Right**: parameter inspector for the selected primitive. The top of the inspector shows an in-editor docs panel — a longer behaviour explanation plus one or more concrete example configurations you can copy. Click **Hide docs & examples** to collapse it when you want only the form. Each descriptor needs a **name** (1-40 chars) and an optional **description** (0-200 chars). The header exposes **Templates**, **Load**, **Save**, and live validation status. ### Templates — starter recipes The **Templates** button opens a picker with pre-composed recipes drawn from the examples below (Boosted Pawn, 3-Charge Shield, Aura King, Vampire, Low-HP Fortress). Picking a template replaces the current draft with a fresh copy — the id is regenerated so you can save the loaded template as your own library entry without collisions. ### Attribute-name autocomplete Primitives that target an attribute (`seed-attribute`, `add-to-attribute`, `multiply-attribute`, `absorb-damage-with-attribute`, `add-aura`) render their `attr` / `targetAttr` field as a **combobox** with grouped suggestions: - **Core numeric attributes** — Hp, HpBonus, RangeBonus, DamageResistance, ReflectDamagePercent, AbsorbDamageRate, HalfmoveClock, FullmoveNumber. Safe targets for add/multiply/aura/absorb primitives. - **Core non-numeric attributes** — CaptureFlags, DirectionAdditions, PromotionOverride, BlockedMoveTypes, AbsorbDamageAttr, HasMoved. Seed-only for most; avoid add/multiply. - **User-defined examples** — ShieldCharges, ArmorPlates, BloodStacks, ManaPool. Illustrative names matching the recipes; invent your own. The combobox is **free-form** — type any name and hit Enter to accept it, even if it isn't in the list. Numeric-context primitives (add, multiply, aura, absorb) rank numeric suggestions first. **Declare vs. consume sites.** The combobox distinguishes between primitives that *declare* a new attribute and primitives that *consume* an existing one: - **`seed-attribute.attr`** is a **declare-site** — any name is fine, including inventing brand new counters. The illustrative "User-defined examples" group is surfaced for inspiration. Typed values outside both the schema and the examples catalog carry an amber `user-defined` badge. - **`add-to-attribute.attr`**, **`multiply-attribute.attr`**, **`absorb-damage-with-attribute.attr`**, and **`add-aura.targetAttr`** are **consume-sites** — they read an attribute that must already exist. The illustrative examples group is **hidden** because those names are fiction until something seeds them. Instead, a "Seeded in this descriptor" group surfaces attrs that `seed-attribute` primitives elsewhere in the current tree actually declare. Typed names that aren't built-in and aren't seeded carry a red `not seeded` warning; names that are seeded carry an emerald `seeded` confirmation. --- ## The 15 effect primitives Primitives are atomic, composable, and pure data. Click any palette entry to add it to the current descriptor's tree. ### State primitives These mutate facts on the piece at apply time. #### `seed-attribute` Seeds a fact `{ attr, value }` on the piece. Overwrites any existing value. > **Example**: `attr=Hp, value=5` — gives the piece 5 HP regardless of the > baseline. #### `add-to-attribute` Reads the existing numeric value of `attr` (treats absent as 0) and writes `existing + delta`. > **Example**: `attr=HpBonus, delta=2` — adds +2 to whatever HpBonus is > already there. #### `multiply-attribute` Reads the existing numeric value of `attr` (no-op if absent) and writes `existing * factor`. > **Example**: `attr=Hp, factor=2` — doubles HP. #### `add-direction` Appends named directions into `DirectionAdditions`. Composes additively with the built-in Direction Additions modifier — both write to the same fact and deduplicate by direction name. > **Example**: `directions=[backward]` — adds backward movement. #### `set-capture-flag` ORs a `CaptureFlag` bitflag into the piece's `CaptureFlags`. > **Example**: `flag=CANNOT_BE_CAPTURED` — makes the piece untargetable. ### Mechanic primitives These plug into the engine's existing pipelines (damage, movement, promotion). #### `absorb-damage-with-attribute` Seeds `{ AbsorbDamageAttr, AbsorbDamageRate }`. Each incoming damage point consumes `rate` of `attr` instead of HP, until `attr` is exhausted. > **Example**: `attr=ShieldCharges, rate=1` — paired with a `seed-attribute` > for `ShieldCharges=3` produces a 3-charge shield. #### `reflect-damage` Seeds `ReflectDamagePercent`. When this piece takes damage, `percentage%` is reflected back to the attacker. > **Example**: `percentage=50` — half-reflective armour. #### `modify-movement-range` Composes additively with the built-in Range Bonus. > **Example**: `delta=1` — adds +1 to the piece's range. #### `block-move-type` Filters out moves matching the given type (`capture` / `step` / `slide`). > **Example**: `moveType=capture` — pacifist piece, can move but not capture. #### `override-promotion` Sets `PromotionOverride` to a target piece type. Mirrors the built-in Promotion Override modifier. > **Example**: `target=knight` — pawns promote to knights only. ### Advanced primitives These compose other primitives. #### `add-aura` Seeds an `AuraSpec` entry. Every piece within `radius` (Chebyshev / king-move distance) gets `delta` added to `targetAttr`. > **Example**: `radius=2, targetAttr=HpBonus, delta=1` — every piece within 2 > squares gets +1 HP. Auras recompute after every move. A piece moving INTO range picks up the contribution; a piece moving OUT loses it on the next pass. #### `on-turn-start` Wraps a list of nested primitives that run when this piece's color begins a turn. > **Example**: `primitives=[{kind: 'add-to-attribute', params: {attr: 'Hp', delta: 1}}]` > — heals 1 HP each turn. #### `on-capture` Wraps a list of nested primitives that run when this piece captures another. > **Example**: `primitives=[{kind: 'add-to-attribute', params: {attr: 'Hp', delta: 1}}]` > — vampire piece, heals on capture. #### `on-damaged` Wraps a list of nested primitives that run when this piece takes damage. > **Example**: `primitives=[{kind: 'reflect-damage', params: {percentage: 25}}]` > — auto-reflects on damage. #### `conditional` Branches on a condition and runs the matching primitive list. Supported condition types: - `attr-lt` / `attr-gt` — numeric comparison - `attr-eq` — exact match (string / number / boolean / null) - `always` — runs `then` - `never` — runs `else` (or no-op if no else) > **Example**: > ``` > condition: { type: 'attr-lt', attr: 'Hp', value: 2 } > then: [{ kind: 'set-capture-flag', params: { flag: 'CANNOT_BE_CAPTURED' }}] > ``` > — when low on HP, becomes invulnerable. > **Note (T3 limitation)**: Trigger primitives (`on-turn-start`, `on-capture`, > `on-damaged`) and `conditional` currently SEED their hook facts on the piece > but the engine pipeline that fires them at the corresponding game phase is > not yet wired in T3. The data is correct; the runtime trigger evaluation > ships in a follow-up. --- ## Composing primitives — simple to complex ### Simple: a "boosted pawn" One `add-to-attribute` primitive with `attr=HpBonus, delta=2`. Save as "Boosted Pawn". Reference from a profile's per-type entry to give every white pawn +2 HP. ### Medium: a "shield" Two primitives: 1. `seed-attribute`: `attr=ShieldCharges, value=3` 2. `absorb-damage-with-attribute`: `attr=ShieldCharges, rate=1` Pieces start with 3 shield charges; each damage point depletes one charge before HP. ### Complex: an "aura king" One primitive: 1. `add-aura`: `radius=2, targetAttr=HpBonus, delta=1` Apply to the king's per-type entry. Every piece within 2 squares of the king gets +1 HP. --- ## Saving and loading The editor's **Save** button writes the current descriptor to your local **custom modifier library** (storage key `houserules:custom-modifiers:v1`). **Load from library** opens a picker to recall any saved descriptor. The library is local to your browser. To share, paste the JSON shape of a saved descriptor — or use the modifier profile sharing flow (which embeds the custom descriptor in the share payload). ### Library limits - **20 descriptors per library** — same FIFO eviction rule as profiles (oldest non-starred entry is evicted; star to protect). - **10 descriptors per multiplayer room** — a room can register up to 10 custom descriptors. Servers reject the 11th. --- ## Using a custom modifier in a profile Open the Modifier Profile editor, add a **Per-Type** or **Per-Instance** entry (left or center panel), and pick your custom descriptor from the **Kind** dropdown. Custom descriptors appear in the dropdown under a **Custom (from library)** group. Custom modifiers don't take a per-instance value — the descriptor's primitive list is the entire payload. The editor displays a small summary card ("Custom modifier — N primitives") instead of a value input. --- ## Multi-profile stacking The lobby's profile picker stays single-select for the primary profile. Below it, a **Stacked (solo only)** list lets you append additional profiles in order. Use the up/down arrows to reorder. When multiple profiles are active, contributions stack across profiles per the same per-kind rules used within a single profile. The "priority-wins" rule (promotion override, capture flags) gives the LAST profile in the list precedence. > **Limitation (T3)**: Multi-profile stacking is solo-only on the wire. > Multiplayer rooms still send one profile per `room.create`. Wire-level > stacking is a follow-up. --- ## Aura effects in detail `add-aura` measures distance with the **Chebyshev metric** (king-move distance): two squares are within radius R when `max(|file_a − file_b|, |rank_a − rank_b|) ≤ R`. Radius 1 covers the 8 neighbours; radius 2 covers a 5×5 box minus the source square. Auras recompute after every successful move. If a source piece moves OUT of range of a target, the contribution is retracted on the next compute. Multiple auras to the same `targetAttr` accumulate additively. Self-application is skipped — an aura's source piece never affects itself. > **Limitation (T3)**: `AuraContributions` are written correctly, but most > attribute consumers (HP/Range read points) don't yet layer the aura > contribution on top of the directly-seeded value. The data is there; > consumer wiring is a follow-up. --- ## Limits and DoS guards - **50 primitives total per descriptor** (counted recursively across nested trees). - **Recursion depth ≤ 3** — `on-turn-start` containing `on-capture` containing `conditional` is the maximum nesting depth allowed. - **20 descriptors per library** with starred-aware FIFO eviction. - **10 descriptors per multiplayer room**, server-enforced. - **Name 1-40 chars, description 0-200 chars** — descriptor metadata. The editor's footer shows live validation against all of these. A descriptor that fails any check is rejected on Save with the failing rule highlighted.