You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
455 lines
11 KiB
455 lines
11 KiB
// @ts-check
|
|
class EventEmitter {
|
|
constructor() {
|
|
this.listeners = {};
|
|
}
|
|
|
|
on(message, listener) {
|
|
if (!this.listeners[message]) {
|
|
this.listeners[message] = [];
|
|
}
|
|
this.listeners[message].push(listener);
|
|
}
|
|
|
|
emit(message, payload = null) {
|
|
if (this.listeners[message]) {
|
|
this.listeners[message].forEach((l) => l(message, payload));
|
|
}
|
|
}
|
|
}
|
|
|
|
class GameObject {
|
|
constructor(x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.dead = false;
|
|
this.type = '';
|
|
this.width = 0;
|
|
this.height = 0;
|
|
this.img = undefined;
|
|
}
|
|
|
|
draw(ctx) {
|
|
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
|
|
}
|
|
|
|
rectFromGameObject() {
|
|
return {
|
|
top: this.y,
|
|
left: this.x,
|
|
bottom: this.y + this.height,
|
|
right: this.x + this.width,
|
|
};
|
|
}
|
|
}
|
|
|
|
class Hero extends GameObject {
|
|
constructor(x, y) {
|
|
super(x, y);
|
|
(this.width = 99), (this.height = 75);
|
|
this.type = 'Hero';
|
|
this.speed = { x: 0, y: 0 };
|
|
}
|
|
}
|
|
|
|
class Laser extends GameObject {
|
|
constructor(x, y) {
|
|
super(x, y);
|
|
this.width = 9;
|
|
this.height = 33;
|
|
this.type = 'Laser';
|
|
let id = setInterval(() => {
|
|
if (!this.dead) {
|
|
this.y = this.y > 0 ? this.y - 20 : this.y;
|
|
if (this.y <= 0) {
|
|
this.dead = true;
|
|
}
|
|
} else {
|
|
clearInterval(id);
|
|
}
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
class Explosion extends GameObject {
|
|
constructor(x, y, img) {
|
|
super(x, y);
|
|
this.img = img;
|
|
this.type = 'Explosion';
|
|
(this.width = 56 * 2), (this.height = 54 * 2);
|
|
setTimeout(() => {
|
|
this.dead = true;
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
class Monster extends GameObject {
|
|
constructor(x, y) {
|
|
super(x, y);
|
|
this.type = 'Monster';
|
|
(this.width = 98), (this.height = 50);
|
|
let id = setInterval(() => {
|
|
if (!this.dead) {
|
|
this.y = this.y < HEIGHT ? this.y + 30 : this.y;
|
|
if (this.y >= HEIGHT - this.height) {
|
|
this.dead = true;
|
|
eventEmitter.emit('MONSTER_OUT_OF_BOUNDS');
|
|
}
|
|
} else {
|
|
clearInterval(id);
|
|
}
|
|
}, 1500);
|
|
}
|
|
}
|
|
|
|
const Messages = {
|
|
MONSTER_OUT_OF_BOUNDS: 'MONSTER_OUT_OF_BOUNDS',
|
|
HERO_SPEED_LEFT: 'HERO_MOVING_LEFT',
|
|
HERO_SPEED_RIGHT: 'HERO_MOVING_RIGHT',
|
|
HERO_SPEED_ZERO: 'HERO_SPEED_ZERO',
|
|
HERO_FIRE: 'HERO_FIRE',
|
|
GAME_END_LOSS: 'GAME_END_LOSS',
|
|
GAME_END_WIN: 'GAME_END_WIN',
|
|
COLLISION_MONSTER_LASER: 'COLLISION_MONSTER_LASER',
|
|
COLLISION_MONSTER_HERO: 'COLLISION_MONSTER_HERO',
|
|
KEY_EVENT_UP: 'KEY_EVENT_UP',
|
|
KEY_EVENT_DOWN: 'KEY_EVENT_DOWN',
|
|
KEY_EVENT_LEFT: 'KEY_EVENT_LEFT',
|
|
KEY_EVENT_RIGHT: 'KEY_EVENT_RIGHT',
|
|
GAME_START: 'GAME_START',
|
|
};
|
|
|
|
class Game {
|
|
constructor() {
|
|
this.points = 0;
|
|
this.life = 3;
|
|
this.end = false;
|
|
this.ready = false;
|
|
|
|
eventEmitter.on(Messages.MONSTER_OUT_OF_BOUNDS, () => {
|
|
hero.dead = true;
|
|
});
|
|
eventEmitter.on(Messages.HERO_SPEED_LEFT, () => {
|
|
hero.speed.x = -10;
|
|
hero.img = heroImgLeft;
|
|
});
|
|
eventEmitter.on(Messages.HERO_SPEED_RIGHT, () => {
|
|
hero.speed.x = 10;
|
|
hero.img = heroImgRight;
|
|
});
|
|
eventEmitter.on(Messages.HERO_SPEED_ZERO, () => {
|
|
hero.speed = { x: 0, y: 0 };
|
|
if (game.life === 3) {
|
|
hero.img = heroImg;
|
|
} else {
|
|
hero.img = heroImgDamaged;
|
|
}
|
|
});
|
|
eventEmitter.on(Messages.HERO_FIRE, () => {
|
|
if (coolDown === 0) {
|
|
let l = new Laser(hero.x + 45, hero.y - 30);
|
|
l.img = laserRedImg;
|
|
gameObjects.push(l);
|
|
cooling();
|
|
}
|
|
});
|
|
eventEmitter.on(Messages.GAME_END_LOSS, (_, gameLoopId) => {
|
|
game.end = true;
|
|
displayMessage('You died... - Press [Enter] to start the game Captain Pew Pew');
|
|
clearInterval(gameLoopId);
|
|
});
|
|
|
|
eventEmitter.on(Messages.GAME_END_WIN, (_, gameLoopId) => {
|
|
game.end = true;
|
|
displayMessage('Victory!!! Pew Pew... - Press [Enter] to start a new game Captain Pew Pew', 'green');
|
|
clearInterval(gameLoopId);
|
|
});
|
|
eventEmitter.on(Messages.COLLISION_MONSTER_LASER, (_, { first: laser, second: monster }) => {
|
|
laser.dead = true;
|
|
monster.dead = true;
|
|
this.points += 100;
|
|
|
|
gameObjects.push(new Explosion(monster.x, monster.y, laserRedShot));
|
|
});
|
|
eventEmitter.on(Messages.COLLISION_MONSTER_HERO, (_, { monster: m, id }) => {
|
|
game.life--;
|
|
if (game.life === 0) {
|
|
hero.dead = true;
|
|
eventEmitter.emit(Messages.GAME_END_LOSS, id);
|
|
gameObjects.push(new Explosion(hero.x, hero.y, laserGreenShot));
|
|
}
|
|
hero.img = heroImgDamaged;
|
|
m.dead = true;
|
|
gameObjects.push(new Explosion(m.x, m.y, laserRedShot));
|
|
});
|
|
eventEmitter.on(Messages.KEY_EVENT_UP, () => {
|
|
hero.y = hero.y > 0 ? hero.y - 5 : hero.y;
|
|
});
|
|
eventEmitter.on(Messages.KEY_EVENT_DOWN, () => {
|
|
hero.y = hero.y < HEIGHT ? hero.y + 5 : hero.y;
|
|
});
|
|
eventEmitter.on(Messages.KEY_EVENT_LEFT, () => {
|
|
hero.x = hero.x > 0 ? hero.x - 10 : hero.x;
|
|
});
|
|
eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => {
|
|
hero.x = hero.x < WIDTH ? hero.x + 10 : hero.x;
|
|
});
|
|
eventEmitter.on(Messages.GAME_START, () => {
|
|
if (game.ready && game.end) {
|
|
// assets loaded
|
|
runGame();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const eventEmitter = new EventEmitter();
|
|
const hero = new Hero(0, 0);
|
|
const WIDTH = 1024;
|
|
const HEIGHT = 768;
|
|
let gameObjects = [];
|
|
let laserRedImg;
|
|
let laserRedShot;
|
|
let laserGreenShot;
|
|
let canvas;
|
|
let ctx;
|
|
let heroImg;
|
|
let heroImgLeft;
|
|
let heroImgRight;
|
|
let heroImgDamaged;
|
|
let lifeImg;
|
|
let monsterImg;
|
|
|
|
let coolDown = 0;
|
|
|
|
const game = new Game();
|
|
|
|
function loadTexture(path) {
|
|
return new Promise((resolve) => {
|
|
const img = new Image();
|
|
img.src = path;
|
|
img.onload = () => {
|
|
resolve(img);
|
|
};
|
|
});
|
|
}
|
|
|
|
function rectFromGameObject(go) {
|
|
return {
|
|
top: go.y,
|
|
left: go.x,
|
|
bottom: go.y + go.height,
|
|
right: go.x + go.width,
|
|
};
|
|
}
|
|
|
|
function intersectRect(r1, r2) {
|
|
return !(r2.left > r1.right || r2.right < r1.left || r2.top > r1.bottom || r2.bottom < r1.top);
|
|
}
|
|
|
|
function draw(ctx, objects) {
|
|
objects.forEach((obj) => {
|
|
obj.draw(ctx);
|
|
});
|
|
}
|
|
|
|
let onKeyDown = function (e) {
|
|
console.log(e.keyCode);
|
|
switch (e.keyCode) {
|
|
case 37:
|
|
case 39:
|
|
case 38:
|
|
case 40: // Arrow keys
|
|
case 32:
|
|
e.preventDefault();
|
|
break; // Space
|
|
default:
|
|
break; // do not block other keys
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', onKeyDown);
|
|
window.addEventListener('keydown', (e) => {
|
|
switch (e.keyCode) {
|
|
case 37:
|
|
// if left
|
|
eventEmitter.emit(Messages.HERO_SPEED_LEFT);
|
|
break;
|
|
case 39:
|
|
eventEmitter.emit(Messages.HERO_SPEED_RIGHT);
|
|
break;
|
|
}
|
|
});
|
|
|
|
// TODO make message driven
|
|
window.addEventListener('keyup', (evt) => {
|
|
eventEmitter.emit(Messages.HERO_SPEED_ZERO);
|
|
if (evt.key === 'ArrowUp') {
|
|
eventEmitter.emit(Messages.KEY_EVENT_UP);
|
|
} else if (evt.key === 'ArrowDown') {
|
|
eventEmitter.emit(Messages.KEY_EVENT_DOWN);
|
|
} else if (evt.key === 'ArrowLeft') {
|
|
eventEmitter.emit(Messages.KEY_EVENT_LEFT);
|
|
} else if (evt.key === 'ArrowRight') {
|
|
eventEmitter.emit(Messages.KEY_EVENT_RIGHT);
|
|
} else if (evt.keyCode === 32) {
|
|
// space
|
|
eventEmitter.emit(Messages.HERO_FIRE);
|
|
} else if (evt.key === 'Enter') {
|
|
eventEmitter.emit(Messages.GAME_START);
|
|
}
|
|
});
|
|
|
|
function cooling() {
|
|
coolDown = 500;
|
|
let id = setInterval(() => {
|
|
coolDown -= 100;
|
|
if (coolDown === 0) {
|
|
clearInterval(id);
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
function displayGameScore(message) {
|
|
ctx.font = '30px Arial';
|
|
ctx.fillStyle = 'red';
|
|
ctx.textAlign = 'right';
|
|
ctx.fillText(message, canvas.width - 90, canvas.height - 30);
|
|
}
|
|
|
|
function displayLife() {
|
|
// should show tree ships.. 94 * 3
|
|
const START_X = canvas.width - 150 - 30;
|
|
for (let i = 0; i < game.life; i++) {
|
|
ctx.drawImage(lifeImg, START_X + (i + 1) * 35, canvas.height - 90);
|
|
}
|
|
}
|
|
|
|
function displayMessage(message, color = 'red') {
|
|
ctx.font = '30px Arial';
|
|
ctx.fillStyle = color;
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(message, canvas.width / 2, canvas.height / 2);
|
|
}
|
|
|
|
function createMonsters(monsterImg) {
|
|
// 98 * 5 canvas.width - (98*5 /2)
|
|
const MONSTER_TOTAL = 5;
|
|
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
|
|
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
|
|
const STOP_X = START_X + MONSTER_WIDTH;
|
|
|
|
for (let x = START_X; x < STOP_X; x += 98) {
|
|
for (let y = 0; y < 50 * 5; y += 50) {
|
|
gameObjects.push(new Monster(x, y));
|
|
}
|
|
}
|
|
|
|
gameObjects.forEach((go) => {
|
|
go.img = monsterImg;
|
|
});
|
|
}
|
|
|
|
function createHero(heroImg) {
|
|
hero.dead = false;
|
|
hero.img = heroImg;
|
|
hero.y = (canvas.height / 4) * 3;
|
|
hero.x = canvas.width / 2;
|
|
gameObjects.push(hero);
|
|
}
|
|
|
|
function checkGameState(gameLoopId) {
|
|
const monsters = gameObjects.filter((go) => go.type === 'Monster');
|
|
if (hero.dead) {
|
|
eventEmitter.emit(Messages.GAME_END_LOSS, gameLoopId);
|
|
} else if (monsters.length === 0) {
|
|
eventEmitter.emit(Messages.GAME_END_WIN);
|
|
}
|
|
|
|
// update hero position
|
|
if (hero.speed.x !== 0) {
|
|
hero.x += hero.speed.x;
|
|
}
|
|
|
|
const lasers = gameObjects.filter((go) => go.type === 'Laser');
|
|
// laser hit something
|
|
lasers.forEach((l) => {
|
|
monsters.forEach((m) => {
|
|
if (intersectRect(l.rectFromGameObject(), m.rectFromGameObject())) {
|
|
eventEmitter.emit(Messages.COLLISION_MONSTER_LASER, {
|
|
first: l,
|
|
second: m,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// hero hit monster
|
|
monsters.forEach((m) => {
|
|
if (intersectRect(m.rectFromGameObject(), hero.rectFromGameObject())) {
|
|
eventEmitter.emit(Messages.COLLISION_MONSTER_HERO, { monster: m, id: gameLoopId });
|
|
}
|
|
});
|
|
|
|
gameObjects = gameObjects.filter((go) => !go.dead);
|
|
}
|
|
|
|
function runGame() {
|
|
gameObjects = [];
|
|
game.life = 3;
|
|
game.points = 0;
|
|
game.end = false;
|
|
|
|
createMonsters(monsterImg);
|
|
createHero(heroImg);
|
|
|
|
let gameLoopId = setInterval(() => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.fillStyle = 'black';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
displayGameScore('Score: ' + game.points);
|
|
displayLife();
|
|
checkGameState(gameLoopId);
|
|
draw(ctx, gameObjects);
|
|
}, 100);
|
|
}
|
|
|
|
window.onload = async () => {
|
|
canvas = document.getElementById('myCanvas');
|
|
ctx = canvas.getContext('2d');
|
|
|
|
heroImg = await loadTexture('spaceArt/png/player.png');
|
|
heroImgLeft = await loadTexture('spaceArt/png/playerLeft.png');
|
|
heroImgRight = await loadTexture('spaceArt/png/playerRight.png');
|
|
heroImgDamaged = await loadTexture('spaceArt/png/playerDamaged.png');
|
|
monsterImg = await loadTexture('spaceArt/png/enemyShip.png');
|
|
laserRedImg = await loadTexture('spaceArt/png/laserRed.png');
|
|
laserRedShot = await loadTexture('spaceArt/png/laserRedShot.png');
|
|
laserGreenShot = await loadTexture('spaceArt/png/laserGreenShot.png');
|
|
lifeImg = await loadTexture('spaceArt/png/life.png');
|
|
|
|
game.ready = true;
|
|
game.end = true;
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.fillStyle = 'black';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
displayMessage('Press [Enter] to start the game Captain Pew Pew', 'blue');
|
|
|
|
// CHECK draw 5 * 5 monsters
|
|
// CHECK move monsters down 1 step per 0.5 second
|
|
// CHECK if monster collide with hero, destroy both, display loose text
|
|
// CHECK if monster reach MAX, destroy hero, loose text
|
|
// TODO add explosion when laser hits monster, should render for <=300ms
|
|
// TODO add specific texture when moving left or right
|
|
// TODO take damage when a meteor moves into you
|
|
// TODO add meteor, meteors can damage ships
|
|
// TODO add UFO after all monsters are down, UFO can fire back
|
|
// TODO start random green laser from an enemy and have it go to HEIGHT, if collide with hero then deduct point
|
|
|
|
// CHECK draw bullet
|
|
// CHECK , bullet should be destroyed at top
|
|
// CHECK space should produce bullet, bullet should move 2 step per second
|
|
// CHECK if bullet collide with monster, destroy both
|
|
// CHECK if bullet rect intersect with monster rect then it is colliding..
|
|
};
|