472 lines
9.4 KiB
JavaScript
472 lines
9.4 KiB
JavaScript
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();
|