Custom Directions
Define game-specific movement types beyond the built-in compass directions. Portals, teleporters, climbing ropes, magic doors—any special navigation that doesn't fit NORTH/SOUTH/EAST/WEST.
Prerequisites
Before starting, you should:
Overview
We'll create custom directions by:
- Registering a new direction with its opposite
- Using the custom direction in connections
- Handling different opposite relationships
Step 1: Register a Custom Direction
Use registerDirection() to create a new direction with its opposite mapping.
import { createSpatialGraph, Direction } from '@motioneffector/spatial'
const graph = createSpatialGraph()
// Register a portal direction (self-opposite)
graph.registerDirection('PORTAL', { opposite: 'PORTAL' })
The opposite parameter defines what direction the reverse connection uses. For a portal, entering from either side uses the same direction.
Step 2: Use in Connections
Use the custom direction like any built-in direction.
graph.createNode('shrine')
graph.createNode('mirror-shrine')
// Connect via portal (bidirectional by default)
graph.connect('shrine', 'PORTAL' as any, 'mirror-shrine')
// Check the connection
const dest = graph.getDestination('shrine', 'PORTAL' as any)
console.log(dest) // 'mirror-shrine'
// Reverse also works (because opposite is 'PORTAL')
const reverse = graph.getDestination('mirror-shrine', 'PORTAL' as any)
console.log(reverse) // 'shrine'
Note: TypeScript requires as any for custom directions since they're not in the built-in Direction type.
Step 3: Handle Different Opposites
Custom directions can have three types of opposite relationships:
// Self-opposite: same direction both ways
graph.registerDirection('PORTAL', { opposite: 'PORTAL' })
// Paired opposites: ENTER and EXIT are distinct
graph.registerDirection('ENTER', { opposite: 'EXIT' })
// This also registers EXIT → ENTER automatically
// No opposite: one-way only
graph.registerDirection('WARP', { opposite: null })
Complete Example
import { createSpatialGraph, Direction } from '@motioneffector/spatial'
const graph = createSpatialGraph()
// === REGISTER CUSTOM DIRECTIONS ===
// Portal: magical doorway, works both ways
graph.registerDirection('PORTAL', { opposite: 'PORTAL' })
// Climb/Descend: for ropes and ladders
graph.registerDirection('CLIMB', { opposite: 'DESCEND' })
// Warp: one-way teleport trap
graph.registerDirection('WARP', { opposite: null })
// === CREATE NODES ===
graph.createNode('mage-tower')
graph.createNode('shadow-realm')
graph.createNode('cliff-base')
graph.createNode('cliff-top')
graph.createNode('trap-room')
graph.createNode('dungeon-start')
// === PORTAL CONNECTION ===
// Bidirectional magical portal
graph.connect('mage-tower', 'PORTAL' as any, 'shadow-realm')
const portalDest = graph.getDestination('mage-tower', 'PORTAL' as any)
console.log('Portal leads to:', portalDest) // 'shadow-realm'
// === CLIMB/DESCEND CONNECTION ===
// Rope climbing - bidirectional with different names
graph.connect('cliff-base', 'CLIMB' as any, 'cliff-top')
console.log(graph.getDestination('cliff-base', 'CLIMB' as any)) // 'cliff-top'
console.log(graph.getDestination('cliff-top', 'DESCEND' as any)) // 'cliff-base'
// === ONE-WAY WARP ===
// Trap that teleports player with no return
graph.connect('trap-room', 'WARP' as any, 'dungeon-start', {
bidirectional: false
})
console.log(graph.getDestination('trap-room', 'WARP' as any)) // 'dungeon-start'
console.log(graph.getDestination('dungeon-start', 'WARP' as any)) // null (no reverse)
// === PATHFINDING WORKS ===
const path = graph.findPath('cliff-base', 'cliff-top')
console.log('Path up cliff:', path) // ['cliff-base', 'cliff-top']
Variations
Ladder Direction
Vertical movement with custom names.
graph.registerDirection('CLIMB_UP', { opposite: 'CLIMB_DOWN' })
graph.connect('basement', 'CLIMB_UP' as any, 'attic')
// Player types "climb up" → goes to attic
// Player types "climb down" → returns to basement
Vehicle Entry
Enter/exit vehicles or mounts.
graph.registerDirection('BOARD', { opposite: 'DISEMBARK' })
graph.createNode('dock')
graph.createNode('ship-deck')
graph.connect('dock', 'BOARD' as any, 'ship-deck')
Conditional Portals
Combine custom directions with gates.
graph.registerDirection('PORTAL', { opposite: 'PORTAL' })
graph.connect('ancient-circle', 'PORTAL' as any, 'demon-realm')
// Only works if player has the crystal
graph.setGate('ancient-circle', 'PORTAL' as any, {
id: 'portal-seal',
locked: true,
keyId: 'void-crystal'
})
Troubleshooting
TypeScript errors with custom directions
Symptom: TypeScript complains about custom direction string.
Cause: Custom directions aren't in the Direction type.
Solution: Use as any type assertion:
graph.connect('a', 'CUSTOM' as any, 'b')
Or create a typed constant:
const PORTAL = 'PORTAL' as const
graph.registerDirection(PORTAL, { opposite: PORTAL })
graph.connect('a', PORTAL as any, 'b')
Reverse connection not created
Symptom: Bidirectional connection only works one way.
Cause: Custom direction has opposite: null.
Solution: Register the direction with a valid opposite:
// This won't create reverse connections
graph.registerDirection('TELEPORT', { opposite: null })
// This will
graph.registerDirection('TELEPORT', { opposite: 'TELEPORT' })
See Also
- Directions Concept — Understanding direction mechanics
- Direction Utilities API — Full method reference