Repository navigation
random item properties — vanilla's weighted per-instance stat suffixes, end-to-end #31
Description
Vanilla's "of the Bear" system IS weighted per-instance randomized stats, unimported. All wire
plumbing verified present: gtker exposes set_item_random_properties_id (+ property_seed +
enchantment) in the vanilla update mask, the item-query response carries random_property
(the client's <Random enchantment> preview line), and our loot body already has the
random_property_id u32 hardcoded 0 (codec/loot.rs:37).
Build: (1) IMPORT — ItemRandomProperties.dbc + SpellItemEnchantment.dbc → server mirror
tables (game_item_random_property / game_item_enchantment — id, suffix name, up to 3
enchant refs → stat kind/amount rows), and the dump's item_enchantment_template →
game_item_property_weight (the WEIGHTED roll table keyed by the template's RandomProperty
group); item_template.RandomProperty joins the imported columns (the import audit — old
work-item 213, private archive — lists it dropped). (2) ROLL — one weighted-pick fn (the
group_pick idiom) called at BOTH creation sites: the creature/chest loot roll and
E_CREATE_ITEM (crafting); stamp random_property_id END-appended on ItemInstance
(default 0 = plain). (3) FOLD — equipped_stat_bonus adds the property's stat rows
(mirror-table lookup; the enchant-overlay pattern). (4) WIRE — emit the id in: the
loot-response item block, the item CREATE object (set_item_random_properties_id),
trade/inspect passthrough, and keep template query random_property so craft previews show
<Random enchantment> natively — the blacksmith's mystery preview resolving at creation, for
free. Every touched ITEM_FIELD arm gets the OBJECT_FIELD_TYPE crash-class values pin
(danger-zones §1 — hence not agent-safe).
Done when: an imported suffix item (e.g. a Defias-dropped "of the ..." world drop) rolls
different suffixes across kills (sql distribution over N debug rolls matches the weight
table), the live client tooltip shows the suffix name + stats matching sql, the crafted
fixture previews <Random enchantment> and resolves on craft, and the stat fold moves vitals
(recompute assert).
RE-SCOPED IN (operator decision 2026-07-06, superseding the same-day out-of-scope call):
this IS a vanilla feature, and — clarifying the concern that prompted the earlier call —
vanilla does NOT implement it by seeding item variations. There is ONE template row per
suffix item; the suffix is a per-INSTANCE random_property_id resolved at read time by both
sides. That is precisely the no-duplication, DB-as-truth shape this codebase wants:
one imported template + one END-appended instance column + mirror tables for the stat fold +
a weighted roll at the two creation sites. Nothing in this item creates template copies; if
any implementation attempt reaches for per-variation template rows, that attempt is wrong —
the instance column is the identity. (For custom BEYOND-vanilla pools, see the rejected
custom-pool proposal — old work-item 219, private archive — and its synthetic-entry
correction; different mechanism, still out of scope.)
Migrated 2026-08-11 from the private tracker (spacetime-core#161; originally work-item 218).
Activity
Mathih13 commentedon Aug 22, 2026
Spec
Settled in a design interview on 2026-08-22. This replaces the build plan in the issue body. The differences are listed at the end.
Model
One Item Template per suffix item. No template copies, ever. The suffix is a per-instance value,
random_property_id, that both sides resolve at read time. 0 means a plain item.An implementation that adds a template row per variation is wrong.
Data
Three new tables and one new template column. The importer fills all of them. All are plain SQL rows that a person can read and write.
game_item_random_property. One row per property:property_id, its enchantment references (up to 3, per the DBC row), andsuffixtext. FromItemRandomProperties.dbc.game_item_enchantment. One row per enchantment effect:enchant_id,kind,amount, and a spell reference where the effect needs one. FromSpellItemEnchantment.dbc.game_item_property_weight. The weighted pick table: pool id, property id, weight. From the dump'sitem_enchantment_template.game_item_template.random_property. END-appendedu32,#[default(0)]. The pool id. The importer drops this column today (source column 114, per its own comment atimporter/src/main.rs:395).Two END-appended instance columns, both
u32and#[default(0)]:ItemInstance.random_property_idinmodule/src/items/tables.rs.CorpseLoot.random_property_idinmodule/src/loot/mod.rs.rules::ENCHANTSgoes away.enchant_statreadsgame_item_enchantmentinstead, so the property fold and the enchant fold share one mechanism and one import. The hand-authored const says in its own comment that it stands in for this import. The import audit must assert that enchant 7745 and 7748 keep their current stat and amount, so equipped enchanted items do not move.Three traps, checked against
docs/danger-zones.md:suffixstring is safe becausegame_item_random_propertyis a new table. Danger zone 6 blocks a defaultedStringon an END-append only, and a new table has no rows to backfill.--include-private, plus the post-regen hand-patch listed in danger zone 2.Stat kinds
kindis our own vocabulary, not the DBC encoding. Stable integers, append-only, never reordered, documented inCONTEXT.md. The importer translates the DBC into it, so a person writing a modded row needs no DBC knowledge.The vocabulary covers every suffix family: the five primary stats, health, mana, the six resistance schools, armor, weapon damage, spell power, healing power, mana per 5, health per 5, crit, hit, defense, dodge, parry, block.
The importer never drops a row it cannot map. An unmapped effect imports as kind 0, unknown, and folds as nothing. A mapping fix is then an importer change plus a reimport, with no schema change.
The roll
One pure weighted pick over the pool's rows, in the shape of
group_pick(module/src/loot/mod.rs:160). The pure function takes the roll. The call site suppliesctx.random::<u32>() % 10000, matching the loot roll idiom.The roll runs in
store_item(module/src/items/ops.rs:180). Every item birth already goes through it: grant, buy, loot, craft, buyback, starter kit.store_itemtakes an optional pre-rolled id. If the caller supplies one, it uses it. If not, and the template names a pool, it rolls. That is one roll site, not two.Loot rolls early because the Loot Window must show the suffix before pickup.
roll_corpse_loot(module/src/combat/death.rs:309) stamps the id on theCorpseLootrow, andapply_take_lootpasses that id intostore_item. GameObject loot writesCorpseLootrows through the same path, so chests need no separate work.The merge branch of
store_itemmust not top up a stack whose property id differs. Suffix items are never stackable, so this is a guard, not a behaviour.The fold
The property's rows add on top of the template stat and the enchant, in the overlay shape
equipped_stat_bonusalready uses.Full family support is in scope, in two Tickets.
Tracer: the import, the roll, the two instance columns, the wire, and the fold for the kinds
EquipStatcovers today (strength, agility, stamina, intellect, spirit, crit, hit, armor).Second Ticket: the other families. Each one has a fold that exists and needs a gear term added. None of them is a new system.
resistance_bonus(module/src/spell/math.rs:719) sums auras only. A gear term also makes the sixItemTemplateresist columns live, which the importer fills and nothing reads.weapon_profile_in_slot(module/src/combat/folds.rs:343) reads the template only.effective_dodge_bpand its siblings (module/src/combat/folds.rs:36) are aura-only, and the note at line 23 says so. Copy the crit and hit pattern at line 266, which already sums gear.regen_powerandregen_health(module/src/combat/tables.rs:484) scale on spirit and level. Add a flat term.spell_power(module/src/spell/math.rs:661) sums an intellect ramp and auras. Add a gear term.The wire
Send the id everywhere an item instance goes:
gateway/src/codec/loot.rs:38, the loot bodyrandom_property_id, and the hardcoded zeros at lines 89, 128 and 148.build_item_push_result,gateway/src/codec/item.rs:408.gateway/src/codec/mail.rs:42.gateway/src/codec/auction.rs:33.build_item_create_object,gateway/src/codec/item.rs:329. Add.set_item_random_properties_id(..)to both theUpdateItemandUpdateContainerbuilders. The setter exists in the dependency.build_item_query_response,gateway/src/codec/item.rs:107. Setrandom_propertyfrom the template's pool id. The field is on the wire struct and falls out of..Default::default()as 0 today. This is what makes the client print<Random enchantment>on a craft preview.The partial VALUES crash class in danger zone 3 does not apply here.
build_item_create_objectbuilds a full CREATE mask, and items have no partial VALUES path. The property id never changes after creation, so they still need none. A later change that adds one must route throughdirty_reset, andgateway/src/world/packet_lint.rscatches the mistake.Acceptance
Out of scope
Custom variants beyond vanilla. A content package owns those: SQL rows in the tables above, plus a client addon or XML extension for display. The engine needs no change to support them. Worth stating plainly, because the split is deliberate. A modded property id works server-side and shows no suffix text, because the 5875 client reads suffix names from its own DBC and the protocol has no per-instance name field. The addon bridge (
module/src/bridge.rs,gateway/src/codec/addon.rs) and thesuffixcolumn are the seams a package would build on.The realm update. No suffix drops until a realm reimports. Normal operator process, after merge.
Glossary
Add to
CONTEXT.mdin the Tracer, under a new "Random properties" heading.Random Property: The suffix rolled onto one item instance at creation, held as
random_property_id. 0 is a plain item.Avoid: random enchant, item variant, roll
Property Pool: The weighted set of Random Properties a template can roll, named by
item_template.random_property.Avoid: random property group, suffix group
Suffix: The name a Random Property adds to the item, such as "of the Bear". The server keeps a copy for diagnosis; the client renders its own.
Avoid: affix, postfix
Stat Kind: Our own stable code for what an enchantment effect grants. Append-only, and not the DBC encoding.
Avoid: stat type, mod type, effect type
Differences from the issue body
store_item, instead of at the loot andE_CREATE_ITEMsites separately. Every creation path then behaves the same, and a path added later gets suffixes with no code change.game_item_enchantmentreplacesrules::ENCHANTSrather than sitting beside it. One overlay mechanism, and a documented deferral closes.kinduses our own vocabulary rather than the DBC encoding, so SQL is the authoring surface for a modder.Specified with Claude Code. Fable 5 ran the design interview, Opus 5 wrote this up.
Mathih13 commentedon Sep 28, 2026
Live acceptance on Argus, 2026-09-28
Realm: Argus at Core
eb4ffe4d, Packages9df067d2b. Client: stock 1.12.1 (5875). Character: Pbguide (guid 2117, level 20 Mage).Roll. 37 debug kills of Lord Condar (14268) in front of Pbguide. Every pooled drop got a nonzero
random_property_idon itsgame_corpse_lootrow. Properties seen: 96, 1179, 760, 17, 1879, 839, 840, 1013, 18, 757, 585, 22, 1010, and more.Client display. The loot window and loot tooltips show the Suffix and the stat lines. Each one matches
game_item_random_propertyandgame_item_enchantment:Fold. Willow Boots of Stamina replaced Apprentice's Boots (no stats, no armor). Server max health went from 327 to 357. The character sheet shows Stamina 29 (was 26) and Armor 72 (48 base + 3 robe + 2 pants + 19 boots). The instance kept property 22 through loot, bag and equip.
Gap found and fixed. Owned items showed the Suffix name but no stat lines. The loot tooltip had them, because the client reads an owned item's property stat lines from enchantment slots 3 to 5. #583 sends the property's enchant ids there. After the update, the equipped Willow Boots and a newly taken Ritual Bands of the Whale show their stat lines. That covers the world entry and live insert paths.
<Random enchantment>preview. No vanilla craft, vendor item or quest reward uses a Property Pool, and 1.12 link tooltips do not render the line. The item query value stays pinned by the Gateway test.Commands (on Argus,
-s local, databaselyracore):Every debug spawn row was deleted after its corpse decayed. Screenshots and recordings are with the maintainer.
Pickup from the client (
LootSlotor a click) did not reachgw_take_looton either Gateway build, so the item moved throughdebug_take_loot. That calls the sameapply_take_loot. This is tracked separately.Posted by Claude Opus 5.5 via Claude Code.
Mathih13 commentedon Sep 28, 2026
Screenshots from the live acceptance on Argus, taken with a stock 1.12.1 client as Pbguide.
Top row, left to right:
Bottom row, left to right:
The commands and SQL results are in the comment above. #462, #466 and #583 ship the spec, and this check verifies it live. The follow-ups are #584 (client loot pickup) and #585 (permanent enchants on the client).