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.
12 KiB
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
- Open the Modifier Profile editor (
+ Modifier Profilefrom the rules drawer, or via the layout's modifier-profile picker). - In the editor's header, click + Custom Modifier. The Custom Modifier editor opens as a nested modal.
- 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.attris 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 amberuser-definedbadge.add-to-attribute.attr,multiply-attribute.attr,absorb-damage-with-attribute.attr, andadd-aura.targetAttrare 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 thatseed-attributeprimitives elsewhere in the current tree actually declare. Typed names that aren't built-in and aren't seeded carry a rednot seededwarning; names that are seeded carry an emeraldseededconfirmation.
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 aseed-attributeforShieldCharges=3produces 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 comparisonattr-eq— exact match (string / number / boolean / null)always— runsthennever— runselse(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) andconditionalcurrently 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:
seed-attribute:attr=ShieldCharges, value=3absorb-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:
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):
AuraContributionsare 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-startcontainingon-capturecontainingconditionalis 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.