Unit command

Real-time strategy and tactics units: click, box and control-group selection, move, attack-move, patrol, hold, gather, carry and build orders, formations, a shared walk grid, rally points and two hooks to change orders.

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

Also read offline — CLI: grip-forge docs units-commandMCP tool: gripforge_docs { "topic": "units-command" }

Selectable units that take orders (units.command) #

What it is for #

units.command is the core of a real-time strategy or tactics game. A unit is any entity that carries the component commandable (faction, stats, order queue, what it carries); a building, or anything else the player may select, carries selectable { faction, radius }. All the state of a unit lives in its components, so a save holds it and a load carries on exactly. The local player's faction selects (click, drag box, control groups) and orders (move, attack, gather, build...). The kit draws nothing itself: marks() tells every renderer what to mark (a ring under each selected unit or building, a health bar over each selected or damaged one). It leaves the models, the animation and the camera to the engine, and the rules of production, economy and opponents to the kits built on top of it.

Turn it on #

gripforge_gamekit_install   { project_id: "gkp_…", id: "units.command" }
gripforge_gamekit_configure { project_id: "gkp_…", id: "units.command",
                              config: { "faction": "blue", "spacing": 2.8 } }

The kit needs game.engine, combat.health, combat.damage and input.actions. Models come from the asset slot rts_unit (one per unit type, chosen by the variant).

First example #

<!-- test: kits/game_engine/host/tests/parity.rs::rust_host_matches_the_typescript_traces --> The fixture units.command.orders runs it: with faction set to blue, units are spawned, selected, then ordered. A spec needs a faction and a type; a missing one answers null instead of an id.

{ "op": "call", "service": "units.command", "method": "spawn",
  "args": [{ "faction": "blue", "type": "soldier", "health": 100, "speed": 4,
             "attack": { "damage": 10, "range": 1.5, "cooldown": 1 } }, [0.2, 0.1]] }
{ "op": "call", "service": "units.command", "method": "order",
  "args": [["e1", "e2", "e3"], { "type": "move", "point": [14.3, 12.7] }] }

The three units walk to the point in formation. Adding { "queue": true } as the third argument puts the order after the ones they already have.

How it plays #

  • Select: left click or tap on one of your units or buildings; Shift adds or removes. Drag a box to take every unit inside. Ctrl with a digit 1 to 5 sets a control group, the digit selects it. Z selects the whole army (workers left out). A click on an enemy shows it without ordering anything.
  • Order: right click, or tap with units selected. On an enemy: attack. On a resource node: workers gather. On one of your construction sites: workers build. On a drop-off: a loaded worker carries back. Elsewhere: move in formation. Shift queues. M, A and P arm move, attack-move and patrol for the next click (Escape disarms); S stops, H holds position. With buildings selected, a right click sets their rally point.
  • Mini-map: a left click moves the camera, a right click orders the selection there.

Settings and defaults #

SettingTypeDefaultMeaning
factiontextemptythe faction the local player commands (empty: the team of the player entity, or what a game kit sets)
cellSizenumber1metres per cell of the walk grid (0.5 to 4)
pickRadiusnumber1.2how far from a unit, beyond its own radius, a click still selects it (0 to 6)
spacingnumber2.4formation spacing in unit radii (1.5 to 6)
corpseSecondsnumber1.5a dead unit lies this long before it is removed (0 to 30)

A unit spec (spawn) takes faction, type, name, health, speed, radius (default 0.45, at least 0.2), sight (default 9), attack { damage, range, cooldown, windup?, projectileSpeed? }, worker { carry, seconds }, passive, supply, tags, slot, variant, scale and color (a mini-map marker).

Services, events, variables #

  • units.command (service):
    • units: player(), setPlayer(faction), spawn(spec, at, yaw?) (answers the id), units(faction?), unit(id), factionOf(id), hostile(a, b).
    • selection: selection(), select(ids, { add }), pick(point), selectAt(point, { add }), selectBox(corners, { add }), selectArmy(), clearSelection(), group(n), setGroup(n, ids?), recallGroup(n), marks().
    • orders: order(ids, order, { queue }) with an order { type, point?, target? } where type is move, attack_move, attack, hold, patrol, gather, return or build; command(point, target?, { queue, ids, type }) (what a click does); stop(ids?), hold(ids?); arm(mode), armed(), onArmed(mode, handler) (a placement kit takes the next click), removeArmed(id).
    • walk grid: blocked(x, z), clear(from, to), footprintFree(at, size, yaw?), nearestOpen(at), setBounds(min, max), bounds(), stats().
  • Events: units.spawned, units.selected { ids, faction }, units.ordered { ids, type, point?, target?, queued }, units.rally { ids, point, target }, units.group { group, ids }, units.idle, units.deposited { unit, faction, item, count }, units.lost. Listens to combat.damaged, combat.killed and minimap.clicked.
  • Variables: units.selected (how many are selected) and units.armed (the order waiting for a click).
  • Input actions: units.select, units.order, units.queue, units.group_mod, one group action per digit 1 to 5, units.cmd_move, units.cmd_attack, units.cmd_patrol, units.cmd_stop, units.cmd_hold, units.select_army, units.cancel; the player can remap them.

Rules #

  • Walking: the ground is a grid. Entities tagged blocks_path with a size block the cells under their footprint. A unit walks straight when the line to its goal is clear, else down a flow field computed once per goal and shared by the group. Units push each other apart, slide along walls and never enter a blocked cell.
  • Fighting: an idle unit with an attack takes on the nearest hostile it sees (fighters, then workers, then buildings) and goes home once it is too far. Attack-move and patrol fight on the way, hold only strikes what is in reach, a struck idle unit strikes back. Strikes go through combat.attacks when installed, else straight to combat.damage. Factions are hostile unless faction.relations says otherwise.
  • Gathering: a worker walks to a resource.nodes node, works for its seconds, carries the load to the nearest dropoff of its faction, banks it with economy.stockpile when a kit serves it, and goes back.
  • Building: a worker next to an entity with construction { progress, seconds } raises its progress; several build faster. The kit that placed the site finishes it.
  • Death: a unit killed through combat plays its death and is removed after corpseSeconds.

Hooks and modding #

  • Hook unit.order: payload { ids, unit, count, faction, type, point?, target?, queued }, asked once per order given to a group (player, script or AI) before any unit takes it. type is mutable between move, attack_move and patrol when the order goes to a point, queued is mutable, and cancelling gives no order and no units.ordered. A hold asks it too; a stop does not.
  • Hook unit.rally: payload { ids, building, count, faction, point, target? }, asked before the selected buildings get a rally point; cancelling leaves their rally points as they were.
  • A mod may change pickRadius, spacing and corpseSeconds.
{ "id": "march_fights", "hook": "unit.order", "match": { "type": "move" }, "set": { "type": "attack_move" } }

Engines #

The kit runs on the web and in the native Rust host. The web page draws the drag box, a ring and a health bar under each selected thing, and health bars over damaged units, as a ui.webui fragment.

When it does not do what you expect #

  • A click selects nothing: the unit's faction is not the one in faction (an enemy is shown but not selected), or the click is farther than pickRadius.
  • spawn answers null: the spec has no faction or no type, or the position is not an array of numbers.
  • Units do not walk around a building: it lacks the tag blocks_path or a size.
  • An order does nothing: a hook cancelled it, or none of the ids is a living unit of the player.
  • A worker does not gather: no resource.nodes node of its item, or no dropoff for its faction.

Limits #

Targets are the web and the native Rust host only. The kit does not train units, spend money or play an opponent: those belong to the kits that build on it.

Versions #

Kit 1.3.0 (adds marks()). Service units.command 1.0.0.