Warman's AI companions are bots that fight alongside players. They need to navigate rooms, fight enemies, use portals, and follow the player, all inside a lockstep simulation where every action must be deterministic. The brain that drives them is a state machine controlled by WarScript, the built-in scripting language.
The Brain Architecture
Each companion gets an AICompanionBrain instance. The brain runs a WarScript script that gets called once per simulation tick with a on_tick(delta_time) function. The script issues high-level commands like "follow the owner," "attack the nearest enemy," or "use a portal."
That matters for lockstep. Because bots produce standard input commands that flow through the normal simulation tick pipeline, there's no special bot code path. The determinism guarantees that every client computes the same bot behaviour from the same inputs.
All Movement Goes Through A*
Bots don't cheat. They can't teleport, walk through walls, or hover over cliffs. Every movement decision goes through the same A* pathfinding that the world uses. When the brain decides to move somewhere, it computes an A* path from the bot's current position to the target, then advances along the path waypoint by waypoint each tick.
The movement output is a velocity vector, not a position. The brain converts the world-space direction to the camera-relative input space (matching the isometric camera rotation) so the bot's movement matches what a real player's WASD input would produce. Movement speed, wall collision, and ramp navigation all end up identical to a human player.
Combat Kiting
The combat system uses a simple state machine: approach, attack burst, reposition, repeat. When told to attack the nearest enemy, the brain finds the closest living enemy in the room, pathfinds to attack range, then fires a burst of attacks (around 8 ticks) while cycling through skill slots 1 through 4. After the burst, the bot pathfinds to a random position within a kite radius around the enemy and repeats.
Ranged bots behave differently. They try to maintain distance. If a ranged bot finds itself closer than melee range, it pathfinds away before attacking. The bot checks its equipped weapon type to decide the behaviour: if it's a RangedWeapon, it kites at range; otherwise, it closes to melee.
Portal Navigation and Memory
The portal system was the hardest part to get right. When the player's script tells the bot to explore (use a new portal), the brain searches the room for IBotApproachable interactables, portals that expose a walkable approach position inside their trigger zone.
The bot keeps a memory of visited portals. When using "use new portal," it filters out already-visited ones and picks the nearest unvisited portal. If all portals have been visited, it picks a random one as an escape hatch (but never the arrival portal, to prevent immediately going back). The arrival portal (the one the bot came through when entering the room) is tracked separately and always excluded from the first pass.
The approach itself works like a real player: the bot pathfinds to the portal's approach position (which is inside the trigger zone on walkable ground), then its movement velocity triggers the interaction detection system. The bot sets executeInteract = true each tick during the approach, so the moment it enters the trigger zone, the portal fires.
Stuck Detection
Bots can get stuck. Ramp edges, narrow passages between cliffs, and terrain geometry edges all cause situations where the bot has a valid path but can't actually make progress. The brain tracks the bot's position each tick and counts how many ticks it's been within a small threshold of the same spot.
After 15 stuck ticks (about 0.75 seconds at 20 tps), the brain tries recovery steps in order: first, skip to the next waypoint in the current path (maybe the approach angle is bad). If that doesn't help, recompute the entire path from the current position. If that still fails, clear the path entirely and let the script decide what to do next. This multi-stage approach handles most terrain edge cases without the bot needing to understand the geometry.