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.
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.
| Kit | What it adds |
|---|---|
combat.core | health and teams, the damage pipeline, shield, invulnerability, knockback, statuses |
combat.melee | timed melee strikes (arc, circle or line) with a windup shown as a telegraph, an active window that hits each target once, a recovery |
weapons.loadout | the player weapon as data: combo with a chain window, a special on cooldown, a dash attack, fists |
combat.ranged | aiming, hitscan or projectile fire, magazines and reload, fire rate, spread, recoil, damage falloff; for the player and for NPCs |
weapons.arsenal | the guns the player owns, switching, ammo reserves, granted by inventory items |
combat.projectiles | linear or ballistic projectiles with swept contacts, trails, homing, bounces, area damage, chain jumps, on-hit status |
combat.attacks | target-locked attacks for MOBA, RTS and tower defense: windup, melee contact or homing flight, damage exactly at impact |
combat.abilities | reusable abilities with cost, cooldown, conditions, cancellable windup, active and recovery, bounded targeting |
combat.stamina | a stamina gauge spent on attacks, dodges, guards and sprint, with an exhaustion state |
combat.lockon | lock the camera and body on the most relevant enemy, switch target, break on death or distance |
combat.duel | two fighters, frame-timed attacks, guard, rounds, reactive AI, local versus or solo |
combat.turns | a turn-based encounter engine: energy, enemy intents, statuses with stacks |
combat.moveset, combat.defense, combat.grabs, combat.hit_reactions, combat.weakpoints | Rust 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 #
| Key | Default | Meaning |
|---|---|---|
playerHealth | 60 | player maximum health |
playerShield | 0 | player maximum shield (0: none) |
hitInvulnerable | 0.5 | seconds of invulnerability after a player hit (status ticks do not grant it) |
friendlyFire | false | damage between members of the same team |
knockbackScale | 1 | multiplies every knockback |
knockbackSeconds | 0.12 | seconds 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? }.
- Blocked hits apply nothing and emit nothing:
unknown(no entity or no health),dead,team(same team, unlessfriendlyFire; a request without a source is never blocked this way),invulnerable. - Amount:
(amount + sum of add) x product of multiplyover 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). - Shield takes the hit before health (
combat.shield.absorbed, just beforecombat.damaged). - Health decreases, knockback pushes the target,
combat.damagedis emitted (also for a 0 amount). - At 0 hp: tag
dead,combat.killed; for the player alsoplayer.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 lethalscripthit 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 #
| What | Name | Detail |
|---|---|---|
| Hook | combat.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 |
| Hook | combat.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 |
| Hook | combat.heal | { source, target, amount }; mutable amount; cancellable |
| Hook | combat.shield | { source, target, amount, max }; mutable amount, max; cancellable |
| Hook | combat.status.apply | { source, target, status, stacks, duration }; mutable stacks, duration; cancellable (an immunity) |
| Events | combat.damaged, combat.killed, combat.healed, combat.shield.gained, combat.shield.absorbed, combat.status.applied, combat.status.expired, player.died | combat.damaged has source: null for statuses and scripts |
| Registry | statuses | moddable |
{ "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 #
| Symptom | Cause and fix |
|---|---|
| A hit applies nothing | read blocked in the answer: team (same team without friendlyFire), invulnerable, dead |
| An enemy has no health | it was not registered: use an archetype combat component or combat.health.register |
| A boon does not change damage | its modifier is on the wrong stage (outgoing matches the source, incoming the target) |
| A shield does nothing | its maximum is 0: set playerShield, a shield on the instance, or a max in addShield |
| A status never stacks | its stacking is refresh: use stack and a maxStacks above 1 |