class Position { constructor(x, y) { this.x = x; this.y = y; } getDistance(position, test) { return Math.sqrt(Math.pow(position.x - this.x, 2) + Math.pow(position.y - this.y, 2)); } } class Node { constructor(x, y) { this.position = new Position(x, y); this.g_cost = null; this.h_cost = null; this.f_cost = null; this.previous = null; this.isTraversable = true; } } class Map { constructor(width, height) { this.width = width; this.height = height; this.nodes = []; this.startNode = null; this.targetNode = null; this._initialize(); } _initialize() { for (let y = 0; y < this.height; y++) { let nodes = []; for (let x = 0; x < this.width; x++) { nodes.push(new Node(x, y)); } this.nodes.push(nodes); } } getNodeAt(x, y) { return this.nodes[y][x]; } getNeighbours(node, allowDiagonals = false) { let positions = [ new Position(node.position.x - 1, node.position.y), new Position(node.position.x + 1, node.position.y), new Position(node.position.x, node.position.y - 1), new Position(node.position.x, node.position.y + 1), ]; if (allowDiagonals) { positions = positions.concat( [ new Position(node.position.x - 1, node.position.y - 1), new Position(node.position.x + 1, node.position.y - 1), new Position(node.position.x - 1, node.position.y + 1), new Position(node.position.x + 1, node.position.y + 1), ] ); } const neighbours = []; for (const position of positions) { if (position.x < 0 || position.x >= this.width || position.y < 0 || position.y >= this.height) { continue; } neighbours.push(this.getNodeAt(position.x, position.y)); } return neighbours; } draw(canvas, unitWidth, unitHeight) { canvas.width = this.width * unitWidth; canvas.height = this.height * unitHeight; const context = canvas.getContext('2d'); for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { this.drawNode(x, y, this.getNodeAt(x, y).isTraversable ? COLOR_GROUND : COLOR_WALL, canvas); } } } drawNode(x, y, color, canvas) { const context = canvas.getContext('2d'); const unitWidth = canvas.width / this.width; const unitHeight = canvas.height / this.height; context.fillStyle = color; context.fillRect(x * unitWidth, y * unitHeight, unitWidth, unitHeight); } static loadFromLayout(layout) { const width = layout[0].length; const height = layout.length; const map = new Map(width, height); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { switch (layout[y].substr(x, 1)) { case '#': map.getNodeAt(x, y).isTraversable = false; break; case ' ': map.getNodeAt(x, y).isTraversable = true; break; case 'S': map.startNode = map.getNodeAt(x, y); map.startNode.isTraversable = true; break; case 'T': map.targetNode = map.getNodeAt(x, y); map.targetNode.isTraversable = true; break; } } } return map; } } class Pathfinder { constructor(map) { this.map = map; this.open = []; this.closed = []; this.paths = {}; } calculatePath(positionStart, positionTarget) { this.open.push(this.map.getNodeAt(positionStart.x, positionStart.y)); let count = 0; while (this.open.length > 0) { count++; if (count > 1000) { alert('Path could not be calculated!'); return; } let current = this._getCheapestNode(this.open); this._removeNodeFromOpen(current); this.closed.push(current); if (current.position.x === positionTarget.x && current.position.y === positionTarget.y) { break; } for (const neighbour of this.map.getNeighbours(current)) { if (!neighbour.isTraversable || this.closed.indexOf(neighbour) > -1) { continue; } const is_inside_open = this.open.indexOf(neighbour) > -1; const g_cost = neighbour.position.getDistance(positionStart) * 10; const h_cost = neighbour.position.getDistance(positionTarget) * 10; const f_cost = g_cost + h_cost; if (!is_inside_open) { neighbour.g_cost = g_cost; neighbour.h_cost = h_cost; neighbour.f_cost = f_cost; neighbour.previous = current; } if (!is_inside_open || current.position.getDistance(neighbour.position) < neighbour.position.getDistance(neighbour.previous.position)) { neighbour.f_cost = f_cost; neighbour.previous = current; if (!is_inside_open) { this.open.push(neighbour); } } } } } getPath(positionStart, positionTarget) { this.map.startNode = this.map.getNodeAt(positionStart.x, positionStart.y); this.map.targetNode = this.map.getNodeAt(positionTarget.x, positionTarget.y); let path = []; if (!this.map.getNodeAt(positionTarget.x, positionTarget.y).isTraversable) { return path; } this.calculatePath(positionStart, positionTarget); let targetNode = null; for (const node of this.closed) { if (node.position.x == positionTarget.x && node.position.y == positionTarget.y) { targetNode = node; break; } } let node = targetNode; while (node !== null) { path = [node].concat(path); node = node.previous; } return path; } reset() { this.open = []; this.closed = []; for (const node of this.map.nodes) { node.g_cost = null; node.h_cost = null; node.f_cost = null; node.previous = null; } this.map.startNode = null; this.map.targetNode = null; } _getCheapestNode(nodes) { let cheapest = nodes[0]; for (const node of nodes) { if (node.f_cost < cheapest.f_cost || (node.f_cost == cheapest.f_cost && node.h_cost < cheapest.h_cost)) { cheapest = node; } } return cheapest; } _removeNodeFromOpen(node) { const index = this.open.indexOf(node); if (index === -1) { return; } this.open = this.open.slice(0, index).concat(this.open.slice(index + 1)); } } class Player { constructor(x, y) { this.x = x; this.y = y; this.speed = 0.5; this.color = '#ff0000'; } draw(canvas, color = this.color) { const context = canvas.getContext('2d'); const width = canvas.width / GRID_WIDTH; const height = canvas.height / GRID_HEIGHT; context.fillStyle = color; context.fillRect(this.x * GRID_WIDTH, this.y * GRID_HEIGHT, GRID_WIDTH, GRID_HEIGHT); } } function calculateRoute(start, target) { const map = Map.loadFromLayout(layout); const canvas = document.getElementById('canvas'); if (!map.getNodeAt(target.x, target.y).isTraversable) { return []; } map.draw(canvas, GRID_WIDTH, GRID_HEIGHT); const pathfinder = new Pathfinder(map); const path = pathfinder.getPath(start, target); /* for (const node of path) { map.drawNode(node.position.x, node.position.y, '#ffff00', canvas); } */ return path; } function movePlayerTo(player, x, y, canvas) { const deltaX = x - player.x; const deltaY = y - player.y; player.draw(canvas, COLOR_GROUND); if (x !== player.x) { player.x += deltaX > 0 ? player.speed : -player.speed; if (deltaX < 0.1 && deltaX > -0.1) { player.x = x; } } if (y !== player.y) { player.y += deltaY > 0 ? player.speed : -player.speed; if (deltaY < 0.1 && deltaY > -0.1) { player.y = y; } } player.draw(canvas); } function movePlayerRoute(player, route, canvas) { let index = 1; const interval = setInterval( () => { movePlayerTo(player, route[index].position.x, route[index].position.y, canvas); if (route[index].position.x === player.x && route[index].position.y === player.y) { index++; } if (index === route.length) { clearInterval(interval); oldTarget = route[route.length - 1]; isTargeting = false; } }, 20 ); } function update() { map.draw(canvas, GRID_WIDTH, GRID_HEIGHT); player.draw(canvas); } const layout = [ ' ##########################', ' S # # # # #', ' # ###### # # ####### # #', ' # # # ##### #', ' # #### # # ####### # #', ' # # # # # ##### # #', ' # # #### # # ### # # # #', ' # # # # # # # # # #', ' # ###### # ### # # # # # #', ' # # # # # # #', '###################### # ##### #', '# # # # # # # ', '# # # # # ## # # # ##### # ', '# # # # # # # # # ', '# ##### ## ##### ##### # # ', '# # # # # # ', '##### ###### # ######### # ', '# # # ', '########################## ' ]; const GRID_WIDTH = 40; const GRID_HEIGHT = GRID_WIDTH; const COLOR_WALL = '#aaaaaa'; const COLOR_GROUND = '#005500'; const map = Map.loadFromLayout(layout); const canvas = document.getElementById('canvas'); let oldTarget = map.startNode.position; let isTargeting = false; const player = new Player(oldTarget.x, oldTarget.y); canvas.addEventListener( 'click', (event) => { if (isTargeting) { return; } isTargeting = true; const rect = canvas.getBoundingClientRect(); const x = parseInt((event.clientX - rect.x) / GRID_WIDTH); const y = parseInt((event.clientY - rect.y) / GRID_HEIGHT); if (player.x === x && player.y === y) { isTargeting = false; return; } const target = new Position(x, y); const route = calculateRoute(new Position(player.x, player.y), new Position(x, y)); if (route.length > 1) { movePlayerRoute(player, route, canvas); } else { isTargeting = false; } } ); const picker = document.getElementById('color-picker') picker.value = player.color; picker.addEventListener( 'input', (event) => { player.color = event.target.value; update(); } ); update();