278 lines
8.3 KiB
JavaScript
278 lines
8.3 KiB
JavaScript
function dirNameToValue(dirName) {
|
|
switch (dirName) {
|
|
case 'left':
|
|
return {x: -1, y: 0};
|
|
case 'right':
|
|
return {x: 1, y: 0};
|
|
case 'top':
|
|
return {x: 0, y: -1};
|
|
case 'bottom':
|
|
return {x: 0, y: 1};
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class Ghost extends MovingObject {
|
|
constructor(game, x, y) {
|
|
super(game, x, y);
|
|
this.speed = 2;
|
|
this.dir = {x: 0, y: 0};
|
|
|
|
this.lastDirChange = {x: Infinity, y: Infinity};
|
|
}
|
|
|
|
draw(ctx, conf) {
|
|
const size = conf.tileSize;
|
|
const cx = this.x * size;
|
|
const cy = this.y * size;
|
|
|
|
const left = -0.5 * size;
|
|
const top = -0.5 * size;
|
|
const right = 0.5 * size;
|
|
const base = 0.5 * size - 3;
|
|
|
|
const inc = size / 10;
|
|
const high = 3;
|
|
const low = -3;
|
|
|
|
ctx.save();
|
|
ctx.translate(cx, cy);
|
|
ctx.fillStyle = "#0f0";
|
|
ctx.beginPath();
|
|
|
|
// Draw body
|
|
ctx.moveTo(left, base);
|
|
|
|
ctx.quadraticCurveTo(left, top, 0, top);
|
|
ctx.quadraticCurveTo(right, top, right, base);
|
|
|
|
// Wavy things at the bottom
|
|
ctx.quadraticCurveTo(right - (inc * 1), base + high, right - (inc * 2), base);
|
|
ctx.quadraticCurveTo(right - (inc * 3), base + low, right - (inc * 4), base);
|
|
ctx.quadraticCurveTo(right - (inc * 5), base + high, right - (inc * 6), base);
|
|
ctx.quadraticCurveTo(right - (inc * 7), base + low, right - (inc * 8), base);
|
|
ctx.quadraticCurveTo(right - (inc * 9), base + high, right - (inc * 10), base);
|
|
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Draw eye background
|
|
ctx.fillStyle = "#fff";
|
|
ctx.beginPath();
|
|
|
|
ctx.arc(left + size * .3, top + size * .3, size / 6, 0, 2 * Math.PI);
|
|
ctx.arc(right - size * .3, top + size * .3, size / 6, 0, 2 * Math.PI);
|
|
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Draw eyes
|
|
const f = size / 12;
|
|
const offset = {
|
|
x: this.dir.x * f,
|
|
y: this.dir.y * f,
|
|
};
|
|
|
|
ctx.fillStyle = "#000";
|
|
ctx.beginPath();
|
|
|
|
ctx.arc(left + size * .3 + offset.x, top + size * .3 + offset.y, size / 15, 0, 2 * Math.PI);
|
|
ctx.arc(right - size * .3 + offset.x, top + size * .3 + offset.y, size / 15, 0, 2 * Math.PI);
|
|
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
update(deltaTime) {
|
|
const nextTile = this.getNextTile(0, 0);
|
|
if (nextTile.distanceX <= 0.5 && nextTile.distanceY <= 0.5 && !(nextTile.tile.x === this.lastDirChange.x && nextTile.tile.y === this.lastDirChange.y)) {
|
|
const surroundings = this.game.map.getSurroundingTiles(nextTile.tile.x, nextTile.tile.y);
|
|
const nonSolid = Object.entries(surroundings).filter(surrounding => !surrounding[1].solid);
|
|
|
|
if (nonSolid.length === 1) {
|
|
this.changeDir({x: -this.dir.x, y: -this.dir.y});
|
|
this.lastDirChange = nextTile.tile;
|
|
} else {
|
|
const possibleDirections = [];
|
|
|
|
nonSolid.forEach(entry => {
|
|
const dir = dirNameToValue(entry[0]);
|
|
if (dir.x === -this.dir.x && dir.y === -this.dir.y)
|
|
return;
|
|
possibleDirections.push(dir);
|
|
});
|
|
|
|
if (possibleDirections.length === 1) {
|
|
this.changeDir(possibleDirections[0]);
|
|
this.lastDirChange = nextTile.tile;
|
|
} else if (possibleDirections.length === 2) {
|
|
const newDirection = possibleDirections.find(dir => dir.x !== -this.dir.x || dir.y !== -this.dir.y);
|
|
this.changeDir(newDirection);
|
|
this.lastDirChange = nextTile.tile;
|
|
} else {
|
|
let bestScore = -Infinity;
|
|
let bestDir;
|
|
|
|
const player = this.game.getPlayer().getNextTile(0, 0).tile;
|
|
possibleDirections.forEach(dir => {
|
|
const score = this.getDirectionValue(nextTile.tile, dir, player);
|
|
if (score > bestScore) {
|
|
bestDir = dir;
|
|
bestScore = score;
|
|
}
|
|
});
|
|
|
|
if (bestDir) {
|
|
this.changeDir(bestDir);
|
|
this.lastDirChange = nextTile.tile;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
super.update(deltaTime);
|
|
}
|
|
|
|
getDirectionValue(tile, dir, goal) {
|
|
const newTile = this.game.map.getTile(tile.x + dir.x, tile.y + dir.y);
|
|
|
|
if (!newTile || newTile.solid) {
|
|
return -Infinity;
|
|
}
|
|
|
|
const pathFinder = new Pathfinder(this.game.map.tiles);
|
|
const path = pathFinder.findShortestPath(newTile, goal);
|
|
|
|
// const curDistance = (tile.x - goal.x) ** 2 + (tile.y - goal.y) ** 2;
|
|
// const newDistance = (newTile.x - goal.x) ** 2 + (newTile.y - goal.y) ** 2;
|
|
//
|
|
// let score = curDistance - newDistance;
|
|
let score = 1 / path.length * 10;
|
|
|
|
if (this.dir.x === -dir.x || this.dir.y === -dir.y) {
|
|
score -= Math.abs(score) / 2;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
}
|
|
|
|
class Pathfinder {
|
|
constructor(grid) {
|
|
this.grid = [];
|
|
this.queue = [];
|
|
|
|
grid.forEach((col, x) => {
|
|
this.grid.push([]);
|
|
col.forEach(tile => {
|
|
this.grid[x].push(tile.solid ? 2 : 0);
|
|
});
|
|
});
|
|
}
|
|
|
|
findShortestPath(start, end) {
|
|
this.grid[end.x][end.y] = 10;
|
|
|
|
const location = {
|
|
x: start.x,
|
|
y: start.y,
|
|
path: [],
|
|
status: 1,
|
|
};
|
|
|
|
this.queue = [location];
|
|
|
|
return this.loop();
|
|
}
|
|
|
|
loop() {
|
|
while (this.queue.length > 0) {
|
|
const current = this.queue.shift();
|
|
|
|
let newLocation;
|
|
|
|
// Explore north
|
|
newLocation = this.exploreInDirection(current, {x: 0, y: -1});
|
|
if (newLocation.status === 10) {
|
|
return newLocation.path;
|
|
} else if (newLocation.status === 5) {
|
|
this.queue.push(newLocation);
|
|
}
|
|
|
|
// Explore east
|
|
newLocation = this.exploreInDirection(current, {x: 1, y: 0});
|
|
if (newLocation.status === 10) {
|
|
return newLocation.path;
|
|
} else if (newLocation.status === 5) {
|
|
this.queue.push(newLocation);
|
|
}
|
|
|
|
// Explore south
|
|
newLocation = this.exploreInDirection(current, {x: 0, y: 1});
|
|
if (newLocation.status === 10) {
|
|
return newLocation.path;
|
|
} else if (newLocation.status === 5) {
|
|
this.queue.push(newLocation);
|
|
}
|
|
|
|
// Explore west
|
|
newLocation = this.exploreInDirection(current, {x: -1, y: 0});
|
|
if (newLocation.status === 10) {
|
|
return newLocation.path;
|
|
} else if (newLocation.status === 5) {
|
|
this.queue.push(newLocation);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
locationStatus(location) {
|
|
const width = this.grid.length,
|
|
height = this.grid[0].length;
|
|
const x = location.x,
|
|
y = location.y;
|
|
|
|
if (x < 0 || x >= width || y < 0 || y >= height) {
|
|
return 7; // Invalid
|
|
}
|
|
if (this.grid[x][y] === 10) {
|
|
return 10; // Goal
|
|
}
|
|
if (this.grid[x][y] !== 0) {
|
|
return 6; // Blocked: location is either an obstacle or has already been visited
|
|
} else {
|
|
return 5; // Valid
|
|
}
|
|
}
|
|
|
|
exploreInDirection(location, direction) {
|
|
const newPath = location.path.slice();
|
|
|
|
const x = location.x + direction.x,
|
|
y = location.y + direction.y;
|
|
|
|
newPath.push({
|
|
x: x,
|
|
y: y,
|
|
direction: direction,
|
|
});
|
|
|
|
const newLocation = {
|
|
x: x,
|
|
y: y,
|
|
path: newPath,
|
|
status: -1, // unknown status
|
|
};
|
|
newLocation.status = this.locationStatus(newLocation);
|
|
|
|
if (newLocation.status === 5) {
|
|
this.grid[x][y] = 4; // Set as visited
|
|
}
|
|
|
|
return newLocation;
|
|
}
|
|
} |