# 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](guide:combat-turns). ## Turn it on ```text 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 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): ```text 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. | Collection | Holds | |---|---| | `tactics_terrains` | a `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_maps` | `width` x `height`, `rows` of terrain symbols, optional `levels` (rows of digits), `deploy` cells for the allies, `cellSize` | | `tactics_units` | a 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_battles` | a `map`, the `units` of both sides (`unit`, `side`, `at`, `leader`), `victory` (rout, boss, survive) and `defeat` (rout, leader) | ```json { "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 | Setting | Default | Meaning | |---|---|---| | `battle` | empty | the battle played at start (a `tactics_battles` id; empty: the first) | | `autoStart` | `true` | the battle starts with the game; off: `start()` or the `start_tactics_battle` action | | `cellSize` | 2 | metres per cell unless the map says otherwise | | `levelStep` | 0.5 | metres between two levels | | `climbCost` | 1 | extra move points per level climbed (flying units ignore it) | | `heightBonus` | 0.2 | a hit from a higher cell deals this much more, from a lower one this much less | | `aiDelay` | 0.6 | seconds between two enemy actions (0: all at once) | | `aiBehaviour` | `charge` | what an enemy without an `ai` block does: `charge` or `hold` | | `aiPrefer` | `nearest` | which ally it goes for: `nearest`, `weakest` or `strongest` | | `autoEndTurn` | `true` | the player phase ends once every ally has acted | | `tiles` | `true` | one tile entity per cell in the world | | `cameraDistance`, `cameraHeight` | 0 | fixed camera (0: from the size of the map) | | `showHighlights` | `true` | highlight reachable cells, covered cells and targets | | `highlightMove`, `highlightAttack`, `highlightTarget` | `#4c9bff`, `#ff5a46`, `#ffd23f` | highlight colours | | `highlightOpacity` | 0.35 | opacity 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 | Hook | Payload | Effect | |---|---|---| | `tactics.start` | `battle`, `map`, `allies`, `enemies` | cancelled: `start()` answers false, nothing is placed | | `tactics.move` | `unit`, `type`, `side`, `from`, `to`, `cost` | cancelled: the unit stays | | `tactics.action` | `unit`, `side`, `move`, `target`, `cell` | cancelled: nothing is played and the action is not spent | | `tactics.target` | `unit`, `type`, `target`, `candidates`, `behaviour` | mutable `target` and `behaviour`; cancelled: the enemy waits | | `tactics.end` | `result`, `reason`, `turns` | cancelled: the battle goes on | ```json { "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.