Architecture / traits & effects
The trait & effect architecture
A gene changes a horse’s coat through the
three-phase pipeline. This page is the other half:
the Minecraft-specific things a gene can make a horse do
— walk on water, trail particles as it moves, be milked for a fluid. A
data-driven gene taps that through an effects block; the full
behaviour system it is a slice of is a design, recorded here so the slice has
somewhere to grow into.
Implemented: the effects block is parsed and
validated in common/ (GeneAbility,
AbilityType, GeneSpecParser,
HorseAbilities; unit-tested). The NeoForge translator
(server/GeneAbilityHandler, server/GeneYieldHandler)
executes traversal, emitter, mob_effect,
yield and glow (a dynamic minecraft:light
block plus a client-side full-bright coat layer).
Defined but not executed yet:
attribute (parsed and carried, logged once at runtime).
Owner-confirmed in-game (2026-09-02): Waterborn's coat + trail and
Suntouched's glow; the rest is unverified — see
To be verified. Everything below the
“full architecture” heading is unbuilt.
What a gene can do today: the effects block
A data-driven gene’s JSON may carry an effects array alongside
its layers: a closed set of eight verbs —
traversal (a movement / survival flag), attribute (a
condition-gated modifier), emitter (particles on a trigger),
mob_effect (an aura on self or rider), yield (the
horse hands something back on a right-click), glow (the horse
emits world light and/or renders some coat parts full-bright),
healing (an aura that mends what stands near it) and
spread (ground cover creeping out from the hooves). Every entry also
takes an optional when condition and a minDose
(1 = any expressing copy, 2 = homozygous only).
Since 2026-09-04 this is not a data-driven-only path. A
hand-written gene implements AbilityContribution and returns the same
records, and HorseAbilities.activeFor collects both kinds together.
That matters for this document’s argument rather than just for the
plumbing: the shipped slice is no longer “the thing gene files can do”
but “the thing any gene can do”, which is the shape the
architecture below always assumed. The last two verbs were both written for
built-in genes (healer,
verdant) and are usable from JSON today, which is
the property the shared vocabulary was chosen for.
Gene effects is the full
reference — every verb’s parameters, the triggers, the condition
flags, the execution and merge model, and the
modular contract for adding a new effect (one AbilityType
declaration; the parser never changes). The rest of this page is the wider
architecture that effects is a first slice of.
Waterborn — the worked example
common/src/main/resources/horsegenetics/example-genes/waterborn.json.
A magical gene whose one visible outcome takes either variant combination
(Wtb
n, 1 in 90 per allele) that does four things,
each a different part of the system:
| Behaviour | Expressed as |
|---|---|
| Neon-blue stripes down the mane and tail | an ordinary magical coat layer — a STRIPES mask on the HAIR parts, a TOWARD op at #1ec8ff |
| Walks on water (grown horses) | effects → traversal walk_on_water, when: { flag: adult } |
| Blue particles wherever it walks | effects → emitter, trail at the feet, on_move, minecraft:dust at #1ec8ff, chance 0.7 |
| Mares can be milked for water | effects → yield, on_interact minecraft:bucket → minecraft:water_bucket, when mare & tamed, cooldown 2400 |
{
"format": 2,
"key": "example.waterborn",
"name": "Waterborn",
"phase": "magical",
"priority": 215,
"alleles": [
{ "token": "Wtb", "label": "Waterborn (Wtb)" },
{ "token": "n", "label": "Wild-type (n)" }
],
"knobs": [
{ "name": "stripeSeed", "type": "seed" },
{ "name": "stripeSpacing", "min": 1.6, "max": 2.8 }
],
"expressions": [
{
"id": "waterborn",
"name": "Waterborn",
"description": "Neon-blue streaks in the mane and tail, blue dust off the hooves, a grown horse walks over water, and a tamed mare can be milked for a bucket of it.",
"when": [ "Wtb/Wtb", "Wtb/n" ],
"layers": [
{
"name": "neon blue stripes down the mane and tail",
"masks": [
{ "type": "STRIPES", "parts": [ "HAIR" ], "seed": "$stripeSeed",
"spacing": "$stripeSpacing", "duty": 0.4, "warp": 0.8 }
],
"op": { "type": "TOWARD", "color": "#1ec8ff", "strength": 92, "opacity": 100 }
}
],
"effects": [
{ "type": "traversal", "flag": "walk_on_water", "when": { "flag": "adult" } },
{
"type": "emitter", "kind": "particle", "shape": "trail", "anchor": "feet",
"trigger": { "on_move": {} },
"particle": "minecraft:dust", "color": "#1ec8ff", "chance": 0.7
},
{
"type": "yield",
"trigger": { "on_interact": "minecraft:bucket" },
"consumes": "minecraft:bucket", "produces": "minecraft:water_bucket",
"cooldown": 2400,
"when": { "all": [ { "flag": "sex_female" }, { "flag": "tamed" } ] }
}
]
},
{ "id": "wild", "name": "Wild type", "description": "No blue streaks and no affinity for water.", "wildType": true }
],
"founders": { "Wtb/Wtb": 0.0123457, "Wtb/n": 2.1975309, "n/n": 97.7901234 }
}
A copy of waterborn.json lives in the NeoForge module's resources
(horsegenetics/genes/index.json + the file), so
GeneSpecLoader.fromClasspath() registers it in the mod
constructor. It is the first gene file to ship — done
deliberately to make the effects work testable in-game — so the
in-game genotype code is now 20 segments and the horse-
dimension’s genotype space grows. :common:test is unaffected (that index
is not on the test classpath, so coat-golden.txt and the
17-built-in count still hold). The other example-genes/*.json are
still just examples — drop one in .minecraft/phc/genes/
to load it.
Where the work lives
The same split as the rest of the mod. common/ owns the
vocabulary and the parse — what a flag is called, which
ops exist, turning JSON into GeneAbility records, and deciding which
abilities a genotype expresses (HorseAbilities.activeFor). It never
imports Minecraft, so it ports to 1.12.2 unchanged. The NeoForge module owns the
execution — it reads that list every tick and evaluates
conditions, spawns particles, holds flags, swaps items. A version bump rewrites
the translator and nothing else.
The full architecture
Revision 2 of the trait-system design. None of this section is built
— it is the shape the effects block is a first slice of, kept so
later work has a plan to follow rather than reinvent.
Core model
Horse -> has a set of Traits
Trait -> a named bundle of Components
Component -> (Trigger, Condition, Selector, Effect)
Four axes describe any behaviour:
| Axis | Question | Example |
|---|---|---|
| Trigger | When does it fire? | when a rider mounts |
| Condition | Is it active right now? | daytime and sky-visible |
| Selector | Who or what does it act on? | undead within 8 blocks |
| Effect | What happens? | deal 2 damage, set flee flag |
Adding behaviour should mean writing a JSON file. Writing Java means you’ve hit a genuinely new primitive — which should be rare.
Components
1. Trigger
The firing vocabulary. Every component that isn’t continuous declares one.
- Time —
continuous·interval(ticks)·on_cooldown_expire(ability_id) - Lifecycle —
on_spawn·on_tame·on_death·on_load - Rider —
on_ride·on_dismount·on_rider_input(key) - Movement —
on_move·on_jump·on_land(fall_distance)·on_sprint_start·on_sprint_end·on_step(distance) - Interaction —
on_interact(item_or_tag)·on_sneak_interact(item_or_tag)·on_interact_fail·on_feed(item_or_tag) - Combat —
on_hurt(source_predicate)·on_hurt_rider·on_hurt_owner·on_damage_dealt·on_kill(selector) - World —
on_entity_spawn_nearby(selector, radius)·on_item_pickup·on_block_break·on_block_place - Edge detection —
on_change(condition_source)
That last one does a lot of work. Biome change, dimension change, weather change, light-level threshold crossing, mood transition, bond tier-up, day/night flip and pool-empty are all the same trigger: watch a condition source, fire on the edge. One implementation, one cached previous value per watched source, and every future condition source gets an edge trigger for free.
{ "trigger": { "on_change": "biome", "direction": "any" } }
{ "trigger": { "on_change": "is_raining", "direction": "rising" } }
Damage hooks are triggers, not a separate component.
on_hurt fires with the incoming damage in context, and the
damage-modifying effects (cancel_damage, reduce_damage,
reflect_damage) are ordinary effect verbs. There is no separate hook
system.
2. Condition
A composable predicate evaluated against a context snapshot. Every other component accepts an optional condition.
- World — time of day · moon phase · weather · sky access · light level · dimension · biome or biome tag · block underfoot · fluid underfoot · in spawn chunks
- Self — health fraction · pool threshold · has trait · has active ability · standing-still duration · velocity · is tamed · is in water/lava
- Rider and owner — rider present · rider’s held item · rider sneaking · rider sprinting · rider flying · owner online · owner sleeping · owner within range
- Entity state —
entity_state(selector, predicate)where predicate ishas_effect(id)·has_tag(tag)·is_glowing·is_invisible·is_on_fire·is_baby·health_below(pct)·count(op, n) - Combinators —
all·any·not·threshold(source, op, value)·curve(source, curve_id)
Conditions evaluate to a scalar 0.0–1.0, not a boolean.
Boolean contexts treat > 0.5 as true. This lets effects ramp
— a sun-powered horse gains speed gradually through the morning rather than
snapping at dawn. curve remaps a scalar through a named easing
function for non-linear response.
Integration conditions. Sources like claimed-chunk membership or region-protection state belong in optional compat submodules that register additional condition sources at load. A trait referencing an unregistered source is disabled with a load-time warning rather than crashing.
3. Selector
The targeting DSL. One implementation, used everywhere something needs to find entities or blocks.
- Entity selectors — type · entity tag (
undead,arthropod,passive,hostile,tamed,players,horses,items) · radius or box · line-of-sight requirement · state predicate · limit · sort - Relationships —
self·rider·owner·owner_target·owner_last_attacker·attacker·last_attacker·untamed·owner_other_horses - Block selectors — block or block tag · block state predicate · shape (sphere, cylinder, contact ring, cone) · radius · limit · sort
- Sort modes —
nearest·farthest·random·lowest_health·highest_health
last_attacker and owner_last_attacker are memory-backed
— store a UUID with a timeout, never a hard entity reference.
nearest plus a block tag covers finding the closest chest, bed,
campfire or ore without a dedicated selector for each.
4. Effect
The verb vocabulary. Keep this list closed. It’s the main lever on long-term maintainability — everything else composes these.
| Group | Verbs |
|---|---|
| Entity | apply_effect, remove_effect, transfer_effect, damage, heal, modify_attribute, set_ai_flag, teleport, impulse, spawn_entity, spawn_area_cloud, set_entity_tag |
Damage-context (only under on_hurt) | cancel_damage, reduce_damage, reflect_damage |
| World | transform_block, place_block, break_block, place_structure, set_weather, strike_lightning, set_time, set_moon_phase (the last four config-gated) |
| Output | give_item, consume_item, give_experience, play_sound, queue_sound_sequence, spawn_particles, emit_light, send_message (chat | action_bar | title | subtitle), open_screen |
| State & control | set_state, modify_pool, grant_trait, revoke_trait, modify_cooldown, store_waypoint, remove_waypoint, recall_waypoint, navigate_to, deny_spawn |
set_state deliberately absorbs a large family of one-off verbs.
Setting a follow target, a guard anchor, a patrol route, a model scale, an
emissive overlay, a particle colour or a visual flag are all writing a state
field that some other system reads. Reach for set_state before
adding a verb.
5. Aura
Aura { radius, interval, mode: entity|block,
selector, condition, effects[], max_targets }
Block-mode auras run a transform_block table across the selected
volume. Contact effects are a radius of ~1 with a grounded condition.
max_targets is not optional — an unbounded
aura in a mob farm is a server-killer.
6. Emitter
Emitter { kind: light | particle | sound | block_trail,
shape: point | ring | cone | sphere | trail | burst,
anchor: entity | feet | head | eyes | forward_offset | impact_point | target,
trigger, condition,
color_source: fixed | dye_slot | bound(state_field),
params }
shape and anchor do most of the work. A hoofprint is
particle + point + feet. A landing shockwave is
particle + ring + impact_point. A forward light beam is
particle + cone + forward_offset. These are parameters, not separate
emitter types. color_source binds visual output to state, so a
dye-driven glow and a mood-driven glow are one emitter with a different binding.
7. Yield
Yield { trigger, condition, cooldown,
consumes: item?,
output: items | fluid | effect | entity | block | structure | area_cloud,
roll_table: id? }
Interval production, container-based extraction and death drops are one system.
consumes handles the empty-container pattern; roll_table
supports weighted and conditional outputs.
8. Resource pools
Pool { id, min, max, initial,
regen: { rate, interval, condition },
decay: { rate, interval, condition },
on_empty: effects[], on_full: effects[],
persist: bool, visible: bool }
Stamina, hunger, thirst, body temperature, stored charge, distance travelled,
aggression, alertness, curiosity and loyalty are all pools — and so are
bond and mood intensity. Pools connect through three existing
seams: conditions read them via threshold / curve,
effects write them via modify_pool, abilities consume them via
cost: { pool, amount }. Pools that drive AI feed goal weights
through condition-scaled modifiers, so personality profiles and momentary
emotion use the same pathway.
9. Memory
Memory { home, owner_last_death, own_death_site,
visited[], notable_finds[], named[],
last_attacker_uuid, owner_last_target_uuid, patrol_route[] }
Written by store_waypoint and by relationship tracking; read by Locator, by the teleport / navigate_to destination resolvers, and by Goals. Entity references are UUIDs with a timeout, never hard references.
10. Locator
Locator { resolver: biome | structure | block | entity_selector | item | waypoint(name),
activation: interact(item) | passive | trigger,
output: particle_trail | chat | action_bar | sound | write_to_item | navigate }
Death sites, bed locations, world spawn and home are all waypoint(name). The difference between pointing toward something and travelling to it is the navigate output, not a second system.
11. Ability
Ability { trigger, condition, cooldown,
cost: { pool, amount }?,
requires_item: item_or_tag?, consumes_item: bool,
effects[] }
Cooldowns are keyed per ability ID and stored in horse state, not on the entity
instance. An ability whose cost cannot be paid does not fire and emits
on_interact_fail — that is how you build “the horse is
too tired” feedback without special-casing it.
12. Attributes and traversal flags
A flat map, merged across all traits.
Attributes — movement speed, jump strength, max health,
armor, knockback resistance, scale.
Traversal flags — walk_on_water,
walk_on_lava, fire_immune, fall_immune,
flight, underwater_breathing, swim_speed,
water_averse, phasing.
Both accept condition-gated modifiers, so a conditional buff is not a separate
concept from a permanent one. (This component is the one the
effects block implements first.)
13. Goal set
A library of parameterised AI goals. Traits contribute goals with weights; they do not contribute goal code.
| Goal | Parameters |
|---|---|
Follow | selector, min distance, max distance |
Watch | selector, range |
Stalk | selector, min/max distance, break_line_of_sight |
Patrol | route (waypoints from Memory), radius, speed |
StayAt | anchor (state field), leash length |
Flee | selector, range |
Attack | selector |
SeekItem / SeekEntity / SeekBlock | filter or selector or predicate, radius, stop distance |
Herd | selector, cohesion weight, separation weight |
Investigate | trigger source, radius, curiosity weight |
Idle | wander weight |
Personality is a named weight profile over this library plus
threshold adjustments — not separate behaviour code. A stationary guard is
StayAt(anchor_state) + Attack(hostile).
14. Interaction table
Right-click dispatch: held item or item tag → component ID. Fires on_interact_fail when nothing matches. Also holds the diet table:
item_tag -> { accepted, pool_deltas{},
effects_on_horse[], effects_on_rider[],
refusal_message }
15. Inventory
Inventory { slots, pickup: none|all|filter, filter[], display_slot }
display_slot renders a single item visibly on the horse. Item-seeking is a pickup filter paired with the SeekItem goal.
16. State
State { trait_ids[], granted_traits[] (with expiry),
pools{}, personality_id, cooldowns{}, memory,
visual{} (dye colors, scale, overlays),
navigation{} (follow target, guard anchor, patrol route),
discovered_traits[] }
Stored as a NeoForge Data Attachment. The read source for bound() emitter colours, pool-based conditions, goal navigation targets, and the render layer.
Trait relations
Trait { id, cost,
requires: [trait_id], // inert unless all present
conflicts: [trait_id], // mutually exclusive; higher priority wins
grants: [trait_id], // implies another trait
replaces: [trait_id], // supersedes a weaker version
priority: int,
...components }
Resolution order: expand grants transitively → apply
replaces → drop traits with unmet requires →
resolve conflicts by priority → enforce the trait point budget.
Cycles are a load-time error. replaces is what makes tiered traits
clean — a strong variant supersedes the weak one rather than stacking.
Trait scaling
Any numeric field in a component accepts a scaling block instead of a literal:
{ "value": { "base": 1.0, "scale_by": "bond", "curve": "ease_out", "max": 3.0 } }
scale_by names a condition source or a pool. Same scalar machinery as conditions, applied to magnitude rather than gating.
Trait discovery
Trait { discovery: { hidden_until: condition, hint: message_id?, reveal_on: trigger[] } }
A hidden trait is fully active — it just doesn’t appear in the stat display until revealed by witnessing the effect, reaching a bond threshold, using a specific item, or breeding a horse that expresses it. A horse whose full capability is legible from a book on first tame is a spreadsheet; one whose traits emerge through use is a character.
Visual legibility convention: every trait should declare at least one emitter, message or otherwise perceivable effect. A trait with no observable signature is a design error.
Variants and spawn rules
A variant is a spawn preset: a spawn condition plus a trait list. Variants contain no mechanics of their own.
{
"id": "nether",
"spawn": {
"dimension": "minecraft:the_nether",
"biomes": ["#minecraft:is_nether"],
"light_level": { "max": 7 },
"near_blocks": [{ "block": "#minecraft:base_stone_nether", "radius": 4 }],
"near_entities": [{ "selector": { "entity_tag": "hostile" }, "max": 2, "radius": 16 }],
"group_size": { "min": 1, "max": 3 },
"weight": 4
},
"traits": ["lava_walker", "fire_immune", "ember_hooves", "blaze_yield"]
}
Merge policy
A horse carries several traits at once, so resolution rules are defined up front.
| Component | Rule |
|---|---|
| Attributes | Additive modifiers, vanilla operation semantics; cap the total per attribute |
| Auras | All stack; deduplicate identical (selector, effect) pairs |
| Traversal flags | Boolean OR, except mutually exclusive pairs resolved by trait priority |
| Emitters | All run; light level takes the maximum, not the sum |
| Abilities | Cooldowns keyed per ability ID, never global |
| Goals | Priority = base priority + trait weight; document the numeric bands |
| Pools | A pool declared by multiple traits takes the widest bounds and the sum of regen/decay rates |
| Diet | A rejection anywhere overrides any acceptance |
| Yields | Independent cooldowns; no shared budget unless configured |
| Damage effects | cancel_damage wins over reductions; reductions apply multiplicatively; reflection uses post-reduction damage |
| Granted traits | Merge normally, but never re-trigger on_tame or other lifecycle triggers |
Trait budget. Each trait carries a cost. Spawn rules
and breeding enforce a cap set in config, not code. Temporarily granted traits
bypass the budget but carry an expiry.
Domain and integration split
Consistent with the pure-domain common/ module and thin NeoForge translator.
Pure Java, zero Minecraft imports: condition evaluation against a ContextSnapshot interface; selector definitions and predicate logic; trigger edge detection and previous-value bookkeeping; trait registry, JSON parsing, relation resolution, merge policy; resource-pool math; cooldown bookkeeping; personality weight profiles; yield roll tables and diet lookup; trait budget and discovery-state arithmetic.
NeoForge translator layer: populating ContextSnapshot from the level; executing selectors as entity and block queries; the effect verb executor; goal adapters onto vanilla Goal; trigger dispatch from game events; data attachments, networking, rendering, particles.
Build order
The spec’s order, annotated with where the gene layer sits. The shipped slice took a shortcut — it built parts of steps 2, 3 and 9 without steps 1 and 4 — which is why several later items are blocked on structural work rather than on their own complexity.
| # | Step | State |
|---|---|---|
| 1 | Trait registry, JSON loading, State attachment | Partial — gene JSON loads and validates; no trait IDs, minimal State |
| 2 | Effect executor — start with apply_effect, damage, heal, play_sound, spawn_particles, give_item, set_state, modify_pool | Partial — a different eight: particles, traversal, yield items; no damage, heal, sound, state or pools |
| 3 | Condition engine | Partial — boolean flags, translator-side |
| 4 | Trigger dispatch, including on_change | Partial — four triggers, no edge detection |
| 5 | Selector and Aura | Not started |
| 6 | Emitter, with shape and anchor from the start | Mostly — shape and anchor are in, narrower sets |
| 7 | Resource pools | Not started |
| 8 | Attributes and traversal flags | Traversal live; attributes parse only |
| 9 | Yield | Live, narrowed |
| 10 | Interaction and diet tables | Not started |
| 11 | Trait relations and budget (before the trait count passes ~20) | Blocked on step 1 |
| 12 | Goal library and personality profiles | Not started |
| 13 | Memory and Locator | Not started |
| 14 | Ability and cooldowns | Not started — yield holds the cooldown pattern |
| 15 | Inventory | Not started |
| 16 | Trait discovery | Not started |
| 17 | Variants and spawn rules — pure data by this point | Not started |
Retrofitting anchors onto emitters later would mean rewriting every emitter definition, which is why step 6 was done properly. The same argument applies to step 1 now: every trait-set feature is waiting on it.
Structured sound signalling
Pattern { steps: [ { pitch, volume, sound, delay_ticks } ] }
A SignalBinding maps a trigger to a pattern. The encoder is small.
The hard part is designing a pattern vocabulary players can learn under ambient
noise with no reference chart — keep the initial set to three or four
signals with obviously different rhythms, since rhythm survives a noisy
soundscape and pitch does not.
Deliberately excluded
Recorded so these don’t get re-proposed — each is already expressible.
| Proposed | Where it lives instead |
|---|---|
on_biome_change, on_dimension_change, on_weather_change | on_change(condition_source) |
is_crouching | Duplicate of rider_sneaking |
is_in_loaded_chunk | Not a condition. If the chunk isn’t ticking, nothing evaluates. |
has_ridden_distance | A pool plus threshold |
nearest_chest, nearest_bed | Block selector with sort: nearest |
items_in_radius | Entity selector with entity_tag: items |
nearest_player_death, spawn_location, bed_location | Memory waypoints via waypoint(name) |
clone_entity | spawn_entity with source-data copying |
set_light_level | emit_light |
send_action_bar, send_title | send_message with a channel |
set_follow_target, set_guard_position, set_patrol_points | set_state on navigation fields |
set_model_scale, set_emissive_texture, set_particle_color | set_state on visual fields, read by the renderer |
set_aura, toggle_ability | grant_trait / revoke_trait with duration |
light_pulse, shockwave, directional_beam, footstep_effect | Emitter shape + anchor combinations |
colored_aura | Emitter with color_source: bound(state) |
drop_sequence | A yield on an interval trigger |
Guard goal | StayAt + Attack composed |
Separate DamageHook component | Triggers plus damage-context effect verbs |
Invariants
- New behaviour is data. If a feature request requires Java, it’s a new primitive — an architectural decision, not a task.
- The effect vocabulary is closed. Growth happens through composition. Reach for
set_statebefore adding a verb. - Conditions gate everything. No component implements its own situational logic.
- One selector implementation. No component hand-rolls entity lookup.
- One trigger vocabulary. No component invents its own firing mechanism.
- Domain logic never imports Minecraft. The translator layer is a translator, nothing more.
- Every radius effect has a target cap. No exceptions.
- Every trait has a perceivable signature. A trait players can’t observe is a bug.
- Entity references are UUIDs with a timeout. Never a hard reference held across ticks.
Divergences from the spec
Places where the shipped gene layer and the architecture spec disagree. Each is either a deliberate narrowing, a rename that should be reconciled, or a genuine gap worth a decision. None of them should be quietly harmonised on one side without a note here.
| Divergence | Kind | Note |
|---|---|---|
The charges verb | Addition | The spec has no notion of one trait modifying another trait’s cooldown. It exists because two loci that must cooperate cannot see each other’s epigenome - a gene is handed its own values and nobody else’s. Naming a yield kind is the only version of “milked more than once a day” that works without breaking that. If the spec grows trait IDs and relations, this is the first thing that should move onto them. |
The breath verb | Addition | The spec has underwater_breathing as a boolean traversal flag and no graded form. Same argument as water_movement_efficiency one row below: it is a magnitude, and a magnitude is not a flag. |
The on_death and item_drop verbs | Addition | The spec models death effects under spawn_entity / impulse and drops under an unbuilt roll-table system. Both are far larger than what two genes needed, so these are deliberately narrow: three world effects and five drops, all closed sets. If the roll tables are ever built, item_drop becomes a preset over them. |
The mob_aura verb | Addition | The spec would express this as set_ai_flag plus a selector, and cannot yet, because selectors are unbuilt. When they land this should be re-read as a selector plus two AI flags rather than kept as its own verb. |
The combat verb | Addition | Arguably should be an attribute on attack_damage. It is not, because the gene’s number is absolute: a modifier would make a horse’s sheet read as an adjustment to a baseline the reader has to go and look up, and the baseline is itself a gene constant. |
yield has a kind | Addition | Purely so charges can name it. The spec has no equivalent; it would use a trait ID. |
minDose exists on every effect | Addition | Genetics-specific. The spec’s trait model has no dose concept, and nothing in it needs one. |
The glow verb | Addition | The spec models a light as emitter kind:light and has no emissive-render concept at all. glow folds “emit world light” and “render these coat parts full-bright” into one verb because they are the two halves of one visible trait. If the spec’s emitter ever grows a real light kind, the light half should move there and glow keep only the emissive parts. |
glow has a client-render half | Addition | Every other verb is translator-only. glow.parts is drawn by client/EmissiveCoatLayer, so “add a verb” is no longer always just a server case. |
sex_female / sex_male / adult / baby flags | Addition | Genetics and vanilla age; is_baby exists in the spec only as an entity_state predicate on a selected entity, not as a self condition. |
| Conditions are boolean, not 0–1 scalars | Narrowing | Known. Blocks ramping effects and curve. See the scalar model. |
Conditions evaluate in the translator, not against ContextSnapshot | Gap | Breaks the domain/translator split for condition logic. See Domain and integration split. |
anchor: body vs the spec’s entity | Rename | Same thing, two names. Pick one before external gene packs exist. |
Emitter shapes lack cone and sphere | Narrowing | No blocker; both are pure geometry. |
water_movement_efficiency is an attribute here, a traversal flag in the spec | Rename | The gene layer is arguably right — it is a magnitude, and traversal flags are booleans. Fix the spec, not the code. |
flight and phasing traversal flags absent | Gap | Both are large behavioural changes; absence is probably correct for now. |
on_interact takes an item ID, not an item tag | Narrowing | The spec says item_or_tag everywhere an item appears. Tag support is a translator-side lookup. |
yield.produces accepts a recognised item set only | Narrowing | The spec allows fluid, effect, entity, block, structure and area-cloud outputs, plus roll tables. |
| No trait IDs, cost, relations or budget | Gap | The structural prerequisite for most of the unbuilt half of this page. |
| No per-attribute cap | Gap | The spec requires one. attribute is now wired and the cap is still absent, which is exactly the order this row warned against. |
walk_on_water + water_averse unresolved | Gap | The spec resolves mutually exclusive flags by trait priority; there is no priority here. |
AbilityType.CONDITION_FLAGS is a closed constant | Gap | Blocks the spec’s load-time registration of integration condition sources. |
common/genetics/spec/GeneAbility.java,
common/genetics/spec/AbilityType.java,
common/genetics/spec/HorseAbilities.java,
neoforge-26.1.2/…/server/GeneAbilityHandler.java,
…/server/GeneYieldHandler.java