Tactics grid

Usage guide of tactics.grid: turn-based tactical battles on a grid with terrains, levels, unit types, skills, a data-driven enemy AI, victory and defeat conditions, settings, data collections, service, events and hooks.

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

Also read offline — CLI: grip-forge docs tactics-gridMCP tool: gripforge_docs { "topic": "tactics-grid" }

Grid tactics battles (tactics.grid) #

What it is for #

A turn-based tactical RPG on a grid of cells. The player moves each unit along the cheapest path within its move points, strikes or plays a skill on a target in range, or waits; then the enemies play. Everything is data: terrains, maps, unit types and battles. The kit owns the board, the selection, the player's orders, the enemy phase, and the effect of cover and height on damage. The fights themselves (health, block, statuses, effects, hooks) are those of combat.turns, see Turn-based combat.

Turn it on #

gripforge_gamekit_install   { project_id: "gkp_…", id: "tactics.grid" }
gripforge_gamekit_configure { project_id: "gkp_…", id: "tactics.grid",
                              config: { "battle": "ford", "aiBehaviour": "charge", "heightBonus": 0.2 } }

It requires game.engine and combat.turns. Without documents the kit plays its own battle: a ford, six unit types, three allies against four raiders and their warlord. Your own content goes in four data collections, described below.

First example #

<!-- test: kits/game_engine/host/tests/parity.rs::rust_host_matches_the_typescript_traces (fixture tactics.grid.battle) --> The parity scenario plays the player phase of the default battle: it selects an ally, asks for the cells it reaches and its targets, moves it, arms a skill, plays it, and checks the refusals (a cell out of reach, a cell outside the map):

select    ["knight"]
reachable []                      -> cells the knight can walk to
moveTo    [4, 3]                  -> walks the cheapest path
arm       ["shield_wall"]
targets   []                      -> units the armed move may be played on
act       ["knight"]
moveTo    [9, 9]                  -> refused (not reachable)

Data collections #

A document of the same id replaces a built-in one.

CollectionHolds
tactics_terrainsa symbol per terrain, its move cost, passable, cover (damage taken x 1 - cover) and color. Built in: plain, road, forest, hill, shallow water, wall
tactics_mapswidth x height, rows of terrain symbols, optional levels (rows of digits), deploy cells for the allies, cellSize
tactics_unitsa registry of unit types: hp, move, moveType (foot or fly), initiative, kind (normal, elite, boss), moves (the first is the basic attack: range, target, uses, effects in the language of combat.turns), start statuses, ai, extends
tactics_battlesa map, the units of both sides (unit, side, at, leader), victory (rout, boss, survive) and defeat (rout, leader)
{ "id": "ford", "map": "river_ford",
  "units": [ { "unit": "knight", "side": "ally", "at": [1, 4] }, { "unit": "raider", "side": "enemy", "at": [6, 2] } ],
  "victory": { "type": "rout" }, "defeat": { "type": "rout" } }

Settings and defaults #

SettingDefaultMeaning
battleemptythe battle played at start (a tactics_battles id; empty: the first)
autoStarttruethe battle starts with the game; off: start() or the start_tactics_battle action
cellSize2metres per cell unless the map says otherwise
levelStep0.5metres between two levels
climbCost1extra move points per level climbed (flying units ignore it)
heightBonus0.2a hit from a higher cell deals this much more, from a lower one this much less
aiDelay0.6seconds between two enemy actions (0: all at once)
aiBehaviourchargewhat an enemy without an ai block does: charge or hold
aiPrefernearestwhich ally it goes for: nearest, weakest or strongest
autoEndTurntruethe player phase ends once every ally has acted
tilestrueone tile entity per cell in the world
cameraDistance, cameraHeight0fixed camera (0: from the size of the map)
showHighlightstruehighlight reachable cells, covered cells and targets
highlightMove, highlightAttack, highlightTarget#4c9bff, #ff5a46, #ffd23fhighlight colours
highlightOpacity0.35opacity of the highlight fill

Services, events, variables #

  • tactics.grid (service): start(battle?), end(result), active(), phase(), turn(), result(), battle(), map(), units(side?), unit(id), board(), highlights(), selected(), select(id), reachable(id?), targets(id?, move?), moveTo(x, y), arm(move), act(target, move?), wait(), endTurn(), next(), pick(x, y), cellAt(wx, wz), worldOf(x, y), terrains(), maps(), unitDocs(), battles().
  • pick(x, y) is the whole board interface in one call: an ally that may act is selected, a reachable cell moves the selected unit, a target of the armed move is struck, anything else clears.
  • Inputs: tactics.next (Tab), tactics.wait (X), tactics.cancel (Backspace).
  • Events: tactics.started, tactics.phase, tactics.selected, tactics.moved, tactics.acted, tactics.waited, tactics.fallen, tactics.ended (result, reason, turns, battle).
  • Variables: tactics.active, tactics.phase, tactics.turn, tactics.selected, tactics.allies, tactics.enemies, tactics.result, tactics.battle.
  • Script verbs: actions start_tactics_battle and end_tactics_battle, conditions tactics_battle_active and tactics_result.

A unit moves once and acts once. Victory (rout, boss, survive) and defeat (rout, leader) are checked after every action, fall and player turn; tactics.result holds won or lost, for example for the endings of game.endings.

Hooks and modding #

HookPayloadEffect
tactics.startbattle, map, allies, enemiescancelled: start() answers false, nothing is placed
tactics.moveunit, type, side, from, to, costcancelled: the unit stays
tactics.actionunit, side, move, target, cellcancelled: nothing is played and the action is not spent
tactics.targetunit, type, target, candidates, behaviourmutable target and behaviour; cancelled: the enemy waits
tactics.endresult, reason, turnscancelled: the battle goes on
{ "id": "hunt_mage", "hook": "tactics.target", "match": { "type": "raider" }, "set": { "target": "mage" } }

Every setting and the four collections are moddable. While the game runs, a change to tactics_terrains rebuilds the grid, a change to tactics_units updates the units on the board, and aiDelay, aiBehaviour, aiPrefer, heightBonus, climbCost and the highlight keys are read again.

Engines #

The web player and the native kit host run the same rules. The board view draws the grid with terrains and levels, the units with their health, the highlights, the moves of the selected unit, Wait and End turn. It is played with the cursor or a tap, with no direct movement, and outranks the encounter screen of combat.turns. A fixed camera looks over the grid unless a character kit owns the camera.

When it does not do what you expect #

  • start() answers false: a tap of tactics.start cancelled it, or a battle already runs.
  • An enemy never moves: its behaviour is hold, or a tap of tactics.target or tactics.move cancelled it.
  • A unit cannot enter a cell: the terrain is not passable, or the cost exceeds its move points.
  • The battle does not end: a tap of tactics.end cancelled the condition.

Limits #

  • Cells are walked by paths of the cheapest cost; there is no free movement.
  • A map is at most 64 x 64 cells and a battle at most 64 units.
  • Damage rules other than cover and height belong to combat.turns.

Versions #

Kit 1.0.0 (22 October 2026), built on combat.turns 1.3.0.