Turn-based combat

Usage guide of combat.turns: a turn-based encounter engine with energy, enemy intents and move patterns, statuses with stacks, a data language of effects, driven fights, settings and defaults, collections, events, hooks and verbs.

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

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

Turn-based combat (combat.turns) #

What it is for #

combat.turns runs turn-based encounters: a hero who keeps their health between fights against enemies described as data. The sides take turns, the player spends energy each turn, enemies show what they will do next (their intent) and follow move patterns, and statuses carry stacks and wear off. Effects are a small data language that other kits extend (cards, relics, tactics). Every random draw follows the game seed, so a loaded game continues exactly. Cards, tactics and dice games all fit.

Turn it on #

Install the kit in a project and set its settings with the tools below; gripforge kit api combat.turns lists every setting, and gripforge docs combat-turns prints this guide offline.

gripforge_gamekit_install   { project_id: "gkp_…", id: "combat.turns" }
gripforge_gamekit_configure { project_id: "gkp_…", id: "combat.turns",
                              config: { "heroHealth": 80, "energyPerTurn": 3 } }

First example #

<!-- test: kits/game_engine/host/tests/combat_turns.rs::a_fight_runs_turn_by_turn --> A hero (30 health) starts a fight against a brute with thorns. A hit is answered by thorns, block soaks the enemy's hit and is lost when the player's turn begins, the fight can be saved and loaded in the middle, and a killing blow ends it as won.

combat.turns.start(["brute"], { kind: "elite" })          // true; brute: 20 hp, intent hit for 5, starts with thorns 2
combat.turns.apply([{ type: "apply_status", status: "strength", stacks: 2, target: "self" },
                    { type: "damage", amount: 4 }], { source: "hero", target: "e1" })
                                                          // brute 20 -> 14, thorns hit the hero 30 -> 28
combat.turns.apply([{ type: "block", amount: 3 }], { source: "hero" })
combat.turns.endTurn()                                    // the brute hits: hero 28 -> 26, block back to 0
combat.turns.energy()                                     // 3, turn 2

Settings and defaults #

SettingDefaultMeaning
heroHealth70{{s_heroHealth}}
heroNameHero{{s_heroName}}
energyPerTurn3{{s_energyPerTurn}}
stagetrueshow the fighters in the world: the hero in front of the camera, the enemies facing them

Services #

  • combat.turns (service): start(enemies, { allies, driven, hero }) answers false for an unknown enemy, active(), side(), turn(), hero(), combatants(), enemies(), allies(), energy(), spendEnergy, gainEnergy, endTurn(), heal, loseHp, addMaxHp, apply(effects, ctx), addStatus, damageOf, blockOf, describe(effects), movesOf(id), log(), stats(), end(result), registerEffect(type, fn), on(hook, fn), fire(hook), aim(request), aiming() and pick(target).
  • A kit that places the fighters itself (a grid, a board) starts a driven fight with fighters given as data and allies beside the hero: no intent is rolled, the caller plays every move with act(actor, move, target) and ends the enemy side with endTurn(), and end(result) closes a fight on a result decided outside.
  • The effect language: an effect is { type, target?, amount?, times?, status?, stacks? } with type among damage, block, apply_status, remove_status, heal, lose_hp, gain_energy, gain_max_hp or a type another kit registered; target is target, self, all_enemies, random_enemy, all or hero; amount is a number, or "x" for the energy an X-cost action spent.
  • Two collections hold your content. turn_enemies: id, name, kind (normal, elite, boss), hp (a number or a [min, max] range rolled on appearance), initiative, moves (each with id, name, intent, effects), pattern (mode cycle or random, opening, moves, weights, maxRepeat), start (statuses at the start), size, variant. turn_statuses: id, name, polarity (buff or debuff), decay (turn_start, turn_end, never), dealtAdd, blockAdd, dealtMult, takenMult, blockMult, reflect, keepBlock, tick. Ready-made statuses include vulnerable, weak, frail, strength, poison, thorns and growth.
  • Variables for the HUD: turns.active, turns.turn, turns.energy, turns.hp, turns.max_hp. The input turn_end (key KeyT) ends the player turn.

Events #

  • turns.started: a fight began, with enemies, allies, driven, kind, label.
  • turns.turn: a side starts its turn (side player or enemy, turn).
  • turns.acted: a fighter played a move through act in a driven fight.
  • turns.damaged: a hit landed (source, target, amount, blocked, hp).
  • turns.killed: a fighter fell (id, side, def).
  • turns.ended: the fight ended (result won or lost, turns, kind).
  • turns.hero_died: the hero fell outside a fight (an event, a cost).

Hooks and modding #

  • turns.start: a fight against known enemies is about to start; cancelled, start answers false and nothing is created.
  • turns.move: a fighter is about to play a move (actor, def, move); cancelled, it skips its move this turn (a stun) and the next intent is rolled as usual.
  • turns.effect: one effect is about to run (type, source, target, origin, amount, fight); amount is mutable, and cancelling skips that effect only.
  • turns.hit: one hit is about to land, after strength, weak and vulnerable and before block (source, target, amount, attack); amount is mutable, and cancelling means no hit.
  • turns.status.apply: stacks of a status are about to be added to (or, negative, taken from) a living fighter (target, status, stacks); stacks is mutable, and cancelling or 0 changes nothing (an immunity).
{ "id": "stun_immunity", "hook": "turns.status.apply", "match": { "status": "weak" }, "cancel": true }

Using it from content #

Script verbs: the action start_turn_fight (with enemies, a list of turn_enemies ids, and kind) starts a fight, and the condition turn_fight_active tells whether one is running.

{ "start_turn_fight": { "enemies": ["slime", "slime"] } }

When it does not do what you expect #

SymptomCause and fix
start answers falseAn enemy id is not in turn_enemies, a fight already runs, or a tap of turns.start cancelled it.
A status does nothingIts id is not a ready-made one nor in turn_statuses; an apply_status effect also needs a status and stacks.
Enemies do not play in a driven fightThat is by design: the caller plays each move with act and then calls endTurn().

Limits #

  • The kit has no cards: cards.deck adds them and registers their effect types on this one.
  • The encounter screen (enemies and their intents, the hero, energy, an End turn button) is drawn by the web display module; it steps aside for the screen of a kit that places the fighters itself.
  • It runs on the web player and the native Rust host; there is no Godot, Unity or Unreal adapter.

Versions #

Kit combat.turns 1.3.0 (fighters as data and driven fights since 1.3.0, 2026-10-22). It needs game.engine ^1.