Hyperscape uses a discrete tile-based movement system inspired by RuneScape. The world is divided into tiles, and entities move one tile at a time in sync with server ticks.
The tile system lives in packages/shared/src/systems/shared/movement/TileSystem.ts.
Each entity with movement has a TileMovementState:
Copy
Ask AI
export interface TileMovementState { currentTile: TileCoord; // Current position path: TileCoord[]; // Queue of tiles to walk through pathIndex: number; // Current position in path isRunning: boolean; // Walk (2 tiles/tick) vs Run (4 tiles/tick) moveSeq: number; // Incremented on each new path previousTile: TileCoord | null; // Tile at START of current tick}
// OSRS-ACCURATE: Following a player means walking to their PREVIOUS tile,// creating the characteristic 1-tick trailing effect.previousTile: TileCoord | null;
When a click exceeds the BFS iteration limit (~44 tiles), the system automatically continues pathfinding. New fields added to TileMovementState:
Copy
Ask AI
// NEW fields added February 2026requestedDestination: TileCoord | null; // Original click targetlastPathPartial: boolean; // True if last BFS hit iteration limitnextSegmentPrecomputed: boolean; // True if next segment already sent
How It Works:
1
Initial Click
Player clicks far destination (60 tiles away). BFS runs for 8000 iterations, finds partial path to tile 44. Sets requestedDestination = (60, 60) and lastPathPartial = true
2
Path Continuation
When player reaches end of partial path, system automatically re-pathfinds from tile 44 toward original destination (60, 60). Finds next segment (tiles 44-88 or until destination). Continues until destination reached or unreachable.
3
Seamless Movement
Client receives isContinuation: true flag and appends new path without resetting interpolator. No stop-lurch at segment boundaries.
Server-Side Pre-Computation: Sends next segment 1 tick early to eliminate RTT/2 idle gap at segment boundaries.Client-Side Path Appending: Appends new path segments without resetting interpolator for continuous movement.
Breaking Change (PR #886): Player movement now uses BFS as the primary pathfinder instead of naive diagonal-first. This is a significant change that affects all player movement and combat positioning.
Hyperscape uses BFS (Breadth-First Search) as the primary pathfinding algorithm for all player movement, matching OSRS “smartpathing”:
Copy
Ask AI
// From BFSPathfinder.tsexport class BFSPathfinder { /** * Find optimal path from start to end using BFS. * BFS is the primary pathfinder for all player movement. */ findPath( start: TileCoord, end: TileCoord, isWalkable: WalkabilityChecker, ): TileCoord[]; /** * Multi-destination BFS: find shortest path to ANY destination tile. * Used for combat pathfinding — finds the closest reachable attack position. */ findPathToAny( start: TileCoord, destinations: TileCoord[], isWalkable: WalkabilityChecker, ): TileCoord[]; /** * Naive diagonal pathing — "dumb pathfinding" for NPC chase systems. * Moves diagonally toward target first, then cardinally. * NOT used for player movement (players use BFS). */ findNaivePath( start: TileCoord, end: TileCoord, isWalkable: WalkabilityChecker, ): TileCoord[];}
OSRS Accuracy: Player movement uses BFS (“smartpathing”) which finds optimal paths around obstacles. NPC chase movement uses naive diagonal pathing, which enables safespotting mechanics.
Ranged and magic attacks now require line of sight to prevent attacking through walls:
Copy
Ask AI
/** * OSRS-accurate Line of Sight check for ranged/magic combat. * Traces a line between two tiles using Bresenham's algorithm. */export function hasLineOfSight( from: TileCoord, to: TileCoord, hasBlockingFlags: (x: number, z: number) => boolean,): boolean;
Collision Mask:
Copy
Ask AI
// From CollisionFlags.tsexport const CollisionMask = { BLOCK_LOS: 0x02000000, // Walls, solid objects BLOCKED: 0x00200000, // Trees, rocks, stations BLOCKS_RANGED: BLOCK_LOS | BLOCKED, // Combined mask for LoS checks};
Usage in Combat:
Copy
Ask AI
// Check if ranged/magic attack has line of sightconst hasLoS = hasLineOfSight( attackerTile, targetTile, (x, z) => world.collision.hasFlags(x, z, CollisionMask.BLOCKS_RANGED),);if (!hasLoS) { // Attack blocked by wall/obstacle return;}
Breaking Change: Ranged and magic attacks now require line of sight. You cannot attack through walls even if the target is within Chebyshev range.
The tile system provides helpers to generate all valid attack positions:
Copy
Ask AI
/** * Generate all valid tiles for ranged/magic combat within Chebyshev range * that also have line of sight to the target. */export function getValidRangedTiles( target: TileCoord, combatRange: number, isWalkable: (tile: TileCoord) => boolean, hasBlockingFlags: (x: number, z: number) => boolean,): TileCoord[];/** * Generate all valid tiles for melee combat. * Range 1 (standard): cardinal only (N/S/E/W). * Range 2+ (halberd): all Chebyshev tiles within range. */export function getValidMeleeTiles( target: TileCoord, meleeRange: number, isWalkable: (tile: TileCoord) => boolean,): TileCoord[];
Example: Ranged Combat Positioning
Copy
Ask AI
// Player wants to attack target at (10, 10) with bow (range 7)const validTiles = getValidRangedTiles( { x: 10, z: 10 }, 7, (tile) => world.collision.isWalkable(tile), (x, z) => world.collision.hasFlags(x, z, CollisionMask.BLOCKS_RANGED),);// Returns all tiles 1-7 tiles away with clear line of sight// Example result: [(10,3), (10,4), (11,5), (9,5), ...] (tiles with LoS)// Excludes: tiles behind walls, blocked tiles, tiles at distance 0 or > 7
Example: Melee Combat Positioning
Copy
Ask AI
// Player wants to attack target at (10, 10) with sword (range 1)const validTiles = getValidMeleeTiles( { x: 10, z: 10 }, 1, (tile) => world.collision.isWalkable(tile),);// Returns cardinal tiles only: [(9,10), (11,10), (10,9), (10,11)]// Excludes diagonal tiles for range 1 (OSRS-accurate)
NPC chase movement uses naive diagonal pathing to enable safespotting:
Copy
Ask AI
// From ChasePathfinding.ts// NPCs use "dumb" diagonal-first pathing (OSRS-accurate for safespotting)const path = pathfinder.findNaivePath(mobTile, targetTile, isWalkable);// Naive algorithm:// 1. Move diagonally toward target (both X and Z changing)// 2. Then move cardinally (only X or Z) for remaining distance// 3. If blocked, do nothing (enables safespotting)
Safespotting: The naive diagonal pathing used by NPCs allows players to position themselves where mobs cannot reach them, matching OSRS mechanics. Players use BFS which finds optimal paths, while mobs use naive pathing which can be exploited.
For consistent face direction during resource gathering:
Copy
Ask AI
// Cardinal directions only (no diagonals)export function getCardinalAdjacentTiles( anchorTile: TileCoord, footprintX: number, footprintZ: number,): TileCoord[] { const adjacent: TileCoord[] = []; // Only N/S/E/W edges, no corners // ... (north, south, east, west edges) return adjacent;}// Determine face direction based on positionexport function getCardinalFaceDirection( playerTile: TileCoord, resourceAnchor: TileCoord, footprintX: number, footprintZ: number,): CardinalDirection | null { // Player north of resource → face South // Player east of resource → face West // Player south of resource → face North // Player west of resource → face East}
Hyperscape uses a unified CollisionMatrix for OSRS-accurate tile-based collision. The system handles static objects (trees, rocks, stations), entities (players, NPCs), and terrain (water, slopes).
// Check if tile is walkableif (world.collision.isWalkable(tileX, tileZ)) { // Safe to move here}// Check for static objects only (ignore entities)if (world.collision.hasFlags(tileX, tileZ, CollisionMask.BLOCKS_WALK)) { // Tree, rock, or station blocking}// Check if movement is blocked (includes walls)if (world.collision.isBlocked(fromX, fromZ, toX, toZ)) { // Cannot move from -> to}