-
Notifications
You must be signed in to change notification settings - Fork 0
Combat System
Audience: Engine Developer Status: ✅ Ready
Combat is round-based in the Diku/ROM tradition, driven off the zone pulse. A fight arms one per-zone driver that resolves a round every ~2.5 seconds; each swing runs a layered pipeline — to-hit, avoidance, damage, mitigation — where every number is content-supplied through the check/formula/attribute substrate. The engine names no class, weapon, or condition. All rolls come from a per-zone seeded RNG, so a fight is reproducible when seeded.
Related: Abilities & Effects (the shared damage funnel + reaction checkpoints), Entity Component Model.
The pulse quantum is 250 ms; a combat round is PULSE_VIOLENCE = 10 pulses (~2.5 s). startFight sets both parties to posFighting, sets the fighting pointers (forking a proto-aliased mob via COW first), makes an unengaged target auto-retaliate, and arms one every(PULSE_VIOLENCE) driver per zone (idempotent; self-retires when no live fighters remain).
Each round: gather combatants (players by sorted character id for determinism; mobs by scanning the rooms where a fighting player stands), order them by a stable sort on the content attribute combat_order (the initiative seam — 5e initiative is optional content), top up per_round reaction budgets, then resolve swings per attacker, re-validating the fighting link each iteration. Attacks per round = the attacks attribute (floor 1, hard cap 50 so a haste stack can't spin the goroutine). A killing swing stops the rest of the round.
kill enters combat; flee leaves it (flee <dir> is the ROM panic-flee — it fires the OnLeaveRoom opportunity-attack checkpoint while still engaged). Combat is transient — never persisted, never handed off; a move/transfer calls disengage first, and no fighting pointer ever crosses a zone.
Zone.combatRand is a per-zone *rand.Rand (production-seeded, reseeded to a fixed value for tests/replay). It is the single draw source for to-hit, avoidance, crit, damage dice, in-combat ability rolls, and DoT tick damage — so a fight never touches process-global math/rand and replays deterministically when seeded.
flowchart LR
G["gates<br/>(canAct/canDefend/same-room)"] --> R1["ToHit reaction<br/>(rx: modify 'ac')"]
R1 --> TH["to-hit check<br/>(combat_profile.to_hit)"]
TH --> AV["avoidance ladder<br/>(first success negates)"]
AV --> D["damage (+crit)<br/>weapon dice + bonus"]
D --> M["mitigate<br/>resist × − soak"]
M --> A["apply → OnHit"]
The check primitive is the universal roll for attacks, saves, skill checks, and contested rolls: roll a content dice spec, add a scoped bonus formula, compare against a dc formula (or a contested defender roll), and classify into the first matching ordered band. Bands test the total, the margin over DC, and/or natural faces (the only way to author a nat-20 crit / nat-1 fumble). Roll visibility defaults to hidden; staff rolls on upgrades engine-default-hidden checks to full math.
-
To-hit: the attacker's
combat_profile.to_hitspec resolves; the band label classifies (miss/fumble→ miss,crit→ crit, else hit). A nil profile auto-hits — the degenerate bare-engine case. -
Avoidance: the defender's ordered
avoidancespecs run; the first success negates the swing. The engine hardcodes no dodge→parry→block order — it runs whatever content authored (5e/WoW author none). -
Damage: wielded
Weapondice + type (or a mob's natural attack, orunarmed_dice/ a1d3fallback), plus a scopeddamage_bonusformula. Crit is content — acrit_multattribute scales the whole roll through the samedeal_damagepath. -
Mitigation:
raw × resist/vuln/immune − soak, floored at 0. The resist matrix comes from the damage type'sresistmap (1 neutral, <1 resist, >1 vuln, 0 immune);soakis a flat by-type reduction reading the content attributesoak_<type>fed by armor.
combat_profile_defs drive all three (to_hit, avoidance[], damage_bonus); a malformed sub-spec logs loudly and degrades (auto-hit / no avoidance), never aborts boot.
Every damage source — melee, spell, AoE, DoT tick, opportunity attack — routes through the one shared funnel dealDamage:
guardHarmful (PvP/detached gate) → mitigate → OnDamageTaken reaction
→ subtract from vital pool → threat → OnDamageTaken event → OnHit event → depletion checkpoint
guardHarmful is the single hostility chokepoint (Abilities & Effects). Threat accrues before the death scrub so a killing blow is still attributed.
The cancellable death checkpoint: when the vital pool hits 0, the resource's content on_depleted op-list runs, then the vital is re-read — if a hook revived the victim above 0 (a death-ward modify_resource hp +1, a second wind), the death is cancelled declaratively: no die(), no corpse, no respawn. The re-check is the cancel mechanism; the death hook is recursion-bounded so a ping-pong on_depleted terminates rather than overflowing the stack.
die order: emit the death line → fire OnKill while the victim is still in-room → resolve loot per eligible looter → disengage + scrub threat → latch posDead → fire the Lua death trigger → player respawns / mob becomes a corpse. A corpse is an engine-built container holding all carried + worn items, stamped with a 60 s loot-ownership window keyed by the killer's durable id (anti-ninja-loot); a mob-on-mob kill is free-for-all. Threat is a per-Living map; topThreat picks the highest live in-room foe (deterministic tie-break by name).
Player death is minimal: a full-heal respawn at the start room — no corpse, XP loss, or gear drop. That's a deferred ruleset knob.
- Cast time and cooldown are implemented (cooldown persists as remaining pulses across logout and rehydrates on load).
-
Afflictions/DoTs tick through the affect runtime, drawing from
combatRng()and routing the gated funnel, so a DoT on a protected player is PvP-gated and kills through the uniform death seam. -
Tag-based CC (
prevents) blocks abilities and movement (Abilities & Effects).
Two honest gaps worth flagging:
- Skill lag / WAIT_STATE is logged only, not enforced. An ability's
lagis parsed and logged at commit but imposes no timer — the single biggest combat gap.- Basic auto-swings are not gated by
prevents. The swing gate checks only position (sleeping/dead); there is nopreventsTag(attacker, "attack")check and noposStunnedposition, so a stunned-but-fighting entity keeps auto-swinging. CC that should stop auto-attacks currently only blocks abilities and movement.
Result-altering Lua hooks at named checkpoints, with a closed per-checkpoint field allowlist (a non-allowlisted rx:modify is a silent no-op):
| Checkpoint | Fires |
rx may |
|---|---|---|
ToHit |
defender, before the to-hit roll |
modify("ac", …) only — threaded into the to-hit DC for this swing only, never persisted |
OnDamageTaken |
the damage-taker, after mitigation, before apply |
modify("amount", …) reduce-only, cancel(), replace_target(handle) (redirects the raw blow, re-mitigated against the new target's resistances) |
Both thread the round-shared event budget so a reaction cascade can't blow the heartbeat. (BeforeCastCommit — a counterspell cancelling a cast — is the ability path, not a swing; see Abilities & Effects.)
Combat numbers are overridable through the check-spec / formula / attribute substrate (combat_profile_defs, band labels, crit_mult, attacks, combat_order, soak_<type>, damage-type resist, unarmed_dice, xp_value) — this is the live mechanism. The separate named-formula Lua registry (to_hit/soak/regen/xp_for) is a mostly-dead seam: only regen is wired today; a to_hit/soak/xp_for override parses, warns at load, and is never called. Also reserved: a per-pack PULSE_VIOLENCE override, and GMCP per-stage combat / cooldown / lag HUD deltas.
TelosMUD — Wiki under construction.
- Builder Reference
- Builder Commands
- Trust Tier Model
- Pack Authoring
- Pack MUD Settings
- Pack Lua Scripting
- Pack Lua Hooks
- Pack Entity Reference
- Building Instanced Zones
- Engine Developer Reference
- Architecture Overview
- Entity Component Model
- Zone Runtime & Actor Model
- Instanced Zones
- Command Parser & Targeting
- Edge & Protocol
- GMCP Reference
- Persistence & Durability
- Content Loading & Hot Reload
- Abilities & Effects
- Combat System
- Loot, Spawns & Crafting
- Accounts & Auth Internals
- Orchestration & Directors
- Scoped Event Bus
- Cross-Shard Handoff
- Lua Sandbox Internals
- Distributed Systems Model
- RPC & Protobuf