From f6371235f19ef46d007102481c79e706aaa86264 Mon Sep 17 00:00:00 2001 From: quou Date: Sun, 23 Jul 2023 22:57:33 +1000 Subject: Initial commit --- index.html | 21 ++ res/sprites.png | Bin 0 -> 30486 bytes src/main.js | 767 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ss1.png | Bin 0 -> 23493 bytes ss2.png | Bin 0 -> 22516 bytes thumb.png | Bin 0 -> 98930 bytes tile.png | Bin 0 -> 6014 bytes 7 files changed, 788 insertions(+) create mode 100644 index.html create mode 100644 res/sprites.png create mode 100644 src/main.js create mode 100644 ss1.png create mode 100644 ss2.png create mode 100644 thumb.png create mode 100644 tile.png diff --git a/index.html b/index.html new file mode 100644 index 0000000..2c97b5e --- /dev/null +++ b/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/res/sprites.png b/res/sprites.png new file mode 100644 index 0000000..3cdaedc Binary files /dev/null and b/res/sprites.png differ diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..a796981 --- /dev/null +++ b/src/main.js @@ -0,0 +1,767 @@ +var ctx = null; +var canvas = null; + +var player = null; + +var now = 0; +var last = 0; +var ts = 0; + +var camera_offset = 2; + +const sprite_scale = 2; + +var spritesheet = null; + +const sprites = { + player: { x: 0, y: 0, w: 16, h: 16 }, + rock: { x: 0, y: 32, w: 16, h: 16 }, + hard_rock: { x: 96, y: 32, w: 16, h: 16 }, + sky: { x: 112, y: 16, w: 16, h: 16 }, + background: { x: 112, y: 32, w: 16, h: 16 }, + grass: { x: 128, y: 0, w: 16, h: 16 }, + diamond: { x: 112, y: 0, w: 16, h: 16 }, + food_icon: { x: 128, y: 16, w: 16, h: 16 }, + food: { x: 128, y: 32, w: 16, h: 16 }, +} + +const animations = { + player_dig_right: { + frames: [ + { x: 0, y: 0, w: 16, h: 16 }, + { x: 16, y: 0, w: 16, h: 16 }, + { x: 32, y: 0, w: 16, h: 16 }, + { x: 48, y: 0, w: 16, h: 16 }, + { x: 64, y: 0, w: 16, h: 16 }, + { x: 80, y: 0, w: 16, h: 16 }, + { x: 96, y: 0, w: 16, h: 16 }, + { x: 0, y: 0, w: 16, h: 16 }, + ], + speed: 15, + loop: false + }, + player_dig_left: { + frames: [ + { x: 0, y: 16, w: 16, h: 16 }, + { x: 16, y: 16, w: 16, h: 16 }, + { x: 32, y: 16, w: 16, h: 16 }, + { x: 48, y: 16, w: 16, h: 16 }, + { x: 64, y: 16, w: 16, h: 16 }, + { x: 80, y: 16, w: 16, h: 16 }, + { x: 96, y: 16, w: 16, h: 16 }, + { x: 0, y: 16, w: 16, h: 16 }, + ], + speed: 15, + loop: false + }, + player_hurt_left: { + frames: [ + { x: 144, y: 32, w: 16, h: 16 }, + { x: 144, y: 32, w: 16, h: 16 }, + ], + speed: 5, + loop: false + }, + player_hurt_right: { + frames: [ + { x: 144, y: 16, w: 16, h: 16 }, + { x: 144, y: 16, w: 16, h: 16 }, + ], + speed: 5, + loop: false + }, + tile_decay: { + frames: [ + { x: 0, y: 32, w: 16, h: 16 }, + { x: 16, y: 32, w: 16, h: 16 }, + { x: 32, y: 32, w: 16, h: 16 }, + { x: 48, y: 32, w: 16, h: 16 }, + { x: 64, y: 32, w: 16, h: 16 }, + { x: 80, y: 32, w: 16, h: 16 }, + ], + speed: 20, + loop: false + }, + fire: { + frames: [ + { x: 144, y: 0, w: 16, h: 16 }, + { x: 160, y: 0, w: 16, h: 16 }, + { x: 176, y: 0, w: 16, h: 16 }, + ], + speed: 10, + loop: true + } +} + +const spawn_rates = { + diamond: 10 +} + +var characters = {}; + +var animated_tiles = []; +var objs = []; + +var map = null; +var map_width = 0; +var map_height = 0; +var map_value = 0; +const map_bottom_size = 6; +const map_grid_size = 16; + +var collected = 0; +var gem_count = 0; + +var score = 0; +var level = 0; +var food = 100; +const hunger = 1; + +var name_buffer = ""; + +var win = null; +var next_level_timer = 0; + +var state = "menu"; + +function random_int(min, max) { + return Math.floor(Math.random() * (max - min)) + min; +} + +function random_chance(chance) { + if (chance > 100) { + chance = 100; + } + + return random_int(0, 100) < chance; +} + +function remove_from_array(arr, value) { + var idx = arr.indexOf(value); + if (idx !== -1) { + arr.splice(idx, 1); + } +} + +function turn() { + objs.forEach(function(value) { + if (value.turn) { + value.turn(); + } + }); + + for (let i = 0; i < level; i++) { + let x = random_int(1, map_width - 1); + let y = random_int(1, map_height - 1); + + if (map[x][y] == 0 && obj_at(x, y) == null) { + objs.push(new Fire(x, y)); + } + } +} + +function obj_at(x, y) { + for (let i = 0; i < objs.length; i++) { + if (objs[i].go.x == x && objs[i].go.y == y) { + return objs[i]; + } + } + + return null; +} + +function apply_gravity(obj) { + while (map[obj.go.x][obj.go.y + 1] == 0) { + let other_obj = false; + + for (let i = 0; i < objs.length; i++) { + let value = objs[i]; + if (obj.go.x == value.go.x && obj.go.y + 1 == value.go.y) { + other_obj = true; + break; + } + } + + if (other_obj) { + break; + } + + obj.go.y += 1; + } +} + +function GameObject(sprite) { + this.x = 0; + this.y = 0; + + this.rect = sprite; + + this.animation = null; + this.current_frame = 0; + this.timer = 0; + + this.draw = function() { + ctx.drawImage(spritesheet, this.rect.x, this.rect.y, this.rect.w, + this.rect.h, this.x * map_grid_size * sprite_scale, (this.y + camera_offset) * map_grid_size * sprite_scale, + this.rect.w * sprite_scale, this.rect.h * sprite_scale); + } + + this.update = function() { + if (this.animation != null) { + this.timer += ts * this.animation.speed; + + if (this.timer >= 1.0) { + this.current_frame++; + this.timer = 0; + + if (this.current_frame >= this.animation.frames.length) { + if (this.animation.loop) { + this.current_frame = 0; + } else { + this.animation = null; + } + } + + if (this.animation != null) { + this.rect = this.animation.frames[this.current_frame]; + } + } + } + } + + this.animate = function(animation) { + this.animation = animation; + this.current_frame = 0; + } +} + +function DecayTile() { + this.go = new GameObject(sprites.rock); + this.go.animate(animations.tile_decay); + this.go.type = "fire"; + + this.update = function() { + this.go.update(); + + if (this.go.animation == null) { + map[this.go.x][this.go.y] = 0; + remove_from_array(animated_tiles, this); + } + } +} + +function Fire(px, py) { + this.go = new GameObject(animations.fire.frames[0]); + this.go.animate(animations.fire); + + this.go.x = px; + this.go.y = py; + + this.timer = 0; + this.spawned = false; + + this.update = function() { + this.go.update(); + + if (this.go.x == player.go.x && this.go.y == player.go.y) { + food -= 10; + if (player.face == "left") { + player.go.animate(animations.player_hurt_left); + } else { + player.go.animate(animations.player_hurt_right); + } + remove_from_array(objs, this); + } + + if (!this.spawned && this.timer > 3) { + for (let x = this.go.x - 1; x < this.go.x + 2; x++) { + for (let y = this.go.y - 1; y < this.go.y + 2; y++) { + if (random_chance(40) && (x != this.go.x || y != this.go.y) && map[x][y] == 0 && obj_at(x, y) == null) { + objs.push(new Fire(x, y)); + this.timer = 0; + this.spawned = true; + } + } + } + + this.timer = 0; + } else if (this.spawned && this.timer > random_int(0, 4)) { + remove_from_array(objs, this); + } + + apply_gravity(this); + } + + this.draw = function() { + this.go.draw(); + } + + this.turn = function() { + this.timer++; + } +} + +function Gem(x, y, sprite, is_food) { + this.go = new GameObject(sprite); + + this.go.x = x; + this.go.y = y; + + this.value = random_int(10, 25); + + this.update = function() { + this.go.update(); + + if (this.go.x == player.go.x && this.go.y == player.go.y) { + if (!is_food) { + score += this.value; + collected++; + } else { + food += this.value; + } + + remove_from_array(objs, this); + } + + apply_gravity(this); + } + + this.draw = function() { + this.go.draw(); + } +} + +function destroy_tile(x, y) { + if (map[x][y] != 0) { + map[x][y] = 200; + + let obj = new DecayTile(); + obj.go.x = x; + obj.go.y = y; + obj.go.animate(animations.tile_decay); + + animated_tiles[animated_tiles.length + 1] = obj; + } +} + +function Player() { + this.go = new GameObject(sprites.player); + + this.go.x = 11; + + this.next_move_x = null; + this.next_move_y = null; + + this.face = "right"; + + this.draw = function() { + this.go.draw(); + } + + this.update = function() { + this.go.update(); + + while (map[this.go.x][this.go.y + 1] == 0) { + this.go.y += 1; + + if (this.go.y > 10) { + camera_offset -= 1; + } + } + + if (this.go.animation == null) { + if (this.next_move_x != null) { + this.go.x += this.next_move_x; + this.next_move_x = null; + } + + if (this.next_move_y != null) { + this.go.y += this.next_move_y; + + if (this.go.y > 10) { + camera_offset -= 1; + } + this.next_move_y = null; + } + } + + if (food <= 0) { + win = false; + } + + if (collected == gem_count) { + win = true; + } + + if (this.go.y == map_height - 1) { + win = true; + } + } + + this.update_movement = function(key) { + if (this.go.animation == null) { + if (key == 37) { + this.next_move_x = -1; + this.go.rect = animations.player_dig_left.frames[0]; + this.face = "left"; + + let tile = map[this.go.x + this.next_move_x][this.go.y]; + + if (tile == 1) { + destroy_tile(this.go.x + this.next_move_x, this.go.y); + this.go.animate(animations.player_dig_left); + food -= hunger; + } else if (tile == 2) { + this.next_move_x = null; + } + + turn(); + } else if (key == 39) { + this.next_move_x = 1; + this.go.rect = animations.player_dig_right.frames[0]; + this.face = "right"; + + let tile = map[this.go.x + this.next_move_x][this.go.y]; + + if (tile == 1) { + destroy_tile(this.go.x + this.next_move_x, this.go.y); + this.go.animate(animations.player_dig_right); + food -= hunger; + } else if (tile == 2) { + this.next_move_x = null; + } + + turn(); + } else if (key == 40) { + if (this.face == "right") { + this.go.rect = animations.player_dig_right.frames[0]; + this.next_move_x = 1; + } else { + this.go.rect = animations.player_dig_left.frames[0]; + this.next_move_x = -1; + } + + this.next_move_y = 1; + + if (map[this.go.x + this.next_move_x][this.go.y + this.next_move_y] == 1 || + map[this.go.x + this.next_move_x][this.go.y] == 1) { + if (this.face == "right") { + this.go.animate(animations.player_dig_right); + food -= hunger; + } else { + this.go.animate(animations.player_dig_left); + food -= hunger; + } + + if (map[this.go.x + this.next_move_x][this.go.y + this.next_move_y] == 2) { + this.next_move_y = null; + } + + destroy_tile(this.go.x + this.next_move_x, this.go.y); + destroy_tile(this.go.x + this.next_move_x, this.go.y + this.next_move_y); + } else { + this.next_move_x = null; + this.next_move_y = null; + } + + turn(); + } + } + } +} + +function draw_text(start_x, start_y, text) { + let x = start_x; + let y = start_y; + + for (let i = 0; i < text.length; i++) { + let c = text.charAt(i); + + let rect = characters[c]; + + if (rect) { + ctx.drawImage(spritesheet, rect.x, rect.y, rect.w, + rect.h, x, y, + rect.w * sprite_scale, rect.h * sprite_scale); + } + + x += 16 * sprite_scale; + } +} + +function on_init() { + window.addEventListener("keydown", on_key_down) + + canvas = document.createElement("canvas"); + canvas.width = 800; + canvas.height = 600; + + ctx = canvas.getContext("2d"); + ctx.imageSmoothingEnabled= false + + document.body.insertBefore(canvas, document.body.childNodes[0]); + let interval = setInterval(on_update, 20); + + spritesheet = new Image(); + spritesheet.src = "res/sprites.png" + + now = performance.now(); + last = performance.now(); + + let char_order = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; + let cx = 0; + let cy = 0; + for (let i = 0; i < char_order.length; i++) { + let c = char_order.charAt(i); + + characters[c] = { x: cx, y: cy + 48, w: 16, h: 16 }; + + cx += 16; + + if (cx > 240) { + cx = 0; + cy += 16; + } + } + + init_game(true); +} + +function init_game(restart) { + if (restart) { + score = -100; + level = 0; + food = 100; + } + + score += 100; + + level++; + + win = null; + + collected = 0; + + map = null; + next_level_timer = 0; + + animated_tiles.length = 0; + objs.length = 0; + + camera_offset = 2; + + player = new Player(); + + map_width = Math.round((canvas.width / (sprites.rock.w * sprite_scale))); + map_height = Math.round((canvas.height / (sprites.rock.h * sprite_scale))) * 2; + + map = new Array(map_width); + for (let i = 0; i < map_width; i++) { + map[i] = new Array(map_height); + } + + for (let y = 0; y < map_height; y++) { + for (let x = 0; x < map_width; x++) { + if (random_chance(1)) { + map[x][y] = 0; + } else { + map[x][y] = 1; + } + } + } + + /* Spawn gems */ + gem_count = random_int(10, 20); + for (let i = 0; i < gem_count; i++) { + let x = random_int(1, map_width - 1); + let y = random_int(1, map_height - 1); + + map[x][y] = 0; + + objs.push(new Gem(x, y, sprites.diamond, false)); + } + + /* Spawn fire */ + let fire_count = random_int(5, 15); + for (let i = 0; i < fire_count; i++) { + let x = random_int(1, map_width - 1); + let y = random_int(1, map_height - 1); + + map[x][y] = 0; + + objs.push(new Fire(x, y)); + } + + /* Spawn food */ + let food_count = random_int(2, 10); + for (let i = 0; i < food_count; i++) { + let x = random_int(1, map_width - 1); + let y = random_int(1, map_height - 1); + + map[x][y] = 0; + + let obj = new Gem(x, y, sprites.food, true); + obj.value = random_int(10, 15); + objs.push(obj); + } + + for (let y = 0; y < map_height; y++) { + map[0][y] = 2; + } + + for (let y = 0; y < map_height; y++) { + map[map_width - 1][y] = 2; + } + + for (let x = 0; x < map_width; x++) { + for (let y = map_height; y < map_height + map_bottom_size; y++) { + map[x][y] = 2; + } + } + + map[10][0] = 0; + map[11][0] = 0; + map[12][0] = 0; + map[13][0] = 0; +} + +function on_update() { + if (state == "game") { + if (win == null) { + player.update(); + + animated_tiles.forEach(function(value) { + value.update(); + }); + + objs.forEach(function(value) { + value.update(); + }); + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + /* Draw the sky */ + for (let y = 0; y < 2; y++) { + for (let x = 0; x < map_width; x++) { + let rect = sprites.sky; + + ctx.drawImage(spritesheet, rect.x, rect.y, rect.w, rect.h, + x * map_grid_size * sprite_scale, (y + camera_offset - 2) * map_grid_size * sprite_scale, rect.w * sprite_scale, rect.h * sprite_scale); + } + } + + /* Draw the grass */ + for (let x = 0; x < map_width; x++) { + let y = 1; + let rect = sprites.grass; + + if (map[x][0] != 0) { + ctx.drawImage(spritesheet, rect.x, rect.y, rect.w, rect.h, + x * map_grid_size * sprite_scale, (y + camera_offset - 2) * map_grid_size * sprite_scale, rect.w * sprite_scale, rect.h * sprite_scale); + } + } + + /* Draw the background */ + for (let y = 2; y < map_height + 2; y++) { + for (let x = 0; x < map_width; x++) { + let rect = sprites.background; + + ctx.drawImage(spritesheet, rect.x, rect.y, rect.w, rect.h, + x * map_grid_size * sprite_scale, (y + camera_offset - 2) * map_grid_size * sprite_scale, rect.w * sprite_scale, rect.h * sprite_scale); + } + } + + /* Draw the map */ + for (let y = 0; y < map_height + map_bottom_size; y++) { + for (let x = 0; x < map_width; x++) { + let rect = null; + + if (map[x][y] == 1) { + rect = sprites.rock; + } else if (map[x][y] == 2) { + rect = sprites.hard_rock; + } + + if (rect != null) { + ctx.drawImage(spritesheet, rect.x, rect.y, rect.w, rect.h, + x * map_grid_size * sprite_scale, (y + camera_offset) * map_grid_size * sprite_scale, rect.w * sprite_scale, rect.h * sprite_scale); + } + } + } + + animated_tiles.forEach(function(value) { + value.go.draw(); + }); + + objs.forEach(function(value) { + value.go.draw(); + }); + + player.draw(); + + let score_text = `Score: ${score}`; + let level_text = `Level: ${level}`; + draw_text(canvas.width - score_text.length * 16 * sprite_scale, 0, score_text); + draw_text(canvas.width - level_text.length * 16 * sprite_scale, 16 * sprite_scale, level_text); + + let food_text = `${Math.round(food)}%`; + let r = sprites.food_icon; + ctx.drawImage(spritesheet, r.x, r.y, r.w, r.h, + 0, 0, r.w * sprite_scale, r.h * sprite_scale); + draw_text(16 * sprite_scale, 0, food_text); + + if (win != null) { + let win_text = "Game Over!"; + + if (win) { + win_text = "Level Up!"; + } + + draw_text((canvas.width / 2) - ((win_text.length * 16 * sprite_scale) / 2), + (canvas.height / 2) - ((16 * sprite_scale) / 2), win_text); + + next_level_timer += ts; + + if (win && next_level_timer >= 3) { + init_game(true); + } + } + + now = performance.now(); + ts = (now - last) / 1000; + last = now; + } else if (state == "menu") { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + let text = "[S]tart"; + draw_text((canvas.width / 2) - ((text.length * 16 * sprite_scale) / 2), + ((canvas.height / 2) - ((16 * sprite_scale) / 2)) - 16 * sprite_scale, text); + text = "MINI MINER"; + draw_text((canvas.width / 2) - ((text.length * 16 * sprite_scale) / 2), + (canvas.height / 2) - ((16 * sprite_scale) / 2) - 16 * 3 * sprite_scale, text); + } +} + +function on_key_down(e) { + e.preventDefault(); + + if (e.keyCode == 75) { /* K */ + food = 0; + } + + if (e.keyCode == 80) { /* K */ + score += 10; + } + + if (state == "menu") { + if (e.keyCode == 83) { /* S */ + state = "game"; + init_game(true); + } + } else if (state == "game") { + if (win == null) { + player.update_movement(e.keyCode); + } + } +} diff --git a/ss1.png b/ss1.png new file mode 100644 index 0000000..9bcbd43 Binary files /dev/null and b/ss1.png differ diff --git a/ss2.png b/ss2.png new file mode 100644 index 0000000..26471a6 Binary files /dev/null and b/ss2.png differ diff --git a/thumb.png b/thumb.png new file mode 100644 index 0000000..d11635d Binary files /dev/null and b/thumb.png differ diff --git a/tile.png b/tile.png new file mode 100644 index 0000000..1cd3b81 Binary files /dev/null and b/tile.png differ -- cgit v1.2.3-54-g00ecf