aboutsummaryrefslogtreecommitdiff
path: root/js/mygame.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/mygame.js
downloadjs-twin-shooter-6f5fa0b5240d6edd3bce72ea1549e4cd9b0ce2bc.tar.gz
js-twin-shooter-6f5fa0b5240d6edd3bce72ea1549e4cd9b0ce2bc.zip
Initial import
Diffstat (limited to 'js/mygame.js')
-rw-r--r--js/mygame.js535
1 files changed, 535 insertions, 0 deletions
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);
+ }
+ }
+}