Builds: implementation plan and contracts
How the build system of docs/builds.md (#147–#149) is built by parallel sessions (#150). This page
is the contract between them: who owns which files, the names everyone uses, the order of work and
the rules for merging. The coordinating session (on feat/melee) keeps it current.
1. Streams
Each stream is one session in its own worktree (.claude/worktrees/<name>, #122), on its own branch
cut from feat/melee, merged back into feat/melee by the coordinator.
| Stream | Branch | What | Needs | Writes (owns) |
|---|---|---|---|---|
| F foundation | feat/builds-foundation | codegen, uo_proto::builds types, item table, the ability engine core, buckets, counters (no dice), trigger bus, state registry, hooks in battle.rs, protocol, server loadout from the sheet, #145 techniques ported | – | docs/builds/codegen.py, crates/uo-proto/src/builds/, crates/uo-combat/src/builds/ (skeleton of every module), the hook calls in battle.rs, msg.rs additions, uo-server sim glue |
| V1 effects | art/builds-vfx | V2's engine | effect descriptors | |
| V2 effects | art/builds-vfx-2 | the 3D effects engine and prototype (#152), then reactions, escapes, triggers, impacts, auras, formations | – | crates/uo-client/src/vfx3d/, docs/vfx.md |
| I1 icons | art/builds-icons | weapon ability icons (155) | – | tools/pixel/icons/abilities/, assets/ui/icons/abilities/ |
| I2 icons | art/builds-icons-2 | armour abilities, states, buckets, clothing, jewels, keystones, runes, off-hands, belts, consumables | I1's format | tools/pixel/icons/items/, assets/ui/icons/ (other groups) |
| M1 armour | art/builds-armour | 6 classes x 6 pieces on the base body | – | tools/blender/models/equipment/armour_*.py, assets/models/equipment/<class>_<piece>.glb, coverage.json rows |
| M2 clothing | art/builds-clothing | clothing shapes and dyes for every clothing piece, surcoats, cloaks, belts | – | tools/blender/models/equipment/clothing_*.py, assets/models/equipment/clothing_*.glb, assets/models/equipment/builds_map.json |
| M3 weapons | art/builds-weapons | props for every new weapon, staff and off-hand | – | tools/blender/models/props/, assets/models/props/<key>.glb |
| AN animations | art/builds-anims | the clips abilities and escapes need, on the base body | – | assets/models/characters/base_body.glb (clips only), assets/models/characters/ability_clips.json, the retarget/author scripts |
| W weapon families | feat/builds-weapons | the 17 families' Q/W/E as data + effects, staves replace the spellbook, Tome, mob loadouts | F | uo-combat/src/builds/families.rs, tests_families.rs |
| A armour and escapes | feat/builds-armour | armour classes, set bonuses, weight, D/R/F pools, the 12 escapes and their counters, off-hands, belts, consumables | F | builds/armour.rs, builds/escapes.rs, builds/offhands.rs, tests |
| S states | feat/builds-states | the 20 states, 10 reactions, terrain (water, oil, wood) | F | builds/states.rs, builds/reactions.rs, tests |
| J triggers | feat/builds-triggers | clothing, jewels, keystones, runes | F, S | builds/triggers.rs, builds/keystones.rs, builds/runes.rs, tests |
| C crafting | feat/builds-crafting | tiers, materials, affixes, recipes, designs, the economy's new goods | F | uo-proto/src/builds/craft.rs, region/progress.rs crafting parts, tests |
| U client | feat/builds-client | loadout window, six-key HUD with cooldowns, counters, state icons, tooltips, VFX and clip hookup, online mirror | F, V1, I1, AN | uo-client/src/player_ui/loadout.rs, HUD parts, combat/builds_fx.rs |
| G groups | feat/builds-groups | cloak auras, surcoats and formations | F, S | builds/auras.rs, builds/formations.rs, tests |
| E PvE | feat/builds-pve | elites, loot of materials/components/designs, danger tuned to tiers | F, C | uo-world creature tables, builds/elites.rs |
| N network | feat/builds-net | SDK, MCP tools (loadout, ability, build_code), bots learn loadouts, protocol doc | F | uo-sdk, uo-mcp, docs/protocol.md |
| B balance | feat/builds-balance | uo-loadtest builds against the Power sheet | W, A, J | uo-loadtest |
Order: F and the eight art streams run now. After F merges, code streams run 2–3 at a time (user directive, #150): W + A + S, then J + C + U, then G + E + N, then B.
2. Rules for every stream
- Start from a clone of feat/melee's build (#162):
cp -Rpc /Users/aler/projects/daily/day-1014-uo/.claude/worktrees/melee/target ./target, thenCARGO_INCREMENTAL=0 cargo clean --release -p uo-proto -p uo-combat -p uo-world -p uo-buildings -p uo-atlas -p uo-server -p uo-sdk -p uo-loadtest -p uo-mcp -p uo-client -p uo-baker. Never share aCARGO_TARGET_DIR. - Release builds only, no incremental (user directive, #150: they are smaller on disk):
CARGO_INCREMENTAL=0 cargo build --release …,CARGO_INCREMENTAL=0 cargo test --release -p <crate>. Never a debug build. Run only the crates you touched, the whole workspace once before handing over. The disk is nearly full: delete yourtarget/when the stream is merged. - Stay in your files. A change outside them is a minimal hook (one call, one field), named in your report. Never reformat or reorder code you don't own.
- Keys are the catalogue's (
docs/builds/catalog.py) and append-only. A key the catalogue lacks is added there (andpython3 docs/builds/generate.pyrun) in the same commit. - Decisions: don't edit the decision table in
CLAUDE.md. List the decisions you made (and the user directives you received) in your report; the coordinator records them when merging. - Changelog: one fragment per stream,
changes/2026-10-26-builds-<stream>.md,type: devfor code andtype: artfor assets (the feature post comes with the final merge). - One commit per logical change, messages ending with the co-author line of the conversation.
- Art is checked in the game (the client's galleries and
--screenshot), not only in its tool. - Don't merge, push or deploy; the coordinator merges.
3. Contracts
3.1 Item keys and ids (F)
- Build items get ids 1000..=2999 in a generated table
crates/uo-proto/src/builds/items.rs(from the catalogue), looked up together with the hand-writtenITEMS. - Weapons: the catalogue's item keys (
claymore,fire_staff, …); the existing weapons keep their keys and ids. - Armour pieces:
<class>_<piece>, class ∈cloth padded leather hardened chain plate, piece ∈head body feet hands arms legs(36 keys, e.g.plate_head). The tier is per instance (ItemProps.material); the Hands/Arms/Legs affix design is per instance too (ItemProps.design). The Head/Body/Feet D, R, F are the character's loadout picks (Loadout::{r, d, f}, user directive 2026-10-27), not a property of the piece. The oldleather_*,studded_*,ringmail_*,chainmail_*,plate_*items are replaced (no backward compatibility, user directive on #145). - Clothing, jewels, keystones, runes, off-hands, belts, consumables: the catalogue keys
(
skirt_of_flame,storm_ring,glass_heart,pierceasrune_pierce,torch,potion_belt…).
3.2 Models (M1, M2, M3)
- Armour:
assets/models/equipment/<class>_<piece>.glb, skinned to the base body's skeleton, the vertex-PBR contract of #140/#142, a row per key incoverage.json(coverage.py). Tier hue is applied by the client: metal and leather parts carry the tint marker ofcoverage.json'stintsso the client recolours them by material (iron grey … valorite blue,catalog.MATERIALShue). - Clothing: a few shapes per slot (
clothing_<shape>.glb) andassets/models/equipment/builds_map.jsonmapping every clothing catalogue key to{model, dye}; surcoats show heraldry as a dye pair. - Props:
assets/models/props/<item key>.glbin grip space (#140: origin at the fist, +Y to the tip), socombat/props.rs::prop_modelfinds them by key. - Nobody but AN writes
base_body.glb.
3.3 Effects (V2 engine, then V1 and V2)
Superseded by #152 (user directive): effects are 3D, bold and toon-shaded, not pixel sheets.
V2 builds the engine in crates/uo-client/src/vfx3d/ (procedural meshes, a solid toon material
with the outline tag and an alpha-blended glow material without dithering, a short point light per
core, a descriptor per catalogue key, a gallery and a recording mode with real characters) and a
four-effect prototype; after the user approves it, V1 and V2 author the rest by the old split (V1:
projectiles, hits, zones, states; V2: reactions, escapes, triggers, impacts and arcs, auras,
formations). The API for stream U is documented in docs/vfx.md. The pixel sheets stay unmerged.
3.4 Icons (I1, I2)
- 24x24 pixel icons (drawn x2 in the HUD),
assets/ui/icons/<group>/<key>.png, packed by the stream intoassets/ui/icons/<group>.png+.json(key → rect). Groups:abilities(weapon and armour),states,items,buckets. One frame style per group so icons read as a set; element colours from § 3.3's palette.
3.5 Clips (AN)
- New clips are baked into
base_body.glbunderAb_<Name>names (e.g.Ab_Spin,Ab_Lunge,Ab_Leap_Slam,Ab_Dash,Ab_Roll,Ab_Blink,Ab_Channel_Loop,Ab_Volley,Ab_Throw,Ab_Kick,Ab_Shield_Bash,Ab_War_Cry,Ab_Overhead,Ab_Sweep,Ab_Execute, …). assets/models/characters/ability_clips.jsonmaps every ability and escape key of the catalogue to{clip, rate, impact_frame}; keys without their own clip map to an existing one.
3.6 Engine (F, for W A S J G C E) — as built
Data (crates/uo-proto/src/builds/):
data.rsanditems.rsare generated bypython3 docs/builds/codegen.pyfromcatalog.py(never edit them). Both carry a source hash;cargo test -p uo-proto buildsfails when they are stale (UO_BLESS=1reruns the generator). Build item ids live indocs/builds/item_ids.json(a key keeps its id forever; new keys take the next id from 1000).- Tables in
data:TIERS,MATERIALS,SPREAD_PCT,Tag(enum,Tagsbit order),FAMILIES(with theirFamilyItems),ABILITIES(155 weapon abilities, then the 32 armour abilities fromARMOR_ABILITIES_START),PASSIVES,MASTERIES,ARMOR_CLASSES,ARMOR_STATS,ARMOR_SETS,AFFIXES,AFFIX_DESIGNS,CLOTHING,JEWELS(bothTriggerDefwith aWhen),KEYSTONES,RUNES,OFFHANDS,AURAS,DRILLS,BELTS,CONSUMABLES,StateId+STATES,ReactionId+REACTIONS. items::BUILD_ITEMS(sorted by id) anditems::ids::*: new weapons and staves (classWeaponClass::Staff, skill Magery), the 36 armour pieces<class>_<piece>(noArmorDef: their numbers are the class's, summed into buckets), off-hands, clothing, cloaks (aura keys), surcoats (surcoat_<drill key>), belts, jewels, keystones, runes (rune_<key>), consumables. Existing catalogue items keep their keys and ids.uo_proto::items::{item, item_by_key, all}find both.- Types in
builds/mod.rs:Tags(+Tags::of,has,any,element),Slot(Q W E R D F,index,piece),School,Resource,Element,Shape,AbilityDef,AbilityId(index intoABILITIES, serialised as its key),FamilyDef,ArmorClass,ArmorPiece,ArmorStat,When,TriggerDef,KeystoneDef,RuneDef,Bucket,Cond,BuffStat,Effect(the interpreter's vocabulary),Loadout(+Pick,set,pick,check); helperstier,mult,item_mult_pct,material_tier,tier_of(props),ability,family,weapon_family,affix,affix_design,trigger,keystone,rune,state,armor_piece,armour_pool(the D, R or F pool),example_build(data::EXAMPLE_BUILDS:catalog.BUILDSresolved to keys by codegen). - Per instance (
ItemProps, additive):design(Hands/Arms/Legs: an affix design or affix key),affixes(the exceptional second affix),runes(socket order); the tier is thematerial's (tier_of).Character::loadout(additive) holds Q/W picks per family, the R/D/F picks (Loadout::armour(slot): the pick, else the pool's first) and passives.
Engine (crates/uo-combat/src/builds/):
Kit(onProfile::kit, fixed for the battle):abilities: [Option<AbilityId>; 6](R, D, F: the loadout's picks while a Body, Head, Feet piece is worn),family,school,level,weapon_tier,empower,buckets,armour(pieces per class),triggers,keystone,runes,offhand,aura,drill,belt,passives. Built byKit::from_character(fromProfile::from_character),Kit::for_weapon(item_key, level, loadout),Kit::for_mob(&[ability keys]).Live(onFighter::live, changes in a fight):cooldowns(ready time per slot),counters,states,trig(per-trigger count and ICD),firing(re-entry guard),shield,buffs,untargetable_until,last_qw,uses.exec— the one pipeline:refusal,in_reach,reach,cost,cooldown_ms,needs_enemy,act(fromOrder::Ability(slot, target)),start,launch(projectiles),land(shapes: self, ally, wall, zone, jump, areas, single),tiles(shape, me, at),strike_fighterinbattle.rs(per target: evade, perfect guard, AI parry counter, miss counter for auto-attacks, block counter, damage through buckets, crit counter, reactions, held guard, hooks, momentum, thenexec::after_hit),after_hit/on_self/on_ally/proc_hit(the generic interpreter),apply_state,cleanse,Zone+tick_zones,roll(the ±10 % roll),weapon_hit,spell_hit,ability_damage. Effects of an ability:exec::effects(b, i, slot, id)=families::effects(weapon: defaults from coef +families::extra(key)) orarmour::effects(D, R), thenrunes::modify.buckets::Buckets(‰ for percents, points for armour/ward/guard/toughness):add_piece,add_affix,add_affix_key,deal(dmg, tags),take(dmg, element),crit_pct,max_hits,step(thresholds, tier). Summed once inKit::from_character; the hits bucket goes intoProfile::max_hits, attack speed into #93's SSI.counters::{Counters, Chance}:roll(chance, permille, now)/pct; chances Miss, Crit, Block, Cover, AiParry, CastFail, Fizzle; reset afterFIGHT_GAP(10 s) without a counted event. The ±10 % damage roll and the AI's reaction delay are the onlyrngcalls left inbattle.rs(a test checks it).triggers:Ev(Use, Hit, Crit, Struck, Kill, Escape, StateOnMe),listens(when, ev),fire(b, i, ev)(every Nth, ICD, no re-entry, emitsEvent::Triggered, applieseffect(def, tier)throughexec::proc_hit).states::States:apply(stacks to the catalogue max, refresh, control immunity 3 s after a control ends),remove,cleanse,expire,ticks(damage states once a second), queriesstunned,rooted,silenced,hidden,step_extra,taken_pct,view.
Hooks (builds/hooks.rs, called from battle.rs; streams fill the modules behind them):
| hook | called | fills it |
|---|---|---|
on_use(b, i, slot, ability) | exec::start, after cost and cooldown | J (triggers::effect), A (escapes via on_escape for F) |
on_hit(b, by, j, HitInfo) | after any blow, shot or ability damage lands | leech (F); J |
on_crit / on_struck / on_kill | from on_hit / damage_flags (kill) | J |
on_escape(b, i, ability) | an F escape was used | A (escapes::after), J |
on_state_applied(b, by, j, state) | exec::apply_state | S, J |
tick_states(b) | every step (Battle::step) | S (states.rs) |
tick_auras(b) | once a second | G (auras::tick, formations::tick); zones |
reactions::on_hit(b, by, j, element, dmg) | in strike_fighter and proc_hit, before defences | S |
Where each stream plugs in (stub modules, each with a doc comment naming its owner):
W families.rs (extra(key) per ability, mob_abilities), A armour.rs (set bonuses via
sets(kit), effects(key) for D/R), escapes.rs (refusal, cooldown_ms, after),
offhands.rs (apply(kit)), S states.rs rules + reactions.rs, J triggers::effect,
keystones.rs (apply(kit)), runes.rs (modify), G auras.rs, formations.rs, E
elites.rs, C buckets numbers and uo-proto/src/builds/craft.rs (not created yet). A new
Effect the interpreter doesn't know is a no-op until its stream adds it to exec.
Protocol (appended): Command::Ability { slot, target: Option<TargetRef> },
Command::SetLoadout { loadout } (out of a fight); Command::Technique is gone.
CombatEvent::AbilityUsed (replaces Telegraph), StateApplied, StateEnded, Reaction,
Triggered, Escaped, FormationOn, FormationOff; CauseRef::{Ability, State};
EntityState::{states, counters, cooldowns} (cooldowns as absolute server game ms per key,
0 = ready).
Client minimum (stream U does the real UI): Q W E R D F send Ability (Albion preset);
potion moved to Z, hold to X, the held guard to G, WASD running removed (W and D are ability keys);
the technique bar shows the six keys with cost and cooldown.
The 36 armour models of M1 are in (#155); the stand-ins are gone.
- The ±10 % damage roll is the only RNG left in combat resolution; AI reaction delays may stay random (they aren't a player's dice).
3.7 Crafting (C) — as built
- Materials (
uo_proto::craft::MATERIALS,MatClass=builds::Discipline): one stackable item per catalogue material (ingots, boards, leathers, cloths, gems; new ids 850..=881 initems.rs, with their ores, logs and hides), each opening its tier and nothing else (#148: the #125 bonuses are gone).craft::gem_of(tier),craft::substitute(ruby ↔ sapphire),craft::tier_of_material.builds::material_tierreadsrubyas T3. - Sources:
economy::workhas a node per metal, wood and gem (vein,wood,gem; Mining and Lumberjacking 20.0 per tier);node_ofmaps everyore_vein_<metal>,tree_ash,pine_snowy_small/snow (frostwood),ancient_tree(heartwood),corrupted_tree(bloodwood), crystal clusters (quartz) andgems_*. The server refines a node by its place (work.rs::node_here): trees inside an atlas wood deposit (World::deposits_at) give its wood, gem veins the gem of the danger (work::gem_node). Creatures: fur → spined, troll/ogre/ettinhorned_hide, lizard scales → barbed, drake/dragon scales → dragonscale, ectoplasm and wisp essence → ghostweave/starweave, dragons a star sapphire. - Recipes:
builds::craft::recipes()makes oneRecipeDefper build gear item (idRECIPE_ID_BASE10 000 + item id, key = item key, T1 inputs listed);items::recipes(),recipe,recipe_by_keyandcraft::recipe_forsee both tables (items::RECIPESkeeps the hand-written ones; the old catalogue weapon, shield and armour recipes are gone). Inputs per tier:craft::inputs_for(recipe, &Choice { material, design, affix })→builds::craft::inputs(key, material, design)fromdata::RECIPE_KINDS(codegen now emits it). Consumables keep hand-written recipes; tiered ones (builds::craft::tiered_consumable) take a gem of the tier from T2 (RecipeDef::gem()), which becomes theirprops.material. - Designs:
builds::craft::{design, starter, designs, learnable, knows, needs, check_design, check_affix, group}(Head, Body and Feet take no design and no gem since 2026-10-27; their D, R, F are loadout picks;drop_ability_designsmigrates old sheets, character records v2);Character::designs(additive) holds the learnt ones;ids::DESIGN_SCROLLwithprops.designteaches one (Command::Use);design_drop(Loot, roll)is what bodies hold (server:progress.rs::design_loot, called fromdeath.rs); reciperune_researchgives a rune design (research). - A fresh piece:
craft::finish_with(def, &choice, exceptional, crafter): material (tier), design, exceptional = mark, +20 % durability, +20 % weapon damage (staves too), the chosen second affix on Hands/Arms/Legs.builds::craft::sockets(def, props)= tier sockets + 1 if exceptional, at most 3 (for J's socketing). Build armour wears by class (max_durability) and takes wear when struck (progress.rs::armor_struck). - Protocol:
Command::CraftWithgains optionaldesignandaffix(serde default). - Made to order (the GM's test kit,
uo-server/src/gm/kit.rs):builds::craft::make(key, tier, &Order)makes any build item at any tier with its design, second affix, runes and quality as if crafted;dress(c, example_build, tier)dresses a character in a catalogue example build with its loadout. GM lines:.give NAME ITEM [N] [@MATERIAL|@TIER] [design=] [affix=] [runes=a,b] [exceptional],.design NAME KEY|all,.build NAME BUILD [TIER],.loadout NAME q= w= r= d= f= [passive=]. - Combat hook:
kit.buckets.cast_speedshortens spell abilities' wind-up and recovery (exec::cast_pct) and gives the old spells Faster Casting (a quantum per 15 %).