Skip to content

Loot Spawns and Crafting

Kurt edited this page Jul 10, 2026 · 3 revisions

Loot, Spawns and Crafting

Audience: Engine Developer Status: ✅ Ready

The reward and economy layer: what drops when a mob dies, how item quality and rarity roll, how world bosses respawn on a schedule, how characters advance along tracks, and how crafting and salvage move materials through the economy. As everywhere, the engine supplies mechanism (a loot resolver, a scheduler engine, grant ops, a recipe/salvage runtime) and content supplies every table, tier, and recipe.

Related: Abilities & Effects (the op vocabulary these build on), Combat System (death triggers loot), Orchestration & Directors (scheduled spawns), Pack Entity Reference.

The loot resolver

resolveLoot runs from die() before the threat table is scrubbed (threat is the eligibility source), on the dying mob's zone goroutine with the per-zone seeded RNG.

  • Personal loot only. Each eligible looter rolls the entire table independently, and drops are delivered directly into that looter's inventory — the corpse holds only the body. Shared/contested loot is not implemented.
  • Eligibility = threat: every player who dealt any damage. Tag/group eligibility is not implemented.
  • Roll kinds: guaranteed/weighted_one (pick exactly one), chance (probability gate then one pick), weighted_n (N independent picks). Rolls within a table are independent, not mutually exclusive. Weighting uses the entry's weight, else its rarity tier's default, else 1; quality_floor keeps only entries at or above a tier order.
  • Pity (bad-luck protection): a per-chance-roll {key, step, cap} raises the effective chance by misses × step up to cap; miss counters are per-looter, per-key, and persisted (a hit resets to 0).
  • on_roll(ctx) Lua hatch: runs once per looter after the declarative rolls, inspects ctx.looter/ctx.source, and returns additional prototype refs the caller delivers. It's a read-only decision (can't bypass delivery or spoof a source), fail-closed (no drops on error), capped at 64.

Item quality, rarity & affixes

  • rarity_tier_defs: order (the ordinal / quality_floor key), default weight, color, binds (items of this tier bind on creation — the top-tier no-trade sink), and salvage-derivation fields. The rarity ladder is entirely content.
  • affix_defs: a reusable named {attr, min, max}. A quality pool references one inline or by ref (resolved at build time; an unknown ref becomes an inert empty affix flagged by a boot lint).
  • Per-instance quality roll: {affixes, count, levelMin, levelMax} rolls a Level and count affixes (with replacement). This is a coarse v1 — a repeated attribute takes the last roll (no pool de-dup).
  • Item-delta persistence: Quality{Level, Affixes} is the per-instance delta over the shared prototype — two drops of the same proto differ only here. A worn affix's stat effect is applied by the Wearer gear modifier source (Abilities & Effects); persistence stores the delta (Persistence & Durability).

Not hot-reloadable: loot tables and their resolved affixes are baked at build time; a running shard keeps boot values until restart.

Director-owned scheduled spawns

World-boss schedules are long-timer and owned by the director, distinct from short-cycle zone resets. The scheduler engine (schedule.go) is pure and deterministic given a now, so restart-safety is exhaustively testable: IsDue, AfterSpawn, and AfterDeath (which sets the next spawn to death + interval — so "weekly" means seven days after it dies). on_missed policy handles a window that passed during downtime (spawn_if_overdue vs skip_to_next). State is persisted one key per schedule via a versioned CAS; the tick loop is leader-gated so exactly one director fleet-wide spawns.

The engine provides the scheduler + broadcast + reschedule. The actual spawn and death-report are content Lua (an on_world("spawn.boss") handler spawns the boss; the boss's on("death") signals boss.died back up). Cron/wall-clock scheduling is not implemented — only interval-after-death. And the production director-script that consumes a custom event is not yet authored — see Orchestration & Directors.

The advancement system

A track (track_defs) is the single abstraction for every advancement mode — they differ only in which event feeds the progress attribute. level stays an attribute: levelAttr merely names which attribute (if any) a step raises. Thresholds are ascending; crossing thresholds[i] reaches step i+1, whose grant op-list runs. Two ops drive it:

  • grant_track — add a track at step 0, idempotent (never resets progress; multiclass/reload-safe).
  • advance_track — raise the progress attribute, then apply the grants of every newly-crossed step (a big XP award can jump several levels). Each step's grants run once (the stored step is the high-water, so a reload restores the step without re-running grants). Fires OnTrackStep (and OnLevel for a level track).

The four modes are all the same machinery — only which handler calls advance_track differs: XP-auto (an OnKill handler), use-based (an OnSkillUse handler; a gated/refused op cancels the fire so you can't train past a gate), train-at-trainer (a trainer ability), and point-buy (a spend-points ability; chargen point-buy applies as attribute base on first spawn).

A bundle (bundle_defs) has a kind discriminator (class/race/background/feat/talent/profession) and a grant op-list; apply_bundle runs it on the same context so every grant composes. Multiclass, join-a-guild, and chargen all funnel here. Grant-op precedence is op-list order — there is no separate priority system; nesting runOps on a shared context is the composition.

Crafting & the material economy

  • Binding & the single trade gate: bind_on_pickup binds on personal-loot delivery, bind_on_equip on wear/wield. transferBlocked is the single gate every transfer (give / drop-to-ground / put-in-shared-container) consults. Binding restricts transfer only — a bound item can still be equipped, destroyed, and deconstructed by its owner.
  • Stackable materials: an item with maxStack > 0; Stack{count} is per-instance state, merged/split on pickup/craft/salvage (default max 1000).
  • Recipes (recipe_defs): craft_recipe re-validates every gate as a backstop (profession membership, skill level, station room-flag), validates all inputs before consuming any (all-or-nothing), then consumes and produces. Output quality is a coarse band (qualityBase + skill level); rich affix rolls are deferred. craft <name> resolves via the Diku isname/ordinal grammar, and list_recipes prints exactly the names it accepts.
  • Professions are not a new table — a profession is an ordinary bundle (kind: profession) whose grants hand out craft verbs, the skill track, and a learn_profession op. Membership (the only new state) is capped (content max_professions, default 2); an uncapped gathering/utility bundle is unlimited.
  • Salvage / disenchant: salvage_item consumes a held source (even a bound one — destruction isn't transfer) and rolls a salvage table by reusing the loot resolver. The table is per-item override, else the rarity tier default, else the verb's fixed table. A skill gate (tier.salvageSkill + item Level) plus an over-skill bonus grants extra chance-roll-only passes (so a scarce guaranteed component isn't N-multiplied). Only the base pass advances the player's pity counter — a bonus pass re-rolls at the pity-adjusted (buffed) odds but neither consumes nor compounds it, so one over-skilled salvage can't burn the bad-luck standing several times over. Components bind by tier (tier.binds) — top-tier essence is a no-trade sink, low/mid stays tradeable to feed the market.

Deferred / known gaps: augment_item is a flat-stat-bump stub (deep affixes/sockets not built); rich craft-output affix rolls are deferred; and per-instance quality is coarse (a repeated affix attribute takes the last roll, with no pool de-dup).

Clone this wiki locally