Attr-string fields in the Custom Modifier Editor now differentiate between declare-sites (seed-attribute.attr) and consume-sites (add-to-attribute.attr, multiply-attribute.attr, add-aura.targetAttr, absorb-damage-with-attribute.attr): - Consume-sites hide the illustrative 'User-defined examples' group (ArmorPlates/BloodStacks/ManaPool) because those names are fiction unless something seeds them. - Consume-sites surface a new 'Seeded in this descriptor' group populated from seed-attribute primitives elsewhere in the current tree (including inside trigger children via childPrimitives). - Badge states: emerald 'seeded' when the typed value matches an in-tree seed; red 'not seeded' when it's a non-schema name with no backing seed; amber 'user-defined' only in declare-mode for off-catalog names. Declare-mode behaviour is unchanged — inventing ShieldCharges there still works without warnings.
334 lines
12 KiB
Markdown
334 lines
12 KiB
Markdown
# 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.
|