initial commit
This commit is contained in:
10
DOM.js
Normal file
10
DOM.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Node.prototype.createChild = function(name, className) {
|
||||||
|
const el = document.createElement(name);
|
||||||
|
|
||||||
|
if(typeof className === 'string') {
|
||||||
|
el.classList.add(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appendChild(el);
|
||||||
|
return el;
|
||||||
|
};
|
114
Game.js
Normal file
114
Game.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
class Game {
|
||||||
|
constructor() {
|
||||||
|
this.canvas = document.body.createChild('canvas', 'pacman-game');
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
|
||||||
|
this.loadMap();
|
||||||
|
this.onResize();
|
||||||
|
|
||||||
|
this.gameObjects = [];
|
||||||
|
|
||||||
|
this.paused = false;
|
||||||
|
this.gameOver = false;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.gameObjects.push(new Player(this, this.map.spawn.x + 0.5, this.map.spawn.y + 0.5));
|
||||||
|
|
||||||
|
this.gameObjects.push(new Ghost(this, 5.5, 6.5));
|
||||||
|
// this.gameObjects.push(new Ghost(this, 3.5, 20.5));
|
||||||
|
// this.gameObjects.push(new Ghost(this, 10.5, 7.5));
|
||||||
|
// this.gameObjects.push(new Ghost(this, 19.5, 12.5));
|
||||||
|
|
||||||
|
this.registerKeyListeners();
|
||||||
|
this.registerWindowListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerKeyListeners() {
|
||||||
|
window.addEventListener('keydown', (event) => {
|
||||||
|
const key = event.key;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
this.getPlayer().changeDir({x: -1, y: 0});
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
this.getPlayer().changeDir({x: 0, y: -1});
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
this.getPlayer().changeDir({x: 1, y: 0});
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
this.getPlayer().changeDir({x: 0, y: 1});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerWindowListeners() {
|
||||||
|
window.addEventListener('resize', (event) => {
|
||||||
|
this.onResize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize() {
|
||||||
|
this.calculateConf(window.innerWidth, window.innerHeight);
|
||||||
|
|
||||||
|
this.canvas.width = this.conf.tileSize * this.map.width;
|
||||||
|
this.canvas.height = this.conf.tileSize * this.map.height;
|
||||||
|
|
||||||
|
this.canvas.style.width = this.canvas.width + 'px';
|
||||||
|
this.canvas.style.height = this.canvas.height + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMap() {
|
||||||
|
const mapLoader = new MapLoader("levels/level1.pacmap");
|
||||||
|
mapLoader.start();
|
||||||
|
this.map = mapLoader.getMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateConf(width, height) {
|
||||||
|
this.conf = {
|
||||||
|
tileSize: calculateTileSize(width, height, this.map.width, this.map.height),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getGameObjectsByType(type) {
|
||||||
|
return this.gameObjects.filter(object => object instanceof type);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayer() {
|
||||||
|
return this.getGameObjectsByType(Player)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.clientWidth, this.canvas.clientHeight);
|
||||||
|
|
||||||
|
this.map.draw(this.ctx, this.conf);
|
||||||
|
|
||||||
|
this.gameObjects.forEach(object => {
|
||||||
|
object.draw(this.ctx, this.conf);
|
||||||
|
});
|
||||||
|
|
||||||
|
requestAnimationFrame(() => this.draw());
|
||||||
|
}
|
||||||
|
|
||||||
|
update(lastUpdate) {
|
||||||
|
const updateTime = Date.now();
|
||||||
|
const deltaTime = updateTime - lastUpdate || 16;
|
||||||
|
|
||||||
|
this.gameObjects.forEach(object => {
|
||||||
|
object.update(deltaTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.paused && !this.gameOver)
|
||||||
|
requestAnimationFrame(() => this.update(updateTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.draw();
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
}
|
47
GameMap.js
Normal file
47
GameMap.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
class GameMap {
|
||||||
|
constructor(tiles, spawn) {
|
||||||
|
this.width = tiles.length;
|
||||||
|
this.height = tiles[0].length;
|
||||||
|
this.tiles = tiles;
|
||||||
|
this.spawn = spawn;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.tiles.forEach(row => {
|
||||||
|
row.forEach(tile => {
|
||||||
|
if(tile.solid) {
|
||||||
|
tile.lookAtNeighbours(this.getSurroundingTiles(tile.x, tile.y));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx, conf) {
|
||||||
|
this.tiles.forEach(col => {
|
||||||
|
col.forEach(tile => {
|
||||||
|
tile.draw(ctx, conf);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getTile(x, y) {
|
||||||
|
try {
|
||||||
|
return this.tiles[x][y];
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSurroundingTiles(x, y) {
|
||||||
|
const left = this.getTile(x - 1, y);
|
||||||
|
const right = this.getTile(x + 1, y);
|
||||||
|
const top = this.getTile(x, y - 1);
|
||||||
|
const bottom = this.getTile(x, y + 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
left, top, right, bottom
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
10
GameObject.js
Normal file
10
GameObject.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class GameObject {
|
||||||
|
constructor(game, x, y) {
|
||||||
|
this.game = game;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx, conf) {}
|
||||||
|
update() {}
|
||||||
|
}
|
278
Ghost.js
Normal file
278
Ghost.js
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
87
MapLoader.js
Normal file
87
MapLoader.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
class MapLoader {
|
||||||
|
constructor(fileName) {
|
||||||
|
this.fileName = fileName;
|
||||||
|
this.fileContent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.readFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
readFile() {
|
||||||
|
const rawFile = new XMLHttpRequest();
|
||||||
|
rawFile.open("GET", this.fileName, false);
|
||||||
|
rawFile.addEventListener('readystatechange', () => {
|
||||||
|
if(rawFile.readyState === 4) {
|
||||||
|
if(rawFile.status === 200 || rawFile.status === 0) {
|
||||||
|
const content = rawFile.responseText;
|
||||||
|
console.log(content);
|
||||||
|
this.fileContent = content;
|
||||||
|
|
||||||
|
this.loadMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rawFile.send(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMap() {
|
||||||
|
const tiles = [];
|
||||||
|
const mapRaw = this.fileContent;
|
||||||
|
|
||||||
|
const rows = mapRaw.split(/\r?\n/g);
|
||||||
|
|
||||||
|
if(rows.length === 0) {
|
||||||
|
throw LoadingException;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = rows[0].length,
|
||||||
|
height = rows.length;
|
||||||
|
let spawn;
|
||||||
|
|
||||||
|
for(let x = 0; x < width; x++) {
|
||||||
|
tiles.push([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.forEach((row, y) => {
|
||||||
|
if(row.length !== width) {
|
||||||
|
throw `Row length not consistent. Expected ${width}, got ${row.length} at row ${y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let x = 0; x < row.length; x++) {
|
||||||
|
const tileRaw = row[x];
|
||||||
|
let tile;
|
||||||
|
|
||||||
|
switch (tileRaw) {
|
||||||
|
case 'X':
|
||||||
|
tile = new MapTile(x, y, true);
|
||||||
|
break;
|
||||||
|
case '.':
|
||||||
|
tile = new MapTile(x, y, false, true);
|
||||||
|
break;
|
||||||
|
case '-':
|
||||||
|
tile = new MapTile(x, y, false);
|
||||||
|
break;
|
||||||
|
case 'S':
|
||||||
|
tile = new MapTile(x, y, false);
|
||||||
|
spawn = {x, y};
|
||||||
|
break;
|
||||||
|
case 'O':
|
||||||
|
tile = new MapTile(x, y, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles[x][y] = tile;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.map = new GameMap(tiles, spawn);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMap() {
|
||||||
|
while(!this.map) {
|
||||||
|
|
||||||
|
}
|
||||||
|
return this.map;
|
||||||
|
}
|
||||||
|
}
|
79
MapTile.js
Normal file
79
MapTile.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
class MapTile {
|
||||||
|
constructor(x, y, isSolid, hasCoin) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.solid = isSolid;
|
||||||
|
this.coin = hasCoin;
|
||||||
|
}
|
||||||
|
|
||||||
|
lookAtNeighbours(neighbours) {
|
||||||
|
this.borders = {};
|
||||||
|
|
||||||
|
if(neighbours.left && neighbours.left.solid) {
|
||||||
|
this.borders.left = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(neighbours.top && neighbours.top.solid) {
|
||||||
|
this.borders.top = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(neighbours.right && neighbours.right.solid) {
|
||||||
|
this.borders.right = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(neighbours.bottom && neighbours.bottom.solid) {
|
||||||
|
this.borders.bottom = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx, conf) {
|
||||||
|
if (this.solid) {
|
||||||
|
this.drawSolid(ctx, conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.coin) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate((this.x + 0.5) * conf.tileSize, (this.y + 0.5) * conf.tileSize);
|
||||||
|
ctx.fillStyle = "#fff";
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(0, 0, 0.15 * conf.tileSize, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSolid(ctx, conf) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate((this.x + 0.5) * conf.tileSize, (this.y + 0.5) * conf.tileSize);
|
||||||
|
ctx.strokeStyle = "#00f";
|
||||||
|
ctx.lineWidth = conf.tileSize * .5;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
if (this.borders.left) {
|
||||||
|
ctx.moveTo(0.25 * conf.tileSize, 0);
|
||||||
|
ctx.lineTo(-0.5 * conf.tileSize, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.borders.top) {
|
||||||
|
ctx.moveTo(0, 0.25 * conf.tileSize);
|
||||||
|
ctx.lineTo(0, -0.5 * conf.tileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.borders.right) {
|
||||||
|
ctx.moveTo(-0.25 * conf.tileSize, 0);
|
||||||
|
ctx.lineTo(0.5 * conf.tileSize, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.borders.bottom) {
|
||||||
|
ctx.moveTo(0, -0.25 * conf.tileSize);
|
||||||
|
ctx.lineTo(0, 0.5 * conf.tileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
98
MovingObject.js
Normal file
98
MovingObject.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
class MovingObject extends GameObject {
|
||||||
|
constructor(game, x, y) {
|
||||||
|
super(game, x, y);
|
||||||
|
this.dir = {x: 0, y: 0};
|
||||||
|
this.speed = 1;
|
||||||
|
|
||||||
|
this.turnTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeDir(dir) {
|
||||||
|
if (this.dir.x === dir.x && this.dir.y === dir.y) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dir.x === dir.x || this.dir.y === dir.y) {
|
||||||
|
clearTimeout(this.turnTimeout);
|
||||||
|
this.dir = dir;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isX = this.dir.x !== 0;
|
||||||
|
let curPos;
|
||||||
|
let dirPositive;
|
||||||
|
|
||||||
|
if (isX) {
|
||||||
|
curPos = this.x;
|
||||||
|
dirPositive = this.dir.x > 0;
|
||||||
|
} else if (this.dir.y !== 0) {
|
||||||
|
curPos = this.y;
|
||||||
|
dirPositive = this.dir.y > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetSwitch;
|
||||||
|
if (dirPositive) {
|
||||||
|
targetSwitch = Math.ceil(curPos + 0.5) - 0.5;
|
||||||
|
} else {
|
||||||
|
targetSwitch = Math.floor(curPos - 0.5) + 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = Math.abs(curPos - targetSwitch);
|
||||||
|
const duration = distance / this.speed * 1000;
|
||||||
|
|
||||||
|
clearTimeout(this.turnTimeout);
|
||||||
|
this.turnTimeout = setTimeout(() => {
|
||||||
|
if (isX) {
|
||||||
|
this.x = targetSwitch;
|
||||||
|
} else {
|
||||||
|
this.y = targetSwitch;
|
||||||
|
}
|
||||||
|
this.dir = dir;
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime) {
|
||||||
|
const ticks = Math.floor(deltaTime / 16);
|
||||||
|
const tickLength = deltaTime / ticks;
|
||||||
|
|
||||||
|
for(let i = 0; i < ticks; i++) {
|
||||||
|
this.x += this.dir.x * (this.speed * tickLength / 1000);
|
||||||
|
this.y += this.dir.y * (this.speed * tickLength / 1000);
|
||||||
|
|
||||||
|
const nextTile = this.getNextTile(this.dir.x, this.dir.y);
|
||||||
|
|
||||||
|
if (!nextTile.tile) {
|
||||||
|
if (nextTile.distanceX <= 0.5 || nextTile.distanceY <= 0.5) {
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextTile.tile.solid) {
|
||||||
|
if (nextTile.distanceX <= 0.5) {
|
||||||
|
this.x = nextTile.tile.x - this.dir.x + 0.5;
|
||||||
|
}
|
||||||
|
if (nextTile.distanceY <= 0.5) {
|
||||||
|
this.y = nextTile.tile.y - this.dir.y + 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextTile(offsetX, offsetY) {
|
||||||
|
const tileX = Math.round(this.x - 0.5);
|
||||||
|
const tileY = Math.round(this.y - 0.5);
|
||||||
|
|
||||||
|
const nextX = tileX + offsetX;
|
||||||
|
const nextY = tileY + offsetY;
|
||||||
|
|
||||||
|
const distanceX = Math.abs(this.x - (offsetX >= 0 ? nextX : nextX + 1));
|
||||||
|
const distanceY = Math.abs(this.y - (offsetY >= 0 ? nextY : nextY + 1));
|
||||||
|
|
||||||
|
return {
|
||||||
|
tile: this.game.map.getTile(nextX, nextY),
|
||||||
|
distanceX: distanceX,
|
||||||
|
distanceY: distanceY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
96
Player.js
Normal file
96
Player.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
class Player extends MovingObject {
|
||||||
|
constructor(game, x, y) {
|
||||||
|
super(game, x, y);
|
||||||
|
this.speed = 3;
|
||||||
|
|
||||||
|
this.score = 0;
|
||||||
|
|
||||||
|
this.mouthAngle = 90; // = 90°
|
||||||
|
this.mouthDecreasing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx, conf) {
|
||||||
|
const radius = conf.tileSize * 0.5;
|
||||||
|
const cx = this.x * conf.tileSize;
|
||||||
|
const cy = this.y * conf.tileSize;
|
||||||
|
const mouthAngle = this.mouthAngle / 180 * Math.PI;
|
||||||
|
|
||||||
|
ctx.fillStyle = '#ff0';
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(cx, cy);
|
||||||
|
ctx.rotate(this.getRotateDir());
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
ctx.moveTo(0, 0);
|
||||||
|
ctx.arc(0, 0, radius, mouthAngle / 2, 2 * Math.PI - mouthAngle / 2, false);
|
||||||
|
ctx.lineTo(0, 0);
|
||||||
|
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime) {
|
||||||
|
super.update(deltaTime);
|
||||||
|
|
||||||
|
this.checkForCollisions();
|
||||||
|
|
||||||
|
// Collect coins
|
||||||
|
const currentTile = this.getNextTile(0, 0);
|
||||||
|
|
||||||
|
if(currentTile.tile.coin) {
|
||||||
|
if(currentTile.distanceX <= 0.5 || currentTile.distanceY <= 0.5) {
|
||||||
|
currentTile.tile.coin = false;
|
||||||
|
this.score += 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.mouthDecreasing) {
|
||||||
|
this.mouthAngle -= this.speed * 4;
|
||||||
|
if(this.mouthAngle <= 0) {
|
||||||
|
this.mouthAngle = 0;
|
||||||
|
this.mouthDecreasing = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.mouthAngle += this.speed * 4;
|
||||||
|
if(this.mouthAngle >= 90) {
|
||||||
|
this.mouthAngle = 90;
|
||||||
|
this.mouthDecreasing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForCollisions() {
|
||||||
|
this.game.gameObjects.forEach(object => {
|
||||||
|
if(object === this) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Math.abs(object.x - this.x) < 1 && Math.abs(object.y - this.y) < 1) {
|
||||||
|
if(object instanceof Ghost) {
|
||||||
|
this.gameOver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getRotateDir() {
|
||||||
|
if(this.dir.x === -1) {
|
||||||
|
return Math.PI;
|
||||||
|
}
|
||||||
|
if(this.dir.y === 1) {
|
||||||
|
return Math.PI / 2;
|
||||||
|
}
|
||||||
|
if(this.dir.y === -1) {
|
||||||
|
return 3 / 2 * Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameOver() {
|
||||||
|
this.game.gameOver = true;
|
||||||
|
}
|
||||||
|
}
|
5
helper-methods.js
Normal file
5
helper-methods.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
function calculateTileSize(width, height, tilesX, tilesY) {
|
||||||
|
const tileWidth = width / tilesX,
|
||||||
|
tileHeight = height / tilesY;
|
||||||
|
return Math.floor(Math.min(tileWidth, tileHeight));
|
||||||
|
}
|
28
index.html
Normal file
28
index.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Pacman</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.pacman-game {
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="helper-methods.js"></script>
|
||||||
|
<script src="DOM.js"></script>
|
||||||
|
<script src="GameObject.js"></script>
|
||||||
|
<script src="MovingObject.js"></script>
|
||||||
|
<script src="Ghost.js"></script>
|
||||||
|
<script src="Player.js"></script>
|
||||||
|
<script src="MapTile.js"></script>
|
||||||
|
<script src="GameMap.js"></script>
|
||||||
|
<script src="MapLoader.js"></script>
|
||||||
|
<script src="Game.js"></script>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
21
levels/level1.pacmap
Normal file
21
levels/level1.pacmap
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
XXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
X............X............X
|
||||||
|
X.XXX.XXXXXX.X.XXXXXX.XXX.X
|
||||||
|
XOX-X.X-X.........X-X.X-XOX
|
||||||
|
X.XXX.XXX.XXXXXXX.XXX.XXX.X
|
||||||
|
X............X............X
|
||||||
|
X.XXX.XXXXXX-X-XXXXXX.XXX.X
|
||||||
|
X.......X---------X.......X
|
||||||
|
XXXXXXX.X-XXXXXXX-X.XXXXXXX
|
||||||
|
XXXXXXX.X-X-----X-X.XXXXXXX
|
||||||
|
-------.--X-----X--.-------
|
||||||
|
XXXXXXX.X-X-----X-X.XXXXXXX
|
||||||
|
XXXXXXX.X-XXXXXXX-X.XXXXXXX
|
||||||
|
X............X............X
|
||||||
|
XOXXX.XXXXXX.X.XXXXXX.XXXOX
|
||||||
|
X...X........S........X...X
|
||||||
|
XXX.XXX.X.XXXXXXX.X.XXX.XXX
|
||||||
|
X.......X....X....X.......X
|
||||||
|
X.XXXXXXXXXX.X.XXXXXXXXXX.X
|
||||||
|
X.........................X
|
||||||
|
XXXXXXXXXXXXXXXXXXXXXXXXXXX
|
Reference in New Issue
Block a user