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
| Module | What |
|---|---|
api | ids, Relation, ModOp, Subject, ContextParam, HitDetail, HitKey, ActivationKey, AnimBridge and the combat event records. Compiles with --no-default-features for guests. |
data | the registries’ registration, engine entries, derive steps, GameplayConfig, named predicates |
tags, expr, stats, effects, damage, death, factions, loot, cues, presentation | each subsystem’s registry entry, components and pass |
damage::hooks, damage::heal, damage::sources | data hooks and hook sets, healing, Daedalus’s impacts and explosions |
abilities | ability chains (flow), the server runtime (run), telegraphs, projectiles (projectile), the owner’s prediction and its ledger (predict) |
equipment | slots, item kinds, loadouts and set-equipment |
components | component metadata both hosts register |
world | install_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.
| Registry | Side | Directory | Engine entries |
|---|---|---|---|
manifold:tag | Both | tags | the shared vocabulary (manifold:state.*, manifold:status.*, manifold:damage.* flags, manifold:region.<standard region>, manifold:hit.critical, manifold:cancel.* reasons) |
manifold:stat | Both | stats | the core pack: max_health, health, incoming_damage, incoming_healing, knockback_resistance, cooldown_rate |
manifold:predicate | Both | predicates | none |
manifold:element | Both | elements | manifold:generic |
manifold:damage-formula | Both | damage_formulas | manifold:basic |
manifold:damage-type | Both | damage_types | manifold:generic, manifold:command, manifold:kill |
manifold:effect | Both | effects | none |
manifold:cue | Both | cues | none |
manifold:faction | Both | factions | none |
manifold:loot-table | Server | loot_tables | none |
manifold:presentation-field | Both | presentation | manifold:dead |
manifold:gameplay-config | Both | gameplay | manifold:config (patch it) |
manifold:ability | Both | abilities | none |
manifold:equipment-slot | Both | equipment_slots | none |
manifold:item-kind | Both | items | none |
manifold:damage-hooks | Server | damage_hooks | none |
manifold:impact-damage | Server | impact_damage | none |
manifold:explosion-damage | Server | explosion_damage | none |
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 (thestatcontext set:Thisonly; 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;KeepFractionthen rescales it. The breakdown marks a derived base. - Scoped modifiers. A modifier with
scope: <condition>(in thedamageset, where the victim isThis) 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, andmanifold: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 scopedMore(-0.5)onincoming_damagewithDamageType((any: ["manifold:damage.is_fire"])). Breakdowns list scoped modifiers apart (conditional). - Granted modifiers. An item kind’s
grants: (stat_modifiers: [..])apply with sourceEquipment { slot }while it is equipped, and an ability’spassive: [..]with sourcePassive(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, andworld::refresh_granted_modifiersrebuilds them (after hooks, at the pending pass, and onset-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))
| Part | Does |
|---|---|
modifiers, grant_tags | while the instance is live (neither hidden nor inhibited) |
heal | a 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 |
requirements | an effect_apply condition checked before applying |
immunity | while live, blocks applications whose asset tags match (EffectBlocked { Immune }) |
cleanse | on application, removes the holder’s other instances whose asset tags match |
ongoing | while the holder’s tags do not match, the instance is inhibited: it keeps its timer, its modifiers and tags are revoked |
removal | TagAdded(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 |
stacking | Independent, 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_death | saved (§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
| Key | Stored as | Replication | Written in data | Brings |
|---|---|---|---|---|
manifold-gameplay:stats | Tracked<StatBlock> | server-only, persisted | (base: { "<stat>": 40.0 }) | Stats, ActiveEffects, manifold-gameplay:effects |
manifold-gameplay:effects | Tracked<EffectBlock> | server-only, persisted | added empty to archetypes with stats | its insert restores saved effects and loose tags |
manifold-gameplay:tags | Tracked<TagContainer> | server-only | (); archetype tags are granted | |
manifold-gameplay:damage-receiver | Tracked<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-hooks | Tracked<DamageHookSets> | server-only | (sets: ["<hook set>"]) | |
manifold-gameplay:faction | Tracked<FactionMember> | server-only | (id: Some("<faction>"), inherit_owner_faction: true) | EffectiveFaction |
manifold-gameplay:corpse | Tracked<Corpse> | public | inserted at death | |
manifold-gameplay:dropped-item | Tracked<DroppedItem> | public (a per-player drop reaches only its player), persisted | on manifold:item_drop | HiddenFrom while visible_to is set |
manifold-gameplay:loot-container | Tracked<LootContainer> | server-only, persisted | (table: "<loot table>", mode: Once | PerPlayer) | |
manifold-gameplay:presentation | bytes in the novel bag | public, Critical, relabelled for the owner | fields in data/presentation/ | |
manifold-gameplay:abilities | Tracked<AbilityGrants> | server-only | (granted: ["<ability>"]) | AbilityState |
manifold-gameplay:loadout | Tracked<Loadout> | server-only | (items: [("<slot>", "<item kind>")]) | AbilityState; fills the equipment |
manifold-gameplay:equipment | bytes in the novel bag | public, Critical, relabelled for the owner | through the loadout or set-equipment | |
manifold-gameplay:telegraph | Tracked<Telegraph> | public, Critical | written 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:
- its item kind’s
model(§12.5) when the client has that model; - else its own
manifold-model:model, which the game sets on themanifold:item_droparchetype (the flagship patches inflagship:items/loot_sackindata/archetypes/item_drop.update.ron); - 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):
| System | Does |
|---|---|
GameplayAbilityRequestSystem | clears 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 |
GameplayAbilityTickSystem | interrupts, then every active ability advanced once, in durable-id order; a completed chain’s Next starts the next ability in the same tick |
GameplayProjectileSystem | every projectile flies one tick and lands its ability’s hit |
GameplayEffectTickSystem | due periods, stacks that fall off, expiries, from the timing wheel |
GameplayDamageSourceSystem | Daedalus D’s impacts (read from CharacterImpacts) and F’s explosions (PhysicsDamageSources) into the damage queue |
GameplayDamageSystem | the 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 |
GameplayEffectApplySystem | parts’ operations from the other passes, queued effect applications (chained ones after their parent), then removal triggers and ongoing checks |
GameplayDeathSystem | Died (stream and observer trigger), loot and drops, Corpse, despawn at the deadline, revival |
GameplaySettleSystem | credits this tick’s healing to contributor ledgers, settles touched stats, refreshes factions, writes persist stats back to StatBlock and changed saved effects to EffectBlock |
GameplayCueFlushSystem | B’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 (aLootEntitytable under aLootGenericone). - Luck is the roll’s
luck_fromentity’sGameplayConfig.loot_luck_stat(the flagship’sflagship-game:magic_find): a pool rollsrolls + floor(bonus_rolls × luck)times (at most 16), an entry weighsmax(0, weight + floor(quality × luck)), andv.luckreads 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’sDamageLedger(the receiver needstrack_contributors: true):MinDamageFraction(f)credits players with at leastfof the recorded damage,AnyContributionany damage, healing or blocked damage,KillerOnlythe killer; a victim with no ledger entries credits its killer. Each recipient’s roll is theirs:RollSeed::for_recipientfolds theirPlayerUuidinto the id (pool picks andChancedraws alike), their luck is the roll’s, and they are itsKillerPlayer. Their drops are reserved for and visible to them only, untilexpire_ticks. - Containers.
manifold-gameplay:loot-containernames aLootGenerictable;loot::open_container(world, reg, container, opener, tick)rolls it for the opener (This, and their luck) and remembers the roll:Oncerolls for the first opener,PerPlayeronce 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’sroll-lootop, 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 areFriendly, whatever the PvP policy. A cue withaudience: Partygoes to the involved players and their party members. - PvP is
GameplayConfig.pvp:Offleaves players to the matrix (a team game’s factions);OptInmakes two playersHostilewhen both roots carrymanifold:state.pvpandFriendlyotherwise;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 shipsOff.
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
inputnames 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, keyedActivationKey { 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,PointorTargetPointwith aNetBinding, 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 untilReleased) is active folds into it. D requests abilities withabilities::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; aPointability needs its point), and ten activations a second per player. A refusal is anAbilityFailedrecord; a player’s becomesS2C::AbilityAck::Rejected { input_tick, ordinal, ability, reason }, and a player’s startS2C::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’scancels, death, a lost grant). Ending stops its plays, resets the fieldsSetPresentationwrote to their defaults, revokes its owned tags and clears its telegraph.AbilityStarted,AbilityEndedandNoiseEmittedare stream records inCombatEvents. - Traces.
Traceruns Pygmalion E’s lag-compensated query throughAnimBridge::tracefrom the caster’seyessocket along the view of the frame it runs on, rewound to the shooter’s view for players and unrewound for NPCs. A hit carries itsHitDetail(region, bone, their indices, point, tick) into the damage event, the critical hint becomesmanifold:hit.critical, and F’sHitReacttakes the region and bone indices. Every trace is inTraceLog(256 records). - Telegraphs. A
Telegraphpublishes its shape, pose and resolve tick onmanifold-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’sInputHistory). Clients draw the fill in input time (manifold_client_state::gameplay::telegraph_debug_lines, themanifold:telegraphslayer of both hosts’ world overlays), againstserver_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 itstimeout): with ashapeit 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.
SpawnProjectilespawns its archetype from the caster’s eye along a player’s view or toward an agent’s target, with a server-onlyProjectile. Each tick it flies its ballistic segment: characters with hit volumes throughAnimBridge::trace(unrewound), other damageable entities through their bounds in A’sSpatialIndex, terrain throughline_of_sight(which ends it). Hits apply nearest first up tomax_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 namesmax_targets.Projectileis never saved, so a projectile’s archetype should not persist. Never predicted. - Movement.
DashandLeapwrite C0’sMotionOverrideinto the caster’sMotionOverrides(from the input after the activating one); a leap’s vertical speed applies once at its first step. They end early onHitTargetor a wall C reports (MotionOverrideReports, asHIT_WALL). A play withAnimationroot authority (Pygmalion E4) beats them step by step, so an authored charge is aOneShotwhose clip moves the caster.Facefixes the ability’s facing and becomes C’sMoveIntent.look_atfor agents. B never writes a character’sPosition(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. Withcharges: nthe cooldown (anAggregateeffect withexpire: One) blocks once it holdsnstacks.costsare 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) orengine.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:
| Stage | Built in | Hooks may |
|---|---|---|
admit | dead, self, friendly, invulnerable, immune, the per-attacker window | Cancel |
gather | the formula’s attacker part, the crit roll | Add, Multiply, Set, AddContextTag |
convert | Convert | |
mitigate | armour and per-element resistance, or the formula’s victim part | Add, Multiply, Set |
pre_apply | all of the above and Cancel (block, parry, shields) | |
apply | through incoming_damage into the receiver and the hit region’s resource; the ledger; the window | |
pre_death | a killing hit only | Cancel (the resource is set to the threshold + 1), ApplyEffect |
post_hit, post_kill | knockback (a request) | ApplyEffect, Reflect, Leech, Knockback, Cue |
present | F’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::eventbuilds an event from data.damage::heal::apply_heal(world, reg, &HealEvent, tick, events).manifold_player::impacts::CharacterImpacts(Daedalus D’s impact stream; F pushesEntityImpacts into it) anddamage::sources::PhysicsDamageSources(F pushesExplosionHits, withExplosionHit::newand itsowner,owner_entityandocclusion).world::apply_effect(world, EffectSpec);effects::remove_instance.GameplayExtensions::add_part(EffectPartDef)for a native effect part.world::refresh_granted_modifiersafter changing grants outsideset-equipment.world::grant_tag,revoke_tag,revoke_source;TagContainer::has,matcheswith aCompiledTagQuery.world::stat_final,stat_breakdown;WorldReaderfor conditions.presentation::set_field,bump_counter,reset_field.CueOutbox::burstfor a cue (setparamsandrangeon 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::HiddenFromfor any other entity only some players may see.CombatEvents(the ordered per-tick streams) andDamageTraces(setallorinspectedto record stage-by-stage traces).AnimBridgeRes:trace(E’s lag-compensated query),issueandstop(one-shots),window_openandsweep_window(E3),eye,position_at,has_hit_setandline_of_sightfor ability elements.abilities::run::{request_ability, AbilityRequest}(D’sUseAbility),AbilityState(witheffective_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_commandandAbilityTickets.equipment::{set_equipment, equipped, sync_grants, item_model}.
Limits
- Guest hooks (
Hookactions,gameplay:damage-*) and an effect’sexecutionare B6; the data refuses them with the phase. - Nothing fills
PhysicsDamageSourcesuntil 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
persisteffects, like its equipment, last the session. SetDataloot 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’sblocksays 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 withblockbut nomodel) 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;
Handlernodes 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 itsModDeclarationsactions);gameplay::submit_ability_commandrefuses 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
AreaHituntil its clip carriesswingwindow markers and the held item blade volumes. - Equipment does not persist yet: a player reloads with the loadout.
S2C::DamageNumbercarries 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’sCharacterImpulses. It is not predicted: the owner sees one correction, carried on by the replicatedCharacterCarry.