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; } }