Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Gameplay framework: tags, stats, effects, damage, abilities, equipment

manifold-gameplay is the Talos gameplay framework: hierarchical tags, one condition and number language, stats with a fixed modifier order, effects with explicit stacking, the staged damage pipeline, death, factions, loot tables, cues, the public presentation component, abilities, equipment and item kinds. Content declares all of it in RON under data/; the framework aggregates, applies and explains it identically on native and browser servers. The design and its rationale are in Talos B (docs/superpowers/specs/2026-09-27-talos-b-gameplay-framework-design.md); this page is the current contract for what is built (B0 to B5).

Source: manifold-gameplay (crates/manifold-gameplay/src/) (a wasm-safe logic crate), the server glue in manifold_server_state::gameplay (crates/manifold-server-state/src/gameplay.rs). Tests: crates/manifold-gameplay/tests/ (framework.rs: content through the data stage, golden stat and damage breakdowns, immunity windows, region tags, death and loot, effects, factions, determinism; abilities.rs: held channels, traces, the reload’s deny rule, the charge, the telegraph grace, rejections, interrupts, equipment, prediction and load limits over a fake AnimBridge; abilities_b4.rs: Next, Leap, WaitTags, charges and costs, command-lane activations, backdated cooldowns, WaitWindow and windowed sweeps, projectiles, and the owner’s prediction ledger; stats_effects.rs: derived and scoped stats, equipment and passive modifiers, periods, removal triggers, immunity, cleanse, ongoing, stacks that fall off one at a time, the hidden-instance cap, registered effect parts, persistence and effect timers; damage_breadth.rs: elements and conversion, hook order, region multipliers and armour by region, per-limb resources, block and parry, thorns, lifesteal, heals and the ledger’s totals, pre_death revives, impacts and explosions; loot_factions.rs: nested tables, luck, per-player drops and their visibility, credit rules, containers, parties and PvP; determinism_guard.rs), game/flagship/tests/gameplay_content.rs, and the two-client scenarios in crates/manifold-server/tests/anim_two_client.rs and npc_two_client.rs (the M14 end to end).

The crate

ModuleWhat
apiids, Relation, ModOp, Subject, ContextParam, HitDetail, HitKey, ActivationKey, AnimBridge and the combat event records. Compiles with --no-default-features for guests.
datathe registries’ registration, engine entries, derive steps, GameplayConfig, named predicates
tags, expr, stats, effects, damage, death, factions, loot, cues, presentationeach subsystem’s registry entry, components and pass
damage::hooks, damage::heal, damage::sourcesdata hooks and hook sets, healing, Daedalus’s impacts and explosions
abilitiesability chains (flow), the server runtime (run), telegraphs, projectiles (projectile), the owner’s prediction and its ledger (predict)
equipmentslots, item kinds, loadouts and set-equipment
componentscomponent metadata both hosts register
worldinstall_gameplay, GameplayRegistries, the reader, the passes and the public API

Determinism: arithmetic is f32 summed in a fixed order, transcendentals come only from manifold-det, randomness only from A’s DerivedRandom at sites hashed from registry keys (<id>#<node path>), and no module iterates a hash map. tests/determinism_guard.rs scans every source file with manifold-anim’s logic bans.

Registries

B registers its registries in the boot stage (gameplay::register_boot_registries), so every host and cargo mod validate see them. The engine’s own entries are code trees content can patch.

RegistrySideDirectoryEngine entries
manifold:tagBothtagsthe shared vocabulary (manifold:state.*, manifold:status.*, manifold:damage.* flags, manifold:region.<standard region>, manifold:hit.critical, manifold:cancel.* reasons)
manifold:statBothstatsthe core pack: max_health, health, incoming_damage, incoming_healing, knockback_resistance, cooldown_rate
manifold:predicateBothpredicatesnone
manifold:elementBothelementsmanifold:generic
manifold:damage-formulaBothdamage_formulasmanifold:basic
manifold:damage-typeBothdamage_typesmanifold:generic, manifold:command, manifold:kill
manifold:effectBotheffectsnone
manifold:cueBothcuesnone
manifold:factionBothfactionsnone
manifold:loot-tableServerloot_tablesnone
manifold:presentation-fieldBothpresentationmanifold:dead
manifold:gameplay-configBothgameplaymanifold:config (patch it)
manifold:abilityBothabilitiesnone
manifold:equipment-slotBothequipment_slotsnone
manifold:item-kindBothitemsnone
manifold:damage-hooksServerdamage_hooksnone
manifold:impact-damageServerimpact_damagenone
manifold:explosion-damageServerexplosion_damagenone

Tag paths use [a-z0-9_] segments, so sorted dense ids are the depth-first pre-order: is_a is a range test. Declaring a tag declares its ancestors. B also declares the manifold:item_drop archetype every loot drop spawns from, adds manifold-gameplay:tags to archetypes that declare tags, and checks the stat, loot-table and faction keys archetypes name.

Conditions and numbers

A number is a literal, Molang shorthand or a typed record; a condition is Always, Never, shorthand or a typed record:

duration: For(secs: "4 / q.stat('this', 'manifold:cooldown_rate')"),
conditions: [Present(KillerPlayer), Chance("0.02 * (1 + v.luck / 100)")],
when: ContextTags((any: ["manifold:region.head", "manifold:hit.critical"])),

Shorthand compiles through manifold-molang in MathMode::Portable against a gameplay vocabulary (q.stat('<subject>', '<stat>'[, '<part>']), v.level, v.stacks, v.damage_amount, v.luck, v.impact_energy, v.impact_speed, v.distance, set-by-caller names, math.min max clamp floor pow random, comparisons, &&, ||, !, ?:) and lowers into the same tree as the RON form. Every use site has a context set (effect_apply, damage, ability, impact, loot_entity, loot_generic, spawn, sensor); a reference outside it is a load error with file and line. Trees compile to stack programs (64 nodes, 16 slots, Refs inlined to depth 8) that record their purity and predictability. Other crates add native atoms and subjects on GameplayExtensions (gameplay::gameplay_extensions).

Stats and effects

A stat’s final is (base + Σ flat) × (1 + Σ increased) × Π (1 + more) + Σ final, then the last override, then the clamp and rounding, summed in stat modifier id order. On top of that:

  • Derived stats. derived: "20 + 5 * q.stat('this', 'x:vitality')" makes a stat’s base a pure formula over the same entity’s stats (the stat context set: This only; stats, arithmetic, curves). The data stage rejects cycles at the formula’s file and line and freezes each stat’s dependants, so changing a stat re-settles everything derived from it. A resource derives its maximum, not itself; KeepFraction then rescales it. The breakdown marks a derived base.
  • Scoped modifiers. A modifier with scope: <condition> (in the damage set, where the victim is This) is left out of the cached final and counted only when the stat is read in the damage pipeline and the scope holds: formula stat reads, the basic formula’s armour and resistance, and manifold:incoming_damage. “+100 damage against undead” is (stat: "x:damage_additive", op: Flat, value: 100, scope: Tags(of: This, query: (any: ["x:creature.undead"]))); “half fire damage taken” is a scoped More(-0.5) on incoming_damage with DamageType((any: ["manifold:damage.is_fire"])). Breakdowns list scoped modifiers apart (conditional).
  • Granted modifiers. An item kind’s grants: (stat_modifiers: [..]) apply with source Equipment { slot } while it is equipped, and an ability’s passive: [..] with source Passive(ability) while it is granted by anything. Their values are constants (a value that scales with the wearer is a derived stat), their ids come from the slot or the ability’s key, so re-equipping replaces, and world::refresh_granted_modifiers rebuilds them (after hooks, at the pending pass, and on set-equipment).

An effect is a bag of parts:

Effect(id: "x:poison", asset_tags: ["x:effect.poison"], duration: For("5s"),
    period: (every: "1s", on_apply: false),
    damage: Some((damage_type: "x:poison", base: 2)),
    removal: [SourceDied, Charges(count: 3, spent_on: DamageTaken)],
    stacking: PerSource(reapply: Refresh), persist: true, ui: (timer: SourceOwner))
PartDoes
modifiers, grant_tagswhile the instance is live (neither hidden nor inhibited)
heala number healed through incoming_healing on application, or each period
damage_hooks: [DamageHook(..)]data hooks while the instance is live (below)
period: (every, on_apply)runs the instant parts (base changes from the modifiers, damage, apply_effects, the cue, parts’ on_period) every period; a period at the expiry tick runs; Refresh keeps the phase
requirementsan effect_apply condition checked before applying
immunitywhile live, blocks applications whose asset tags match (EffectBlocked { Immune })
cleanseon application, removes the holder’s other instances whose asset tags match
ongoingwhile the holder’s tags do not match, the instance is inhibited: it keeps its timer, its modifiers and tags are revoked
removalTagAdded(q), DamageTaken(min: ..), DealtDamage, AbilityActivated(q), SourceDied, SourceGone, SourceBeyond(metres: ..), Moved(metres: ..) (distances at 4 Hz), Charges(count: .., spent_on: DamageTaken | DealtDamage | AbilityActivated); removal reason Trigger
stackingIndependent, Replace, PerSource, Aggregate (with expire: One, stacks fall off one at a time), StrongestWins (at most two hidden instances per group)
parts: [Part(id: .., args: (..))]registered native parts (below)
persist, persist_through_deathsaved (§14.3); kept through death
ui: (timer: SourceOwner)the source’s owner receives S2C::EffectTimer on apply and refresh

Triggers are indexed per entity by kind, so an event visits only the instances listening for it; they run at the end of the apply pass against this tick’s damage, abilities, killing hits and tag changes, and an instance never answers events from the tick it was applied in.

Registered effect parts are native part kinds other crates register on GameplayExtensions::add_part (E’s taunt is the first): an EffectPartDef { id, args, requires, predictable, on_apply, on_period, on_remove } whose hooks receive a PartCall (the world, holder, source, owner, instance, level, stacks, parsed arguments, tick) and return PartOp::Damage or PartOp::ApplyEffect. They run after the built-in parts, in list order; state a part keeps lives in its own component.

Persistence. manifold-gameplay:effects (EffectBlock, beside manifold-gameplay:stats) holds the entity’s persist instances by effect key with level, stacks, charges, source id and remaining ticks, and its loose grants of PERSIST tags by name. The settle pass writes it when they change and once a second while a saved instance’s time runs; inserting it (a load) restores them at the next pass without applying anything again. Modifiers and granted tags are rebuilt from the instances.

Archetype variants use the same conditions in the spawn set: a when that is not a literal Chance(p) compiles through the VariantConditionCompiler B installs in the data stage.

Components

KeyStored asReplicationWritten in dataBrings
manifold-gameplay:statsTracked<StatBlock>server-only, persisted(base: { "<stat>": 40.0 })Stats, ActiveEffects, manifold-gameplay:effects
manifold-gameplay:effectsTracked<EffectBlock>server-only, persistedadded empty to archetypes with statsits insert restores saved effects and loose tags
manifold-gameplay:tagsTracked<TagContainer>server-only(); archetype tags are granted
manifold-gameplay:damage-receiverTracked<DamageReceiver>server-only(resource: .., death: (loot: .., corpse: Linger(ticks: 40), despawn: AfterCorpse), track_contributors: .., by_region: [("manifold:head", "<stat>")])HitImmunity, DamageLedger, the receiver’s and regions’ resources at their maximum, the presentation component
manifold-gameplay:damage-hooksTracked<DamageHookSets>server-only(sets: ["<hook set>"])
manifold-gameplay:factionTracked<FactionMember>server-only(id: Some("<faction>"), inherit_owner_faction: true)EffectiveFaction
manifold-gameplay:corpseTracked<Corpse>publicinserted at death
manifold-gameplay:dropped-itemTracked<DroppedItem>public (a per-player drop reaches only its player), persistedon manifold:item_dropHiddenFrom while visible_to is set
manifold-gameplay:loot-containerTracked<LootContainer>server-only, persisted(table: "<loot table>", mode: Once | PerPlayer)
manifold-gameplay:presentationbytes in the novel bagpublic, Critical, relabelled for the ownerfields in data/presentation/
manifold-gameplay:abilitiesTracked<AbilityGrants>server-only(granted: ["<ability>"])AbilityState
manifold-gameplay:loadoutTracked<Loadout>server-only(items: [("<slot>", "<item kind>")])AbilityState; fills the equipment
manifold-gameplay:equipmentbytes in the novel bagpublic, Critical, relabelled for the ownerthrough the loadout or set-equipment
manifold-gameplay:telegraphTracked<Telegraph>public, Criticalwritten by Telegraph elements

Dropped items

A drop is a manifold:item_drop entity with a DroppedItem { stack, .. }. A per-player drop (visible_to: Some(player)) carries manifold_engine::visibility::HiddenFrom(AllExcept(player)), which the drop’s insert hook derives (so a loaded drop gets it back; the marker is never saved) and loot::set_visible_to changes: the server’s interest pass leaves it out of every other client’s scope, so no other client ever receives it (Talos F §6.3). A pet fetches only drops its owner could see. Clients draw it (manifold_client_state::drops, called from present_animated_entities on both hosts) at its replicated position, turning and bobbing in render time, with:

  1. its item kind’s model (§12.5) when the client has that model;
  2. else its own manifold-model:model, which the game sets on the manifold:item_drop archetype (the flagship patches in flagship:items/loot_sack in data/archetypes/item_drop.update.ron);
  3. else nothing.

The flagship’s boar_tusk names flagship:items/boar_tusk; leather and the piglet egg show the sack. A drop an agent carries (manifold-ai:carried) is not drawn on the ground: its carrier’s manifold-ai:carrying draws the same model with its grip node on the carrier’s socket, from the carrier’s pose that frame, as a player’s held item sits at hand_r (see brains).

AbilityState (runtime) brings the telegraph component, the tags component and the presentation component. The presentation and equipment components’ keys are declared at boot with the engine’s fields (manifold:dead, no slots); each host sets the data-declared layouts after its data stage (ComponentMetaRegistry::set_dynamic_schema), before binding tables compile, so tables bind Field("manifold-gameplay:presentation", "<field>") and HeldItem(component: "manifold-gameplay:equipment", field: "<slot>"). A slot holds an item kind’s dense id (u32::MAX when empty); hosts resolve it through the item registry to the model the held item shows (equipment::item_model).

The passes

The server registers B’s systems after AnimHistorySystem, before DynamicLightAnchorSystem and AnimView (B §3.3):

SystemDoes
GameplayAbilityRequestSystemclears last tick’s streams, cues and movement requests; the pending pass (loaded entities’ saved effects, equipment and passive modifiers queued by hooks); players’ activations from this tick’s played input frames and the commands on their press edges; D’s AbilityRequests
GameplayAbilityTickSysteminterrupts, then every active ability advanced once, in durable-id order; a completed chain’s Next starts the next ability in the same tick
GameplayProjectileSystemevery projectile flies one tick and lands its ability’s hit
GameplayEffectTickSystemdue periods, stacks that fall off, expiries, from the timing wheel
GameplayDamageSourceSystemDaedalus D’s impacts (read from CharacterImpacts) and F’s explosions (PhysicsDamageSources) into the damage queue
GameplayDamageSystemthe damage pipeline over the queue, in order-key order, at most damage_events_per_tick, then the hits reflected meanwhile; players’ and agents’ knockback into Daedalus D’s CharacterImpulses
GameplayEffectApplySystemparts’ operations from the other passes, queued effect applications (chained ones after their parent), then removal triggers and ongoing checks
GameplayDeathSystemDied (stream and observer trigger), loot and drops, Corpse, despawn at the deadline, revival
GameplaySettleSystemcredits this tick’s healing to contributor ledgers, settles touched stats, refreshes factions, writes persist stats back to StatBlock and changed saved effects to EffectBlock
GameplayCueFlushSystemB’s cues to the engine cue path as EngineCues; damage numbers, players’ activation answers (accepted and rejected) and source owners’ effect timers to GameplayFeedback, which the outbound phase sends as S2C::DamageNumber, S2C::AbilityAck and S2C::EffectTimer (clients keep them in HitMarkers, AbilityAcks and EffectTimers); Framescope’s gameplay_damage record per hit

Loot

Loot tables are Minecraft’s shape (§12.1). An Item(item: "<key>") entry names an item kind (manifold:item-kind): an unknown key is an error at its file and line. A roll draws from A’s DerivedRandom at (space seed, source durable id, tick), a site per pool (<table>#pool<i>) and the roll number as ordinal:

  • Nested tables. Table(table: "<key>") rolls another table; each nested roll draws from its own seed (RollSeed::nested: the parent’s pool, roll and the child), so a pool that picks one table three times rolls it three ways. The data stage refuses a cycle, a chain more than four deep, and a table nested in one whose context lacks its parameters (a LootEntity table under a LootGeneric one).
  • Luck is the roll’s luck_from entity’s GameplayConfig.loot_luck_stat (the flagship’s flagship-game:magic_find): a pool rolls rolls + floor(bonus_rolls × luck) times (at most 16), an entry weighs max(0, weight + floor(quality × luck)), and v.luck reads it in conditions.
  • Drop modes (an archetype’s death: (drop_mode: ..)): Shared(reserve_ticks: 200) rolls once and reserves the drops for the killing player; PerPlayer(credit: MinDamageFraction(0.05), expire_ticks: 6000) rolls once per credited player from the victim’s DamageLedger (the receiver needs track_contributors: true): MinDamageFraction(f) credits players with at least f of the recorded damage, AnyContribution any damage, healing or blocked damage, KillerOnly the killer; a victim with no ledger entries credits its killer. Each recipient’s roll is theirs: RollSeed::for_recipient folds their PlayerUuid into the id (pool picks and Chance draws alike), their luck is the roll’s, and they are its KillerPlayer. Their drops are reserved for and visible to them only, until expire_ticks.
  • Containers. manifold-gameplay:loot-container names a LootGeneric table; loot::open_container(world, reg, container, opener, tick) rolls it for the opener (This, and their luck) and remembers the roll: Once rolls for the first opener, PerPlayer once per player (at most 1,024 remembered). A container’s draws come from its durable id alone, never the opening tick, so its contents do not depend on when it is opened. The stacks are the game’s to put in its container UI.
  • Pure rolls. loot::roll_loot(world, reg, &LootRoll { .. }) is the roll G’s roll-loot op, previews, fishing and E’s work outputs use; it spawns nothing.

Factions, parties and PvP

factions::relation(a, b) answers, in order: the same entity or OwnedBy root is Friendly; an unexpired RelationOverrides entry; then, when both roots are players, the party and PvP overlays; then the faction matrix.

  • Parties are the game’s: it registers a resolver, GameplayExtensions::set_party_resolver(fn(&VoxelWorld, PlayerUuid) -> Option<PartyId>), which reads whatever state the game keeps (default: no parties). Players in one party are Friendly, whatever the PvP policy. A cue with audience: Party goes to the involved players and their party members.
  • PvP is GameplayConfig.pvp: Off leaves players to the matrix (a team game’s factions); OptIn makes two players Hostile when both roots carry manifold:state.pvp and Friendly otherwise; Regions((any: ["x:zone.arena"])) does the same with a tag query both roots must match (the game grants the region tag). A pet or projectile follows its owner. The right to opt in is a game permission (flagship-game.pvp), not an engine node. The flagship ships Off.

Abilities

An ability is a chain of engine elements in data/abilities/, compiled into a flat pre-order node arena (256 nodes, depth 8, 4 parallel children, 16 element starts per ability per tick; a Repeat without a count must wait). Nodes: Serial, Parallel(until: All | Any | First(i) | Released), Select([(condition, node)]), Repeat(count, until: Released, body) and Next("<ability>"), which ends the chain (it must be last, in a Serial or a Select branch at the end) and starts the named ability with the same target, point and activating input in the same tick (at most four hops per entity per tick). Elements: Commit, Wait, OneShot(play, stop_on_end), BumpCounter, SetPresentation, Face, Telegraph(shape, anchor, lock, ticks, cue), AreaHit(shape | Telegraph(i), hit), SweepVolume(shape, ticks, hit) (or ticks: Window("<name>"), below), Trace(origin, range, shape, hit) (one pellet, no penetration), Dash, Leap(speed, vertical, ticks), WaitTags(query, timeout), WaitWindow("<name>"), SpawnProjectile(archetype, speed, gravity, lifetime, radius, hit), ApplyEffect, EmitCue, Noise(loudness) and SetOutcome. Hits set the outcome bits Select branches test (HIT_ANY, HIT_NONE, HIT_WALL, INTERRUPTED). An ability also names costs: ["<instant effect>", ..] and charges: n (below).

Ability(
    id: "flagship-game:m14_fire",
    blocked: (any: ["flagship-game:state.reloading"]),
    input: Some("flagship-game:fire"),
    predict: Presentation,
    root: Repeat(until: Released, body: Serial([
        Trace(origin: Eyes, range: 200.0, shape: Ray, hit: (
            damage: Some((damage_type: "flagship-game:rifle", base: "10")), max_targets: 1)),
        BumpCounter("flagship-game:shots"),
        Noise(loudness: 48.0),
        Wait("0.2s"),
    ])),
)
  • Activation. An ability’s input names a declared Simulation Button action; the data stage checks it against the host’s input declarations (data stage). A player activates an ability on the press edge of its bound action in a real played input frame (PlayedInputFrames, the edge derived on the client’s input-tick timeline, keyed ActivationKey { input_tick, ordinal }); frames travel redundantly and underrun repeats and idle steps carry no edges, so a lost datagram does not drop a press and a stall does not repeat one (network time (docs/architecture/network-time.md)). A targeted or placed ability’s target or point rides the press as a command on its bound action (input spec §13.6: Target, Point or TargetPoint with a NetBinding, resolved through the player’s own entity ids): the activation takes the command’s key. A press without a command takes ordinal 4 or more (abilities::run::edge_ordinal: by how many abilities of lower id in the registry are bound to an action pressed in the same frame), so the owner numbers it as the server does. A press while a held channel (a chain that runs until Released) is active folds into it. D requests abilities with abilities::run::request_ability. Every activation passes §9.4’s checks: granted, alive, required, blocked, busy (two active at most), a free charge at the effective tick, the costs (CannotAfford), the target (range + 0.5 m for players, relation, line of sight; a Point ability needs its point), and ten activations a second per player. A refusal is an AbilityFailed record; a player’s becomes S2C::AbilityAck::Rejected { input_tick, ordinal, ability, reason }, and a player’s start S2C::AbilityAck::Accepted { input_tick, ordinal, instance, server_tick }.
  • Running. An ability ends on completion, a failed element, an interrupt (on_tags, on_damage_fraction, before_commit_only) or a cancel (another ability’s cancels, death, a lost grant). Ending stops its plays, resets the fields SetPresentation wrote to their defaults, revokes its owned tags and clears its telegraph. AbilityStarted, AbilityEnded and NoiseEmitted are stream records in CombatEvents.
  • Traces. Trace runs Pygmalion E’s lag-compensated query through AnimBridge::trace from the caster’s eyes socket along the view of the frame it runs on, rewound to the shooter’s view for players and unrewound for NPCs. A hit carries its HitDetail (region, bone, their indices, point, tick) into the damage event, the critical hint becomes manifold:hit.critical, and F’s HitReact takes the region and bone indices. Every trace is in TraceLog (256 records).
  • Telegraphs. A Telegraph publishes its shape, pose and resolve tick on manifold-gameplay:telegraph; AreaHit(shape: Telegraph(i)) resolves against exactly that shape. An NPC’s hit on a player is unrewound and hits only if the player was inside at the resolve tick and four ticks before (AnimBridge::position_at, E’s InputHistory). Clients draw the fill in input time (manifold_client_state::gameplay::telegraph_debug_lines, the manifold:telegraphs layer of both hosts’ world overlays), against server_tick_of_input.
  • Windows and melee. WaitWindow("swing") waits for the caster’s named animation window (Pygmalion E §14.2, AnimBridge::window_open; timeout, default 2 s, fails it). SweepVolume(ticks: Window("swing"), ..) is active while that window is open and ends when it closes (it fails if the window never opens within its timeout): with a shape it overlaps that shape each open tick, and without one it sweeps the held item’s blade volumes over the tick (AnimBridge::sweep_window, E §12.2), with targets rewound to a player’s view. Each target once per sweep.
  • Projectiles. SpawnProjectile spawns its archetype from the caster’s eye along a player’s view or toward an agent’s target, with a server-only Projectile. Each tick it flies its ballistic segment: characters with hit volumes through AnimBridge::trace (unrewound), other damageable entities through their bounds in A’s SpatialIndex, terrain through line_of_sight (which ends it). Hits apply nearest first up to max_targets, then it despawns; it also despawns at its lifetime or when its owner is gone. It stops at its first hit unless the hit names max_targets. Projectile is never saved, so a projectile’s archetype should not persist. Never predicted.
  • Movement. Dash and Leap write C0’s MotionOverride into the caster’s MotionOverrides (from the input after the activating one); a leap’s vertical speed applies once at its first step. They end early on HitTarget or a wall C reports (MotionOverrideReports, as HIT_WALL). A play with Animation root authority (Pygmalion E4) beats them step by step, so an authored charge is a OneShot whose clip moves the caster. Face fixes the ability’s facing and becomes C’s MoveIntent.look_at for agents. B never writes a character’s Position (a projectile’s is its own).
  • Cooldowns, charges and costs are effects, paid at Commit (on activation without one). A player’s cooldown starts at the commit’s effective tick: its input tick mapped through the smallest input-to-server offset of the last two seconds, at most 10 ticks back (AbilityState::effective_tick), so a press late after an underrun is not refused and the cooldown ends where the owner predicted. With charges: n the cooldown (an Aggregate effect with expire: One) blocks once it holds n stacks. costs are instant effects that only change resources; activation checks them without applying them (run::can_afford).

Equipment

Slots (data/equipment_slots/) and item kinds (data/items/) are data:

ItemKind(
    id: "flagship-game:m14",
    name: Key("flagship-game.item.m14.name"),
    model: "flagship:weapons/m14",
    grants: (abilities: ["flagship-game:m14_fire", "flagship-game:m14_reload"],
             stat_modifiers: []),
    block: None,
)

An explicit name is a message key its mod’s catalog defines (checked when the catalogs are known, as cargo mod validate knows them); without one the name is <mod>.item.<id>.name, or the humanized id when that message does not exist.

An item kind that is also a block names it (block: "<block key>"), which is what a pickup puts in a block inventory. Equipping grants the kind’s abilities with source Equipment(slot); unequipping removes them and cancels any of them that run. An archetype’s manifold-gameplay:loadout equips its starting items. G’s set-equipment op (equipment::set_equipment, api::SetEquipment) changes a slot; the console’s /entity equip <ref> <slot> <item|clear> (Admin, gameplay.equip) lowers to it on both hosts.

Prediction and feedback

The owner predicts its own presses (B §13): on its rising edge of an ability with predict: Presentation or predict: Local, the client runs the chain through the same flow on its input ticks, keeping each press in its PredictionLedger (abilities::predict, OwnerAnim::ledger). Presentation issues the one-shots as Pygmalion PredictionKeys keyed by the activation’s input tick and shows counter bumps over the server’s value until the server’s catches up. Local also predicts the owned tags, the cooldown instance from the commit’s input tick (so a press during it is not predicted), the cost overlay, and a Dash or Leap as a MotionOverride the owner’s integrator applies from the next input; the data stage refuses a Local ability whose cooldown duration or costs are not constants on predict: true stats.

The ledger reconciles by key. AbilityAck::Accepted keeps a press (one the server derived a few frames late, its own frame lost, is re-keyed and its cooldown moves with it); Rejected, a CommandLost for the command it rode, or an S2C::InputAck that passes it unanswered for rtt + 4 ticks rolls it back: its one-shots are cancelled at once, its tags, cooldown and overlay go, and its dash leaves the integrator and the replay ring, so the reconciler’s snap corrects the position. Cost overlays of answered presses go once the acknowledgement covers them. A press’s target or point is submitted with gameplay::submit_ability_command when the press is seen; the ticket becomes the press’s key. Hit markers and damage numbers are never predicted: the server sends S2C::DamageNumber { target, amount, region, flags, dealt } to the players involved in each hit (a cancelled hit is a marker with the CANCELLED flag), and clients keep them in HitMarkers.

The HUD draws them on both hosts. hit_feedback::HitFeedback (a client resource in manifold-client-state) is the presentation model: each frame the host calls hit_feedback::advance with the frame’s time and the player’s show_damage_numbers setting, then hit_feedback::draw_for_camera, and paints the list with manifold_ui_runtime::paint_hit_feedback in its egui pass, beside the hotbar bar. Only this player’s hits (dealt) draw:

  • The crosshair marker flashes on four diagonal arms around the screen centre: white for a hit, brass for a critical hit or one on manifold:head (which adds a ring), red and longer (0.45 s against 0.22 s) for a kill, dim for a cancelled hit. Hits arriving within 60 ms of a flash’s start are one shot (pellets, a sweep through several regions): they raise that flash’s kind instead of starting another.
  • Damage numbers rise 36 points over 0.9 s from 1.4 m above the target’s presented position, fading over the second half, and fan out in four lanes; one shot’s hits on one target merge into one number. The text is engine.hud.damage_number ({ $amount }, whole above ten, tenths below) or engine.hud.damage_blocked. Settings → Interface → Damage numbers turns them off; the marker stays.

The browser counts frames with a marker and the numbers drawn as window.__STATE__.hudHitMarkerFrames and hudDamageNumbersDrawn, which the hunt gate (e2e/assert-hunt-slice.mjs) requires after its hit.

Damage

The pipeline runs, per event:

StageBuilt inHooks may
admitdead, self, friendly, invulnerable, immune, the per-attacker windowCancel
gatherthe formula’s attacker part, the crit rollAdd, Multiply, Set, AddContextTag
convertConvert
mitigatearmour and per-element resistance, or the formula’s victim partAdd, Multiply, Set
pre_applyall of the above and Cancel (block, parry, shields)
applythrough incoming_damage into the receiver and the hit region’s resource; the ledger; the window
pre_deatha killing hit onlyCancel (the resource is set to the threshold + 1), ApplyEffect
post_hit, post_killknockback (a request)ApplyEffect, Reflect, Leech, Knockback, Cue
presentF’s HitReact, the hit cue (the type’s, else its element’s), the DamageNumber

Elements. Amounts are per element, summed in element order. An Expression formula’s gather runs once on the total and is shared in proportion to the bases; mitigate runs per element. Basic armour scales every element and each element’s resist_stat its own.

Hooks are data (DamageHook(stage, priority, side, when, action)), evaluated in the damage context (This is the victim; v.level and v.stacks are the holding effect’s; v.damage_amount is the stage’s amount, the applied one after apply). They live on effects (damage_hooks), on item kinds (grants: (damage_hooks: [..])) and on archetypes, which name sets from data/damage_hooks/ in manifold-gameplay:damage-hooks. Order: priority (highest first), side (attacker, direct, owner, victim), source (archetype sets, effects by instance, equipment by slot), index. manifold:damage.no_hooks skips them.

// A tank's block, on the effect a held shield ability applies.
DamageHook(stage: PreApply, priority: 100, side: Victim,
    when: Facing(of: This, towards: Attacker, max_degrees: 70.0),
    action: Multiply(value: Const(-0.8))),
// Armour by region, in an archetype's hook set.
DamageHook(stage: Mitigate, side: Victim,
    when: ContextTags((any: ["manifold:region.head"])), action: Multiply(value: -0.5)),
// Thorns, and lifesteal.
DamageHook(stage: PostHit, side: Victim, action: Reflect(fraction: 0.5, damage_type: "x:thorns")),
DamageHook(stage: PostHit, side: Attacker, action: Leech(fraction: 0.25, resource: "manifold:health")),

Reflect hits the attacker with a new REFLECTED event one level deeper, in the same pass; at depth 2 no post_* hook runs, so thorns cannot loop. Region names on a hit become <namespace>:region.<name> context tags; a critical volume hint becomes manifold:hit.critical. by_region gives a region its own resource, which a hit there also drains; death is judged on the main resource.

Healing (damage::heal::apply_heal) mirrors damage through incoming_healing, caps at the maximum, and emits HealApplied { amount, overheal, resource_after, source }. Its producers are effects’ heal and Leech. A dead target is healed only by an applies_to_dead effect.

The ledger (track_contributors) keeps, per owner root: damage; healing done to anyone already in it; and blocked, what that owner’s side cancelled or removed in pre_apply from this target’s hits.

Physics sources. Impacts are Daedalus D’s per-tick CharacterImpacts stream (manifold_player::impacts): characters’ landings and wall hits (struck and crush with Daedalus D1), and Daedalus F’s EntityImpacts on other damage receivers. The source pass reads it, never takes it (mods and presentation read it too): an impact takes the first matching rule of data/impact_damage/ (ImpactDamage(id, rules: [Rule(kinds: [Landing], damage_type, amount: "..v.impact_speed..")])) and credits the other side’s owner entity. A Struck impact from a field (ImpactOther::Field, an explosion’s push) is skipped: the explosion’s own hit damages that character, so it is not hurt twice. Explosions are damage::sources::PhysicsDamageSources, the seam F fills (not built): an ExplosionHit names a data/explosion_damage/ entry (ExplosionDamage(id, damage_type, amount: "..v.distance..", radius)), hitting receivers on the player and creature layers within the radius. Its owner is a contract::Owner with the owner’s entity when loaded, so an offline player’s explosion still credits them, and it carries F’s per-target occlusion factors (one ray per target, the one that scaled F’s impulse; a target not listed takes 1). Explosion damage types carry no knockback: F’s impulse field is the only push, and it reads knockback_resistance (the flagship’s flagship-game:explosion, used by flagship-game:tnt).

A cue may carry effect parameters by declared name (CueEvent.params, [("radius", r)]), which the router resolves to the effect’s slots in S2C::EffectCue.params, and a range of its own (CueEvent.range) that overrides its entry’s max_distance. The router clamps the range to [1, max_cue_range] (256 m) and lets each mod’s cues reach beyond 64 m at most 8 times per tick.

The flagship’s swing goes through the SkyBlock formula (game/flagship-game/data/damage_formulas/skyblock.ron, B §8.10).

Using it

From another crate (abilities, D’s stimuli, E’s threat, G’s console):

  • world::submit_damage(world, DamageEvent, emitter); DamageSpec::event builds an event from data.
  • damage::heal::apply_heal(world, reg, &HealEvent, tick, events).
  • manifold_player::impacts::CharacterImpacts (Daedalus D’s impact stream; F pushes EntityImpacts into it) and damage::sources::PhysicsDamageSources (F pushes ExplosionHits, with ExplosionHit::new and its owner, owner_entity and occlusion).
  • world::apply_effect(world, EffectSpec); effects::remove_instance.
  • GameplayExtensions::add_part(EffectPartDef) for a native effect part.
  • world::refresh_granted_modifiers after changing grants outside set-equipment.
  • world::grant_tag, revoke_tag, revoke_source; TagContainer::has, matches with a CompiledTagQuery.
  • world::stat_final, stat_breakdown; WorldReader for conditions.
  • presentation::set_field, bump_counter, reset_field.
  • CueOutbox::burst for a cue (set params and range on the returned event).
  • factions::relation, can_harm, same_party, pvp_between; GameplayExtensions::set_party_resolver.
  • loot::{roll_loot, LootRoll, RollSeed, open_container, set_visible_to, credited_players}; manifold_engine::visibility::HiddenFrom for any other entity only some players may see.
  • CombatEvents (the ordered per-tick streams) and DamageTraces (set all or inspected to record stage-by-stage traces).
  • AnimBridgeRes: trace (E’s lag-compensated query), issue and stop (one-shots), window_open and sweep_window (E3), eye, position_at, has_hit_set and line_of_sight for ability elements.
  • abilities::run::{request_ability, AbilityRequest} (D’s UseAbility), AbilityState (with effective_tick), TraceLog, AbilityMotion (movement records), ability_override, edge_ordinal, can_afford, charges_used.
  • abilities::projectile::Projectile (what flies).
  • abilities::predict::PredictionLedger (the owner’s predictions) and, on the client, gameplay::submit_ability_command and AbilityTickets.
  • equipment::{set_equipment, equipped, sync_grants, item_model}.

Limits

  • Guest hooks (Hook actions, gameplay:damage-*) and an effect’s execution are B6; the data refuses them with the phase.
  • Nothing fills PhysicsDamageSources until Daedalus F lands; impacts are landings and wall hits against terrain until Daedalus D1.
  • A player’s gameplay components are not in its player record yet, so a player’s persist effects, like its equipment, last the session.
  • SetData loot functions are refused: drops carry an item kind and a count, and item stack data does not exist yet.
  • Pickup is content: who may take a drop is DroppedItem::can_pick_up, and an item kind’s block says what a block inventory holds; the flagship’s inventory lives in its guest and needs G’s guest contract to read drops. A drop’s block (an item kind with block but no model) is not drawn as a block: it shows the drop archetype’s model.
  • Pellets and penetration and a projectile’s decaying shooter rewind are B7; Handler nodes are B6. The data refuses them with the phase.
  • No owner stat, tag or effect view replicates yet (Talos F’s audiences): the owner’s ledger predicts tags, cooldowns and costs from its own presses alone, and checks costs against no resource it cannot see.
  • An ability’s target or point is a command on its bound action, which must be a Button declaring that command kind (ActionDecl::command, a mod’s .command(..) in its ModDeclarations actions); gameplay::submit_ability_command refuses anything else before it is sent or predicted, because the server drops a frame whose command its action does not declare. The flagship’s abilities take no command (the M14 traces along the view).
  • The flagship’s swing stays an unrewound AreaHit until its clip carries swing window markers and the held item blade volumes.
  • Equipment does not persist yet: a player reloads with the loadout.
  • S2C::DamageNumber carries no hit point and no element, so a damage number rises from above the target’s feet and is coloured by its kind (hit, critical, kill, blocked), not its element. Damage taken draws no HUD feedback, and effect timers (EffectTimers) have no HUD row yet.
  • Knockback is a request stream (CombatEvents::knockback); the server routes players’ and agents’ requests into Daedalus D’s CharacterImpulses. It is not predicted: the owner sees one correction, carried on by the replicated CharacterCarry.