aboutsummaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
authorJuan J. Martinez <jjm@usebox.net>2025-02-09 17:25:59 +0000
committerJuan J. Martinez <jjm@usebox.net>2025-02-09 17:25:59 +0000
commit6f5fa0b5240d6edd3bce72ea1549e4cd9b0ce2bc (patch)
tree44ebe6f5cfb1eb9214c5877342c7914de07f215a /js
downloadjs-twin-shooter-6f5fa0b5240d6edd3bce72ea1549e4cd9b0ce2bc.tar.gz
js-twin-shooter-6f5fa0b5240d6edd3bce72ea1549e4cd9b0ce2bc.zip
Initial import
Diffstat (limited to 'js')
-rw-r--r--js/game.js293
-rw-r--r--js/main.js11
-rw-r--r--js/mygame.js535
3 files changed, 839 insertions, 0 deletions
diff --git a/js/game.js b/js/game.js
new file mode 100644
index 0000000..6527584
--- /dev/null
+++ b/js/game.js
@@ -0,0 +1,293 @@
+const floor = Math.floor;
+const max = Math.max;
+const min = Math.min;
+
+class Game {
+ constructor(canvas, width, height) {
+ this.canvas = canvas;
+ this.canvas.width = width;
+ this.canvas.height = height;
+ this.ctx = this.canvas.getContext("2d");
+
+ // override this with your data to be loaded:
+ //
+ // { id: "file.png" }
+ //
+ // Supports .png, .ogg and .json files.
+ //
+ // and also override dataSize with:
+ //
+ // this.dataSize = Object.keys(this.data).length
+ //
+ this.data = {};
+ this.dataSize = 0;
+
+ // override to change keys
+ this.controls = {
+ 80: "pause",
+ 32: "start",
+ 37: "left",
+ 38: "up",
+ 39: "right",
+ 40: "down",
+ 87: "f_up",
+ 83: "f_down",
+ 65: "f_left",
+ 68: "f_right"
+ };
+
+ // will store the resouces indexed by id
+ this.res = {};
+ this.resSize = 0;
+
+ // override if you want to change the limit
+ // defaults to 8 "channels"
+ this.playLimit = 8;
+ this.playCount = 0;
+
+ // test to see if a key is down or not
+ this.keys = {
+ pause: false,
+ start: false,
+ left: false,
+ up: false,
+ right: false,
+ down: false,
+ f_up: false,
+ f_down: false,
+ f_left: false,
+ f_right: false
+ };
+
+ this.gamepads = {};
+
+ this.minFps = 60;
+ this.then = -1 / this.minFps;
+
+ this.loadingError = undefined;
+ }
+
+ start() {
+ this.canvas.style.background = "rgb(21, 21, 21)";
+ this.resize();
+
+ window.onresize = (ev) => {
+ this.resize();
+ };
+
+ window.addEventListener("gamepadconnected", (e) => {
+ console.log(
+ "Gamepad connected at index %d: %s. %d buttons, %d axes.",
+ e.gamepad.index,
+ e.gamepad.id,
+ e.gamepad.buttons.length,
+ e.gamepad.axes.length,
+ );
+ this.gamepads[e.gamepad.index] = e.gamepad;
+ });
+ window.addEventListener("gamepaddisconnected", (e) => {
+ console.log(
+ "Gamepad disconnected from index %d: %s",
+ e.gamepad.index,
+ e.gamepad.id,
+ );
+ delete this.gamepads[e.gamepad.index];
+ });
+
+ this.loader();
+ }
+
+ // override this
+ init() {}
+ // override this
+ update(dt) {}
+ // override this
+ draw() {}
+
+ _update(dt) {}
+ _draw() {}
+
+ resize() {
+ const scale = floor(window.innerHeight / this.canvas.height);
+ this.canvas.style["imageRendering"] = "pixelated";
+ this.canvas.style["transformOrigin"] = "top";
+ this.canvas.style["transform"] = `scale(${scale})`;
+ }
+
+ loop(now) {
+
+ Object.keys(this.gamepads).forEach((g) => {
+ const gp = this.gamepads[g];
+
+ this.keys["f_up"] = gp.buttons[2].pressed;
+ this.keys["f_down"] = gp.buttons[0].pressed;
+ this.keys["f_left"] = gp.buttons[3].pressed;
+ this.keys["f_right"] = gp.buttons[1].pressed;
+
+ if (gp.buttons[14].pressed) {
+ this.keys["left"] = true;
+ this.keys["right"] = false;
+ } else {
+ if (gp.buttons[15].pressed) {
+ this.keys["left"] = false;
+ this.keys["right"] = true;
+ } else {
+ this.keys["left"] = false;
+ this.keys["right"] = false;
+ }
+ }
+
+ if (gp.buttons[12].pressed) {
+ this.keys["up"] = true;
+ this.keys["down"] = false;
+ } else {
+ if (gp.buttons[13].pressed) {
+ this.keys["up"] = false;
+ this.keys["down"] = true;
+ } else {
+ this.keys["up"] = false;
+ this.keys["down"] = false;
+ }
+ }
+ });
+
+ let dt = min(1000 / this.minFps, now - this.then);
+ this._update(dt/1000);
+
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ this.ctx.save();
+ this._draw();
+ this.ctx.restore();
+
+ this.then = now;
+ requestAnimationFrame((now) => {
+ this.loop(now)
+ });
+ }
+
+ loader() {
+ this._draw = this.drawLoading;
+ this.loop(0);
+
+ const onError = (ev) => {
+ console.log(ev);
+ this.loadingError = true;
+ };
+
+ for (const id in this.data) {
+ if (this.data[id].indexOf(".png") != -1) {
+ this.res[id] = new Image();
+ this.res[id].src = this.data[id];
+ this.res[id].onerror = onError
+ this.res[id].onload = (ev) => {
+ this.res[id].onload = null;
+ ev.currentTarget.removeEventListener("error", onError);
+ this.resSize += 1;
+ };
+ continue;
+ }
+ if (this.data[id].indexOf(".ogg") != -1) {
+ this.res[id] = new Audio();
+ this.res[id].src = this.data[id];
+ this.res[id].autoplay = false;
+ this.res[id].addEventListener("error", onError);
+ this.res[id].oncanplaythrough = (ev) => {
+ this.res[id].oncanplaythrough = null;
+ ev.currentTarget.removeEventListener("error", onError);
+ this.resSize += 1;
+ };
+ continue;
+ }
+ if (this.data[id].indexOf(".json") != -1) {
+ let xhr = new window.XMLHttpRequest();
+ xhr.open("GET", this.data[id]);
+ xhr.responseType = "json";
+ xhr.addEventListener("error", onError);
+ xhr.onload = (ev) => {
+ ev.currentTarget.removeEventListener("error", onError);
+ xhr.onload = null;
+ if (xhr.status == 200) {
+ this.res[id] = xhr.response;
+ this.resSize += 1;
+ } else {
+ console.log(ev);
+ this.loadingError = true;
+ }
+ };
+ xhr.send();
+ continue;
+ }
+ }
+ }
+
+ // won't use drawStart/drawEnd because may render regular fonts
+ drawLoading() {
+ // height of the loading bar
+ const h = 6;
+
+ if (this.loadingError == true) {
+ this.ctx.fillStyle = "rgb(255, 128, 128)";
+ this.ctx.font = "8px monospace";
+ this.ctx.fillText(
+ "ERROR Loading Resources",
+ floor(this.ctx.canvas.width * 0.1),
+ floor(this.ctx.canvas.height / 2 - h - h / 2)
+ );
+ }
+
+ this.ctx.fillStyle = "rgb(128, 128, 128)";
+ this.ctx.fillRect(floor(this.ctx.canvas.width * 0.1), floor(this.ctx.canvas.height / 2) - h, this.ctx.canvas.width - floor(this.ctx.canvas.width * 0.2), h);
+ this.ctx.fillStyle = "rgb(255, 255, 255)";
+ this.ctx.fillRect(floor(this.ctx.canvas.width * 0.1), floor(this.ctx.canvas.height / 2) - h, (
+ floor(this.ctx.canvas.resSize * (this.ctx.canvas.width - floor(this.ctx.canvas.width * 0.2)) / this.ctx.canvas.dataSize)
+ ), h);
+
+ // we don't do this on update because we want
+ // the progress bar to finish drawing
+ if (this.resSize == this.dataSize) {
+ console.log("Loader done");
+
+ document.addEventListener("keydown", (ev) => {
+ this.keyDown(ev)
+ }, false);
+ document.addEventListener("keyup", (ev) => {
+ this.keyUp(ev)
+ }, false);
+
+ this.init();
+ this._update = this.update;
+ this._draw = this.draw;
+ }
+ }
+
+ playSnd(playable, loop, clone) {
+ if (this.playCount < this.playLimit) {
+ this.playCount++;
+
+ if (clone || false) {
+ playable = playable.cloneNode(true)
+ }
+ playable.onended = (ev) => {
+ playable.onended = null;
+ this.playCount--;
+ };
+ playable.loop = loop || false;
+ playable.play();
+ }
+ }
+
+ keyDown(ev) {
+ let key = this.controls[ev.keyCode];
+ if (key != undefined) {
+ this.keys[key] = true;
+ ev.preventDefault();
+ }
+ }
+
+ keyUp(ev) {
+ let key = this.controls[ev.keyCode];
+ if (key != undefined) {
+ this.keys[key] = false;
+ }
+ }
+}
diff --git a/js/main.js b/js/main.js
new file mode 100644
index 0000000..808ff4a
--- /dev/null
+++ b/js/main.js
@@ -0,0 +1,11 @@
+window.addEventListener("DOMContentLoaded", (ev) => {
+ var canvas = document.getElementById("game");
+ if (canvas.getContext) {
+ var game = new MyGame(canvas);
+ game.start();
+ } else {
+ canvas.style.backgroundColor = "rgb(21, 21, 21)";
+ canvas.insertAdjacentHTML("afterend", "<p>No Canvas 2D support found, please try a different browser.</p>");
+ console.log("No canvas 2D support found");
+ }
+});
diff --git a/js/mygame.js b/js/mygame.js
new file mode 100644
index 0000000..e4453ff
--- /dev/null
+++ b/js/mygame.js
@@ -0,0 +1,535 @@
+const CANVAS_SIZE = { w: 160, h: 160 };
+const FONT_SIZE = { w: 8, h: 8};
+const SCORE_W = 6;
+
+const DIR_RIGHT = 0;
+const DIR_LEFT = 1;
+const WALK_FRAMES = [ 0, 1, 0, 2 ];
+const FDELAY = 8;
+
+const F_UP = 0;
+const F_DOWN = 1;
+const F_RIGHT = 2;
+const F_LEFT = 3;
+
+const COOLDOWN = 10;
+
+const SPAWN_EFFECT_DELAY = 32;
+
+const DAMAGE_SND_DELAY = 16;
+
+const MAX_LIFE = 20;
+
+const WAVES = [
+ { delay: 128, count: 8 },
+ { delay: 100, count: 8 },
+ { delay: 80, count: 16 },
+ { delay: 60, count: 32 },
+ { delay: 50, count: 64 },
+ { delay: 40, count: 64 },
+ { delay: 20, count: 96 },
+ { delay: 15, count: 96 }
+];
+
+function mkCanvasText(font, text) {
+ let c = document.createElement("canvas");
+ c.width = text.length * FONT_SIZE.w;
+ c.height = FONT_SIZE.h;
+ let ctx = c.getContext("2d");
+
+ ctx.clearRect(0, 0, c.width, c.height);
+ for(let i = 0; i < text.length; i++) {
+ /* our font starts with space, that's ascii 32 */
+ let index = text.charCodeAt(i) - 32;
+ ctx.drawImage(font,
+ 0, index * FONT_SIZE.h, FONT_SIZE.w, FONT_SIZE.h,
+ i * FONT_SIZE.w, 0, FONT_SIZE.w, FONT_SIZE.h);
+ }
+ return c;
+}
+
+
+class Entity {
+ constructor() {
+ this.alive = true;
+ this.x = 0;
+ this.y = 0;
+ this.size = { w: 0, h: 0, r: 0 };
+ }
+
+ collisionWith(other) {
+ let x = (other.x + other.size.w/2) - (this.x + this.size.w/2),
+ y = (other.y + other.size.h/2) - (this.y + this.size.h/2),
+ r = this.size.r + other.size.r;
+ if (x*x + y*y <= r*r) {
+ return true;
+ }
+ return false;
+ }
+}
+
+class Enemy extends Entity {
+ constructor() {
+ super();
+ }
+}
+
+class Slime extends Enemy {
+ constructor(x, y, sprites, target) {
+ super();
+ this.size = { w: 16, h: 16, r: 8 };
+ this.x = x;
+ this.y = y;
+ this.target = target;
+ this.sprites = sprites;
+ this.frame = 0;
+ this.fdelay = 0;
+ }
+
+ update(dt) {
+ this.fdelay++
+
+ if (this.fdelay & 1) {
+ let ix = 0, iy = 0;
+ let t = this.target();
+ if (t.is_alive) {
+ if (this.x > t.x) {
+ ix--;
+ } else if (this.x < t.x) {
+ ix++;
+ }
+
+ if (this.y > t.y) {
+ iy--;
+ } else if (this.y < t.y) {
+ iy++;
+ }
+
+ this.x += ix;
+ this.y += iy;
+ }
+ }
+
+ if (this.fdelay == FDELAY) {
+ this.fdelay = 0;
+ this.frame++;
+ if (this.frame == 3) {
+ this.frame = 0;
+ }
+ }
+ }
+
+ draw(ctx) {
+ ctx.drawImage(
+ this.sprites,
+ this.frame * this.size.w, 0, this.size.w, this.size.w,
+ this.x, this.y, this.size.w, this.size.h);
+ }
+}
+
+class Spawn extends Entity {
+ constructor(x, y, enemy, spawn) {
+ super();
+ this.x = x;
+ this.y = y;
+ this.fdelay = SPAWN_EFFECT_DELAY;
+ this.delay = 0;
+ this.enemy = enemy;
+ this.spawn = spawn;
+ }
+
+ update(dt) {
+ this.delay++;
+ if (this.delay == this.fdelay) {
+ this.spawn(this.enemy);
+ this.alive = false;
+ }
+ }
+
+ draw(ctx) {
+ ctx.beginPath();
+ ctx.arc(this.x, this.y, 1 + this.fdelay - this.delay, 0, 2 * Math.PI);
+ ctx.strokeStyle = `rgba(255, 255, 255, ${this.delay / (this.fdelay * 2)})`;
+ ctx.stroke();
+ }
+}
+
+class Explo extends Entity {
+ constructor(x, y, sprites) {
+ super();
+ this.size = { w: 16, h: 16 };
+ this.x = x;
+ this.y = y;
+ this.sprites = sprites;
+ this.frame = 0;
+ this.fdelay = 0;
+ }
+
+ update(dt) {
+ this.fdelay++;
+ if (this.fdelay == FDELAY)
+ {
+ this.fdelay = 0;
+ this.frame++;
+ if (this.frame == 4) {
+ this.alive = false;
+ }
+ }
+ }
+
+ draw(ctx) {
+ ctx.drawImage(
+ this.sprites,
+ this.frame * this.size.w, 0, this.size.w, this.size.w,
+ this.x, this.y, this.size.w, this.size.h);
+ }
+}
+
+class Bullet extends Entity {
+ constructor(x, y, sprites, dir) {
+ super();
+ this.size = { w: 16, h: 16, r: 2 };
+ this.x = x;
+ this.y = y;
+ this.dir = dir;
+ this.sprites = sprites;
+ }
+
+ update(dt) {
+ switch (this.dir) {
+ case F_UP:
+ this.y -= 2;
+ break;
+ case F_DOWN:
+ this.y += 2;
+ break;
+ case F_RIGHT:
+ this.x += 2;
+ break;
+ case F_LEFT:
+ this.x -= 2;
+ break;
+ }
+
+ if (this.x < 0 || this.x > CANVAS_SIZE.w - this.size.w
+ || this.y < 0 || this.y > CANVAS_SIZE.h - this.size.h) {
+ this.alive = false;
+ }
+ }
+
+ draw(ctx) {
+ ctx.drawImage(
+ this.sprites,
+ this.dir * this.size.w, 0, this.size.w, this.size.w,
+ this.x, this.y, this.size.w, this.size.h);
+ }
+}
+
+class Player extends Entity {
+ constructor(x, y, sprites, ctl, add_bullet) {
+ super();
+ this.size = { w: 16, h: 16, frames: 3, r: 8 };
+ this.x = x;
+ this.y = y;
+ this.sprites = sprites;
+ this.ctl = ctl;
+ this.dir = DIR_RIGHT;
+ this.frame = 0;
+ this.fdelay = 0;
+ this.cooldown = 0;
+ this.add_bullet = add_bullet;
+ this.life = MAX_LIFE;
+ }
+
+ is_alive() {
+ return this.life != 0;
+ }
+
+ is_fire(d) {
+ const all = ["f_up", "f_down", "f_right", "f_left"];
+ return this.ctl[d] && !all.filter((e) => e != d).some((e) => this.ctl[e]);
+ }
+
+ update(dt) {
+ let ix = 0, iy = 0, walking = false;
+ if (this.ctl["up"]) {
+ if (this.y > 0) {
+ iy--;
+ }
+ walking = true;
+ }
+ if (this.ctl["down"]) {
+ if (this.y < CANVAS_SIZE.h - this.size.h) {
+ iy++;
+ }
+ walking = true;
+ }
+ if (this.ctl["left"]) {
+ if (this.x > 0) {
+ ix--;
+ }
+ walking = true;
+ }
+ if (this.ctl["right"]) {
+ if (this.x < CANVAS_SIZE.w - this.size.w) {
+ ix++;
+ }
+ walking = true;
+ }
+
+ if (this.cooldown) {
+ this.cooldown--;
+ } else {
+ if (this.is_fire("f_right")) {
+ this.cooldown = COOLDOWN;
+ this.add_bullet(this.x + 4, this.y + 2, F_RIGHT);
+ } else if (this.is_fire("f_left")) {
+ this.cooldown = COOLDOWN;
+ this.add_bullet(this.x - 4, this.y + 2, F_LEFT);
+ } else if (this.is_fire("f_up")) {
+ this.cooldown = COOLDOWN;
+ this.add_bullet(this.x, this.y - 2, F_UP);
+ } else if (this.is_fire("f_down")) {
+ this.cooldown = COOLDOWN;
+ this.add_bullet(this.x, this.y + 4, F_DOWN);
+ }
+ }
+
+ this.fdelay++;
+ if (this.fdelay == FDELAY)
+ {
+ this.fdelay = 0;
+ if (!walking) {
+ if (this.frame != 0) {
+ this.frame = 0;
+ }
+ } else {
+ this.frame++;
+ if (this.frame == WALK_FRAMES.length) {
+ this.frame = 0;
+ }
+ }
+ }
+
+ if (ix > 0)
+ this.dir = DIR_RIGHT;
+ if (ix < 0)
+ this.dir = DIR_LEFT;
+
+ this.y += iy;
+ this.x += ix;
+ }
+
+ draw(ctx) {
+ ctx.drawImage(
+ this.sprites,
+ WALK_FRAMES[this.frame] * this.size.w + this.dir * this.size.w * this.size.frames, 0, this.size.w, this.size.w,
+ this.x, this.y, this.size.w, this.size.h);
+ }
+}
+
+class MyGame extends Game {
+ constructor(canvas) {
+ super(canvas, CANVAS_SIZE.w, CANVAS_SIZE.h);
+
+ this.data = {
+ player: "img/player.png",
+ bullets: "img/bullets.png",
+ slime: "img/slime.png",
+ explo: "img/explo.png",
+ spawn_snd: "snd/spawn.ogg",
+ explo_snd: "snd/explo.ogg",
+ fire_snd: "snd/fire.ogg",
+ damage_snd: "snd/damage.ogg",
+ font: "img/font.png"
+ };
+ this.dataSize = Object.keys(this.data).length;
+
+ this.player = undefined;
+ this.enemies = [];
+ this.effects = [];
+ this.player_fire = [];
+
+ this.wave = 0;
+ this.wave_count = 0;
+ this.score = 0;
+
+ this.damage_snd_delay = 0;
+ this.next_enemy = WAVES[this.wave].delay;
+ }
+
+ updateScore(add) {
+ this.score += add;
+ let s = Array(Math.max(SCORE_W - String(this.score).length + 1, 0)).join(0) + this.score;
+ this.res["score"] = mkCanvasText(this.res["font"], s);
+ }
+
+ updateWave() {
+ this.res["wave"] = mkCanvasText(this.res["font"], `WAVE ${this.wave + 1}`);
+ }
+
+
+ new_game() {
+ this.damage_snd_delay = 0;
+ this.next_enemy = WAVES[0].delay;
+
+ this.enemies = [];
+ this.effects = [];
+ this.player_fire = [];
+
+ this.wave = 0;
+ this.wave_count = 0;
+
+ /* render the score */
+ this.score = 0;
+ this.updateScore(0);
+
+ /* and the wave */
+ this.updateWave();
+
+ this.player = new Player(CANVAS_SIZE.w / 2 - 4, CANVAS_SIZE.h / 2 - 4, this.res["player"], this.keys, (x, y, d) => {
+ this.player_fire.push(new Bullet(x, y, this.res["bullets"], d));
+ this.playSnd(this.res["fire_snd"], false, true);
+ });
+ }
+
+ init() {
+ this.res["game_over"] = mkCanvasText(this.res["font"], "Game Over");
+ this.res["space_restart"] = mkCanvasText(this.res["font"], "(space to restart)");
+
+ this.new_game();
+ }
+
+ update(dt) {
+ if (this.player.is_alive()) {
+ this.player.update(dt);
+ } else {
+ if (this.keys["start"]) {
+ this.new_game();
+ }
+ }
+
+ this.enemies = this.enemies.filter((e) => {
+ if (e.alive) {
+ e.update(dt);
+ }
+ return e.alive;
+ });
+ this.effects = this.effects.filter((e) => {
+ if (e.alive) {
+ e.update(dt);
+ }
+ return e.alive;
+ });
+ this.player_fire = this.player_fire.filter((e) => {
+ if (e.alive) {
+ e.update(dt);
+ if (e.alive) {
+ this.enemies.find((other) => {
+ if (e.collisionWith(other)) {
+ e.alive = false;
+ other.alive = false;
+ this.effects.push(new Explo(other.x, other.y, this.res["explo"]));
+ this.playSnd(this.res["explo_snd"], false, true);
+ this.updateScore(10);
+ this.wave_count++;
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+ return e.alive;
+ });
+
+ let hit = false;
+ if (this.player.is_alive()) {
+ this.enemies.forEach((e) => {
+ if (e.collisionWith(this.player)) {
+ if (this.player.life > 0.1) {
+ this.player.life -= 0.1;
+ } else {
+ this.effects.push(new Explo(this.player.x, this.player.y, this.res["explo"]));
+ this.playSnd(this.res["explo_snd"], false, true);
+ this.player.life = 0;
+ }
+ hit = true;
+ }
+ });
+ if (hit == true) {
+ if (this.damage_snd_delay < DAMAGE_SND_DELAY) {
+ this.damage_snd_delay++;
+ } else {
+ this.damage_snd_delay = 0;
+ this.playSnd(this.res["damage_snd"], false, true);
+ }
+ } else {
+ this.damage_snd_delay = 0;
+ if (this.player.life < MAX_LIFE) {
+ this.player.life += 0.01;
+ }
+ }
+
+ if (this.next_enemy) {
+ this.next_enemy--;
+ } else {
+ if (this.wave_count < WAVES[this.wave].count) {
+ this.next_enemy = WAVES[this.wave].delay;
+ let x = Math.floor(Math.random() * CANVAS_SIZE.w),
+ y = Math.floor(Math.random() * CANVAS_SIZE.h);
+ let enemy = new Slime(x, y, this.res["slime"], () => {
+ return { x: this.player.x, y: this.player.y, is_alive: this.player.is_alive() };
+ });
+ this.effects.push(new Spawn(x + enemy.size.w/2, y + enemy.size.h/2, enemy, (e) => {
+ this.enemies.push(e);
+ }));
+ this.playSnd(this.res["spawn_snd"], false, true);
+ } else {
+ if (this.wave < WAVES.length) {
+ this.wave++;
+ this.updateWave();
+ }
+ this.wave_count = 0;
+ }
+ }
+ }
+ }
+
+ draw() {
+ this.enemies.forEach((e) => {
+ if (e.alive) {
+ e.draw(this.ctx);
+ }
+ });
+ this.effects.forEach((e) => {
+ e.draw(this.ctx);
+ });
+ this.player_fire.forEach((e) => {
+ e.draw(this.ctx);
+ });
+
+ if (this.player.is_alive()) {
+ this.player.draw(this.ctx);
+ }
+
+ /* health bar */
+ this.ctx.beginPath();
+ this.ctx.fillStyle = "black";
+ this.ctx.fillRect(4, 4, 42, 8);
+ this.ctx.fillStyle = "green";
+ this.ctx.fillRect(5, 5, Math.floor(this.player.life * 40 / MAX_LIFE), 6);
+ this.ctx.stroke();
+
+ /* score */
+ this.ctx.drawImage(this.res["score"], CANVAS_SIZE.w - SCORE_W * FONT_SIZE.w - 4, 4);
+
+ /* wave */
+ this.ctx.drawImage(this.res["wave"], 4, CANVAS_SIZE.h - 8 - 4);
+
+ /* game over */
+ if (this.player.life == 0) {
+ this.ctx.drawImage(this.res["game_over"], CANVAS_SIZE.w / 2 - 36, CANVAS_SIZE.h / 2 - 6);
+ this.ctx.drawImage(this.res["space_restart"], CANVAS_SIZE.w / 2 - 76, CANVAS_SIZE.h / 2 + 4);
+ }
+ }
+}