# 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 ` lists every setting of a kit. ## Turn it on ```text 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? }`. 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: ```json { "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 | ```json { "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 |