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.
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.
| 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) |
{ "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_battleandend_tactics_battle, conditionstactics_battle_activeandtactics_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 |
{ "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 oftactics.startcancelled it, or a battle already runs.- An enemy never moves: its behaviour is
hold, or a tap oftactics.targetortactics.movecancelled 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.endcancelled 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.