Skip to content
Felucca Online

Docs

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.

StreamBranchWhatNeedsWrites (owns)
F foundationfeat/builds-foundationcodegen, 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 effectsart/builds-vfxpixel sheets (#152): authors projectiles, hits, zones and states on V2's 3D engineV2's engineeffect descriptors
V2 effectsart/builds-vfx-2the 3D effects engine and prototype (#152), then reactions, escapes, triggers, impacts, auras, formations–crates/uo-client/src/vfx3d/, docs/vfx.md
I1 iconsart/builds-iconsweapon ability icons (155)–tools/pixel/icons/abilities/, assets/ui/icons/abilities/
I2 iconsart/builds-icons-2armour abilities, states, buckets, clothing, jewels, keystones, runes, off-hands, belts, consumablesI1's formattools/pixel/icons/items/, assets/ui/icons/ (other groups)
M1 armourart/builds-armour6 classes x 6 pieces on the base body–tools/blender/models/equipment/armour_*.py, assets/models/equipment/<class>_<piece>.glb, coverage.json rows
M2 clothingart/builds-clothingclothing 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 weaponsart/builds-weaponsprops for every new weapon, staff and off-hand–tools/blender/models/props/, assets/models/props/<key>.glb
AN animationsart/builds-animsthe 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 familiesfeat/builds-weaponsthe 17 families' Q/W/E as data + effects, staves replace the spellbook, Tome, mob loadoutsFuo-combat/src/builds/families.rs, tests_families.rs
A armour and escapesfeat/builds-armourarmour classes, set bonuses, weight, D/R/F pools, the 12 escapes and their counters, off-hands, belts, consumablesFbuilds/armour.rs, builds/escapes.rs, builds/offhands.rs, tests
S statesfeat/builds-statesthe 20 states, 10 reactions, terrain (water, oil, wood)Fbuilds/states.rs, builds/reactions.rs, tests
J triggersfeat/builds-triggersclothing, jewels, keystones, runesF, Sbuilds/triggers.rs, builds/keystones.rs, builds/runes.rs, tests
C craftingfeat/builds-craftingtiers, materials, affixes, recipes, designs, the economy's new goodsFuo-proto/src/builds/craft.rs, region/progress.rs crafting parts, tests
U clientfeat/builds-clientloadout window, six-key HUD with cooldowns, counters, state icons, tooltips, VFX and clip hookup, online mirrorF, V1, I1, ANuo-client/src/player_ui/loadout.rs, HUD parts, combat/builds_fx.rs
G groupsfeat/builds-groupscloak auras, surcoats and formationsF, Sbuilds/auras.rs, builds/formations.rs, tests
E PvEfeat/builds-pveelites, loot of materials/components/designs, danger tuned to tiersF, Cuo-world creature tables, builds/elites.rs
N networkfeat/builds-netSDK, MCP tools (loadout, ability, build_code), bots learn loadouts, protocol docFuo-sdk, uo-mcp, docs/protocol.md
B balancefeat/builds-balanceuo-loadtest builds against the Power sheetW, A, Juo-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, then CARGO_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 a CARGO_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 your target/ 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 (and python3 docs/builds/generate.py run) 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: dev for code and type: art for 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-written ITEMS.
  • 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 old leather_*, 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, pierce as rune_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 in coverage.json (coverage.py). Tier hue is applied by the client: metal and leather parts carry the tint marker of coverage.json's tints so the client recolours them by material (iron grey … valorite blue, catalog.MATERIALS hue).
  • Clothing: a few shapes per slot (clothing_<shape>.glb) and assets/models/equipment/builds_map.json mapping every clothing catalogue key to {model, dye}; surcoats show heraldry as a dye pair.
  • Props: assets/models/props/<item key>.glb in grip space (#140: origin at the fist, +Y to the tip), so combat/props.rs::prop_model finds 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 into assets/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.glb under Ab_<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.json maps 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.rs and items.rs are generated by python3 docs/builds/codegen.py from catalog.py (never edit them). Both carry a source hash; cargo test -p uo-proto builds fails when they are stale (UO_BLESS=1 reruns the generator). Build item ids live in docs/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, Tags bit order), FAMILIES (with their FamilyItems), ABILITIES (155 weapon abilities, then the 32 armour abilities from ARMOR_ABILITIES_START), PASSIVES, MASTERIES, ARMOR_CLASSES, ARMOR_STATS, ARMOR_SETS, AFFIXES, AFFIX_DESIGNS, CLOTHING, JEWELS (both TriggerDef with a When), KEYSTONES, RUNES, OFFHANDS, AURAS, DRILLS, BELTS, CONSUMABLES, StateId + STATES, ReactionId + REACTIONS.
  • items::BUILD_ITEMS (sorted by id) and items::ids::*: new weapons and staves (class WeaponClass::Staff, skill Magery), the 36 armour pieces <class>_<piece> (no ArmorDef: 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 into ABILITIES, 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); helpers tier, 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.BUILDS resolved 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 the material'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 (on Profile::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 by Kit::from_character (from Profile::from_character), Kit::for_weapon(item_key, level, loadout), Kit::for_mob(&[ability keys]).
  • Live (on Fighter::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 (from Order::Ability(slot, target)), start, launch (projectiles), land (shapes: self, ally, wall, zone, jump, areas, single), tiles(shape, me, at), strike_fighter in battle.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, then exec::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)) or armour::effects (D, R), then runes::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 in Kit::from_character; the hits bucket goes into Profile::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 after FIGHT_GAP (10 s) without a counted event. The ±10 % damage roll and the AI's reaction delay are the only rng calls left in battle.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, emits Event::Triggered, applies effect(def, tier) through exec::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), queries stunned, rooted, silenced, hidden, step_extra, taken_pct, view.

Hooks (builds/hooks.rs, called from battle.rs; streams fill the modules behind them):

hookcalledfills it
on_use(b, i, slot, ability)exec::start, after cost and cooldownJ (triggers::effect), A (escapes via on_escape for F)
on_hit(b, by, j, HitInfo)after any blow, shot or ability damage landsleech (F); J
on_crit / on_struck / on_killfrom on_hit / damage_flags (kill)J
on_escape(b, i, ability)an F escape was usedA (escapes::after), J
on_state_applied(b, by, j, state)exec::apply_stateS, J
tick_states(b)every step (Battle::step)S (states.rs)
tick_auras(b)once a secondG (auras::tick, formations::tick); zones
reactions::on_hit(b, by, j, element, dmg)in strike_fighter and proc_hit, before defencesS

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 in items.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_tier reads ruby as T3.
  • Sources: economy::work has a node per metal, wood and gem (vein, wood, gem; Mining and Lumberjacking 20.0 per tier); node_of maps every ore_vein_<metal>, tree_ash, pine_snowy_small/snow (frostwood), ancient_tree (heartwood), corrupted_tree (bloodwood), crystal clusters (quartz) and gems_*. 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/ettin horned_hide, lizard scales → barbed, drake/dragon scales → dragonscale, ectoplasm and wisp essence → ghostweave/starweave, dragons a star sapphire.
  • Recipes: builds::craft::recipes() makes one RecipeDef per build gear item (id RECIPE_ID_BASE 10 000 + item id, key = item key, T1 inputs listed); items::recipes(), recipe, recipe_by_key and craft::recipe_for see both tables (items::RECIPES keeps 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) from data::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 their props.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_designs migrates old sheets, character records v2); Character::designs (additive) holds the learnt ones; ids::DESIGN_SCROLL with props.design teaches one (Command::Use); design_drop(Loot, roll) is what bodies hold (server: progress.rs::design_loot, called from death.rs); recipe rune_research gives 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::CraftWith gains optional design and affix (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_speed shortens spell abilities' wind-up and recovery (exec::cast_pct) and gives the old spells Faster Casting (a quantum per 15 %).