> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rathforge.com/llms.txt
> Use this file to discover all available pages before exploring further.

# forge-combat

> Native-health-backed HP, a damage validator chain, death/heal/revive, an NPC HP pool, and archetypes.

`forge-combat` owns the damage pipeline. As of v0.2 it no longer keeps its own HP ledger: health is
**HELIX native health**. Every combatant is an actor with an `HCharacterHealthComponent`, and
forge-combat drives it through the documented native globals (`DamageTarget`, `HealTarget`,
`GetHealth`, `IsDeadOrDying`, ...). Damage is still pure Lua and needs no cooked assets. The package
is decoupled from rewards: it fires events and lets content react.

<Note>
  This reverses the old ADR-0001 "FORGE HP is authoritative, native health neutralized via
  `SetEntityInvincible`" model. Native health is now the single source of truth; forge-combat is a
  thin, testable driver over it. Player downed/respawn UX lives in [forge-health](/packages/forge-health).
</Note>

## Concepts

* **Combat id.** A `character_id` for players; NPCs use their own id and register a pawn.
* **Native-backed vs entity pool.** Player pawns take damage through native `DamageTarget`. Native
  `DamageTarget` is **ignored on AI (NPC) pawns**, so an NPC can opt into a session-only Lua HP pool
  by passing `{ hp = N }` to `RegisterCombatant`. Pool combatants take damage/heal/death entirely in
  Lua; the pawn stays registered for position, FX and ragdoll. The pool is not persisted.
* **Damage validators.** A veto chain: registered validators (friendly fire, invulnerability) can
  block a hit before it lands. Pass `ctx = { attacker, spec }` to `DealDamage` to run it.
* **Health and armor are separate pools.** Native damage absorbs into armor before health; both are
  surfaced (armor gets its own HUD bar). HELIX has no runtime max-health setter, so `SetMaxHP` above
  the native base is represented as armor.
* **Death is an event.** For NPCs, reaching 0 fires `died` inline. For players, downed is enabled, so
  a player at 0 goes **downed** (not dead); real death arrives from the native machine (bleed-out,
  give-up, lethal) via the delegate bridge, avoiding a double `died`.

## Exports

### HP verbs

| Export       | Signature                              | Returns                                                                                    |
| ------------ | -------------------------------------- | ------------------------------------------------------------------------------------------ |
| `GetHP`      | `GetHP(id)`                            | `Result{ data = { current_hp, max_hp, pct, armor, max_armor } }`                           |
| `GetArmor`   | `GetArmor(id)`                         | `Result{ data = { armor, max_armor, pct } }`                                               |
| `IsDead`     | `IsDead(id)`                           | `boolean`                                                                                  |
| `SetMaxHP`   | `SetMaxHP(id, max)`                    | `Result` (max above native base becomes armor)                                             |
| `DealDamage` | `DealDamage(id, amount, source, ctx?)` | `Result{ data = { current_hp, died, ... } }` (runs validators; `ctx = { attacker, spec }`) |
| `Heal`       | `Heal(id, amount)`                     | `Result`                                                                                   |
| `Revive`     | `Revive(id, hp?)`                      | `Result` (downed → revive; fully dead → respawn)                                           |

### Combatants, validators, archetypes

| Export                          | Signature                              | Purpose                                                                                               |
| ------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `RegisterCombatant`             | `RegisterCombatant(id, pawn, opts?)`   | register an NPC pawn; `opts = { hp = N }` opts into the Lua HP pool                                   |
| `UnregisterCombatant`           | `UnregisterCombatant(id)`              | drop the pawn + pool entry                                                                            |
| `BindHealthEvents`              | `BindHealthEvents(id)`                 | bind native death/downed/revived delegates → forge-combat events (forge-health calls this per player) |
| `UnbindHealthEvents`            | `UnbindHealthEvents(id)`               | release the bind guard                                                                                |
| `RegisterDamageValidator`       | `RegisterDamageValidator(pkg, export)` | add a veto to the chain                                                                               |
| `SetArchetype` / `GetArchetype` | `SetArchetype(id, tag)`                | tag an id (carried in the `died` event for quest kill objectives)                                     |

```lua theme={null}
local fc = exports["forge-combat"]

-- NPC on the entity pool (native DamageTarget ignores AI pawns)
fc:RegisterCombatant("npc:dummy_1", pawn, { hp = 30 })
fc:SetArchetype("npc:dummy_1", "npc:training_dummy")
fc:DealDamage("npc:dummy_1", 8, casterCid)     -- Lua HP; fires damaged, then died at 0

-- Player pawn (native-backed): downed at 0, not dead
fc:DealDamage(targetCid, 25, casterCid, { attacker = casterCid, spec = { school = "fire" } })
```

## Events

| Event                         | Payload                                                          |
| ----------------------------- | ---------------------------------------------------------------- |
| `forge-combat:server:damaged` | `{ character_id, damage, current_hp, source }`                   |
| `forge-combat:server:healed`  | `{ character_id, healed, current_hp }`                           |
| `forge-combat:server:downed`  | `{ character_id }` (player entered native downed state)          |
| `forge-combat:server:revived` | `{ character_id, source? }`                                      |
| `forge-combat:server:died`    | `{ character_id, source, archetype? }`                           |
| `forge-combat:server:blocked` | `{ character_id, attacker, reason }`, a validator vetoed the hit |

<Note>
  Native access (`DamageTarget`, `GetHealth`, the death/downed delegates) goes through injectable
  seams (`H.PawnFor`, `H.Api.*`), so combat logic is unit-tested headless with an in-memory fake
  component, no live world needed.
</Note>

## Configuration

```lua theme={null}
Config = { DefaultMaxHP = 100, ReviveHP = 1 }
```

`forge-combat` ships **no content**; these are fallback tunables used when a native max can't be read.

## See also

* [forge-health](/packages/forge-health), the player downed screen + give-up respawn over native.
* [forge-gameplay-rules](/packages/forge-gameplay-rules), the runtime toggle of downed/AutoRespawn.
