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