Skip to content

How to Build: Manual Navigation

Auto-battle is great for idle play, but sometimes you want to do something. Maybe there's an enemy camp off the beaten path. Maybe you want to dodge a fight you can't win. Maybe you just want to explore. Manual navigation gives the player that agency — tap anywhere on the terrain, and the hero walks there.

This is the "active play" counterpart to auto-battle's "idle play." The two modes form a clean toggle: manual navigation disables auto-battle, and re-enabling auto-battle resumes road following. Let's wire it up.

From Tap to Movement

The flow when a player taps the terrain is straightforward:

  1. Raycast from the screen tap position down to the terrain surface
  2. Convert the hit point to hex coordinates
  3. Run A* pathfinding from the player's current hex to the target hex
  4. Feed the resulting path as a WaypointQueue to the movement system
  5. Disable auto-battle road following

Most of this infrastructure already exists in the movement and pathfinding systems. The handlePlayerTap system does the heavy lifting:

typescript
function handlePlayerTap(world: World): void

This system listens for pointer input, performs the raycast, runs A*, and sets the movement target. When it fires, it also flips AutoBattle.enabled to false. That single flag change is what switches the game from idle mode to active mode.

The Auto-Retaliate Toggle

Here's an interesting design question: what happens when an enemy aggros while you're manually walking somewhere? Two options:

  • Auto-retaliate ON: The hero stops and fights back. Standard behavior.
  • Auto-retaliate OFF: The hero keeps walking. The enemy still attacks, but the hero doesn't swing back. This lets the player try to run past threats.

This is controlled by a flag on the AutoBattle trait:

typescript
const AutoBattle = trait({
  enabled: true,
  roadPathIndex: 0,
  autoRetaliate: true,
})

When autoRetaliate is false and an enemy enters aggro range, the aggro system skips putting the player into combat state. The enemy still deals damage — you're running through a gauntlet, not turning invisible — but the player doesn't stop to fight.

Notice how this creates a genuine risk-reward decision. You can try to sprint past enemies to reach something valuable, but you'll take damage the whole way. That's the kind of emergent gameplay that makes manual navigation interesting beyond just "tap to walk."

Re-enabling Auto-Battle

The HUD menu bar includes an auto-battle toggle. When the player taps it after navigating manually, the system needs to figure out where on the road to resume from. You can't just jump to road index 0 — that would teleport progress back to the start.

typescript
function reEnableAutoBattle(player: Entity, world: World): void

This function reads the LevelRoadPath, gets the player's current Transform position, and finds the nearest road waypoint. It sets AutoBattle.enabled = true and roadPathIndex to that nearest index. The hero walks to the road and picks up the march from there.

typescript
function findNearestRoadWaypoint(
  roadPath: RoadWaypoint[],
  position: { x: number; z: number }
): number

A simple distance comparison loop over the road path array. Return the index of the closest waypoint.

What This System Depends On

  • Camera — provides pointer input for tap detection
  • Movement — consumes the movement target and waypoint queue
  • Pathfinding — A* path computation from current hex to target hex

We now have both halves of the player experience: idle auto-battle for passive play, and tap-to-move navigation for active play. The two modes complement each other, and switching between them is just a flag flip. Next, we need to give the player something to spend all that gold on.