Combat

Usage guide of combat.core and the combat family: the damage pipeline, health, shield, statuses, knockback, settings and defaults, verbs, hooks (combat.damage, combat.death, combat.heal, combat.shield, combat.status.apply), events, and a map of the melee, ranged, ability, duel, turn-based and fighting kits.

Source: module-kits:kits/combat_core/usage/en.mdView Markdown source

Also read offline — CLI: grip-forge docs combatMCP tool: gripforge_docs { "topic": "combat" }

Combat in your game (combat.* kits) #

Health, damage, shields, statuses and knockback shared by every fighting kit, and the family of kits that build melee, ranged, ability, duel and turn-based combat on top. Readable offline: gripforge docs combat, or the MCP tool gripforge_docs (topic combat).

What it is for #

combat.core holds the rules every fighting kit shares. Nothing in it swings a sword: weapons, enemies and bosses ask it to damage, heal or afflict, and boons plug modifiers into the same pipeline. The other kits decide how a fight looks and feels.

KitWhat it adds
combat.corehealth and teams, the damage pipeline, shield, invulnerability, knockback, statuses
combat.meleetimed melee strikes (arc, circle or line) with a windup shown as a telegraph, an active window that hits each target once, a recovery
weapons.loadoutthe player weapon as data: combo with a chain window, a special on cooldown, a dash attack, fists
combat.rangedaiming, hitscan or projectile fire, magazines and reload, fire rate, spread, recoil, damage falloff; for the player and for NPCs
weapons.arsenalthe guns the player owns, switching, ammo reserves, granted by inventory items
combat.projectileslinear or ballistic projectiles with swept contacts, trails, homing, bounces, area damage, chain jumps, on-hit status
combat.attackstarget-locked attacks for MOBA, RTS and tower defense: windup, melee contact or homing flight, damage exactly at impact
combat.abilitiesreusable abilities with cost, cooldown, conditions, cancellable windup, active and recovery, bounded targeting
combat.staminaa stamina gauge spent on attacks, dodges, guards and sprint, with an exhaustion state
combat.lockonlock the camera and body on the most relevant enemy, switch target, break on death or distance
combat.dueltwo fighters, frame-timed attacks, guard, rounds, reactive AI, local versus or solo
combat.turnsa turn-based encounter engine: energy, enemy intents, statuses with stacks
combat.moveset, combat.defense, combat.grabs, combat.hit_reactions, combat.weakpointsRust cores for fighting games: move clocks and input buffer, guard and parry, grabs and throws, stun and knockdown, damageable parts

Install what the game needs; gripforge kit api <id> lists every setting of a kit.

Turn it on #

gripforge_gamekit_install   { project_id: "gkp_…", id: "combat.core" }
gripforge_gamekit_configure { project_id: "gkp_…", id: "combat.core",
                              config: { "playerHealth": 120, "playerShield": 30, "friendlyFire": false } }
gripforge_gamekit_install   { project_id: "gkp_…", id: "combat.melee" }

The kits that fight require combat.core and game.engine, so installing one brings the other.

Settings of combat.core #

KeyDefaultMeaning
playerHealth60player maximum health
playerShield0player maximum shield (0: none)
hitInvulnerable0.5seconds of invulnerability after a player hit (status ticks do not grant it)
friendlyFirefalsedamage between members of the same team
knockbackScale1multiplies every knockback
knockbackSeconds0.12seconds a knockback takes
presentationProfile""id of a combat.core document in presentation_profiles; its optional healthBars block (version 1: width, height, inset, offset, minWidth, maxWidth, bossScale) sizes the enemy health bars in the Bevy player; empty: the default look

All of them, and the statuses collection, are moddable.

The damage pipeline #

combat.damage.damage({ source, target, amount, kind, tags?, knockback?, direction? }) answers { applied, amount, hp, max, killed, blocked? }.

  1. Blocked hits apply nothing and emit nothing: unknown (no entity or no health), dead, team (same team, unless friendlyFire; a request without a source is never blocked this way), invulnerable.
  2. Amount: (amount + sum of add) x product of multiply over the matching modifiers, in priority order then registration order, then the status multipliers; rounded to two decimals, never negative. addModifier({ id, owner, stage: "outgoing" | "incoming", entity?, kinds?, multiply?, add?, priority? }) returns a remover (a boon, armor).
  3. Shield takes the hit before health (combat.shield.absorbed, just before combat.damaged).
  4. Health decreases, knockback pushes the target, combat.damaged is emitted (also for a 0 amount).
  5. At 0 hp: tag dead, combat.killed; for the player also player.died. A non-player entity at 0 hp is removed on the next update.

Knockback goes along the request's direction, else from the source to the target on the ground plane; distance is knockback x knockbackScale. Dead targets are not pushed.

Services #

  • combat.health: health(id), register(id, max, team), setMax(id, max, heal?), heal(id, amount, source?), kill(id, source?) (a lethal script hit that ignores invulnerability and teams), alive, team, setInvulnerable(id, seconds), shield(id), addShield(id, amount, source?, max?), setShieldMax(id, max).
  • combat.damage: damage(request), addModifier(...), modifiers(), targetsNear(at, radius, team, exclude?) (live entities with health of another team, nearest first).
  • combat.status: define, defs, apply(target, status, { stacks?, source?, duration? }), remove, has, stacks, list, moveMultiplier(target).

Shield #

A shield lives in the component combat.shield = { value, max }. The maximum is the component's, else playerShield for the player, else 0. A game chooses who has one: playerShield, the shield key of an instance's combat component, or a max passed to addShield or the verb shield. It starts empty. Variables: player.shield, player.shield.max.

Statuses #

The statuses collection holds { id, name, duration, maxStacks, stacking: "refresh" | "stack", effects[], color? } with effects outgoing_mult, incoming_mult, move_mult (value^stacks), damage_over_time { amount, every } and delayed_damage { amount, after }. refresh resets the timer, stack adds stacks up to maxStacks. Status damage has kind status and no source. A document { "id": "new_status", "extends": "other" } of a game or a mod inherits an entry.

Using it from content #

Verbs and conditions work in scripts and rules:

{ "damage": { "instance": "barrel_1", "amount": 10 } }
{ "heal": { "entity": "player", "amount": 25 } }
{ "apply_status": { "entity": "player", "status": "weak", "stacks": 1 } }
{ "shield": { "entity": "player", "amount": 25 } }
{ "health_below": 0.3 }

entity is player, an entity id, or { tag }. An archetype component combat { health, team?, statuses?, shield? } registers a placed instance with combat.health (team enemy by default), gives it its shield and applies its statuses.

Hooks, events, registries #

WhatNameDetail
Hookcombat.damage{ source, target, sourceTeam, targetTeam, kind, tags, amount, add, multiply, knockback }; mutable amount, add, multiply, knockback; cancellable (blocked, nothing emitted). Every addModifier is a tap of this hook at its priority (bounded to +-1000). kill() does not ask
Hookcombat.death{ source, target, kind, amount, hp }; mutable hp: above 0 the target survives with that health. Also asked by kill() and when the player reaches 0 outside the pipeline
Hookcombat.heal{ source, target, amount }; mutable amount; cancellable
Hookcombat.shield{ source, target, amount, max }; mutable amount, max; cancellable
Hookcombat.status.apply{ source, target, status, stacks, duration }; mutable stacks, duration; cancellable (an immunity)
Eventscombat.damaged, combat.killed, combat.healed, combat.shield.gained, combat.shield.absorbed, combat.status.applied, combat.status.expired, player.diedcombat.damaged has source: null for statuses and scripts
Registrystatusesmoddable
{ "hook": "combat.damage", "match": { "targetTeam": "enemy", "kind": "attack" }, "mul": { "multiply": 2 } }

Invulnerability windows, status timers and knockbacks advance only while the game is playing. The save keeps the registered player, invulnerability windows, status timers and running knockbacks; modifiers and statuses added with define() are not saved, the kits that own them register them again.

Engines #

The rules are the same Rust implementation on the web, the native player, Godot, Unity and Unreal. The five Rust cores of the fighting family (combat.moveset, combat.defense, combat.grabs, combat.hit_reactions, combat.weakpoints) are also delivered as a C library and WebAssembly module: the engine detects the contacts and applies the results once, the core owns the timing and the rules.

When it does not do what you expect #

SymptomCause and fix
A hit applies nothingread blocked in the answer: team (same team without friendlyFire), invulnerable, dead
An enemy has no healthit was not registered: use an archetype combat component or combat.health.register
A boon does not change damageits modifier is on the wrong stage (outgoing matches the source, incoming the target)
A shield does nothingits maximum is 0: set playerShield, a shield on the instance, or a max in addShield
A status never stacksits stacking is refresh: use stack and a maxStacks above 1