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); } } }