--- /dev/null
+*.xcf
+*.swp
+*.swo
--- /dev/null
+"use strict";
+
+let game;
+
+let game_started = false;
+
+let just_started = true;
+
+let level_number = 1;
+
+let map;
+
+let buttons = {};
+
+let max_unlocked_level;
+let continue_level;
+
+let how_to_play = false;
+
+let disable_transitions = false;
+let disable_colors = false;
+
+let won_everything = false;
+
+let KINDS_OF_FISH = 5;
+
+/* --------- definitions ---------- */
+
+/* state:
+ * STAND: waiting for input
+ * BLOCK: moving block
+ * DRAG: actually dragging block
+ * FISH: moving fish or shark
+ * CONGREGATE: swim together to win
+ * WIN: level complete
+ * LOSESWIM: shark swimming to eat us
+ * LOSE: got eaten by shark
+ */
+let State = { STAND: 0, BLOCK: 1, DRAG: 2, FISH: 3, SWIM: 4, CONGREGATE: 5, WIN: 6, LOSESWIM: 7, LOSE: 8 };
+
+let UIState = { MENU: 0, LEVELSELECT: 1, OPTIONS: 2, GAME: 3, REALLYQUIT: 4, REALLYDELETE: 5 };
+
+let can_continue = false;
+
+let save_data = 1;
+const SAVE_KEY = "casso.continentalshelf.save"
+
+zb.ready(function() {
+ game = zb.create_game({
+ canvas: 'canvas',
+ canvas_w: 576,
+ canvas_h: 864,
+ draw_scale: 3,
+ tile_size: 24,
+ level_w: 8,
+ level_h: 12,
+ background_color: '#192c4a',
+ run_in_background: true,
+ load_with_progress_bar: false,
+ save_key: SAVE_KEY,
+ state: State.STAND,
+ uistate: UIState.MENU,
+ events: {
+ keyup: handle_keyup,
+ mouseup: handle_mouseup,
+ mouseleave: handle_mouseleave,
+ mousedown: handle_mousedown,
+ mousemove: handle_mousemove,
+ gamestart: handle_gamestart,
+ },
+ modes: {
+ menu: {
+ draw: do_draw_menu,
+ },
+ youwon: {
+ draw: do_draw_youwon,
+ },
+ game: {
+ draw: do_draw,
+ update: do_update,
+ },
+ },
+ mode: 'menu',
+ buttons: {},
+ });
+
+ game.register_images({
+ fish: 'img/fish.png',
+ selector: 'img/selector.png',
+ x: 'img/x.png',
+ heart: 'img/heart.png',
+ bubble: 'img/bubble.png',
+ move_tile: 'img/move_tile.png',
+ wall_tile: 'img/wall_tile.png',
+ decorations: 'img/decoration.png',
+ levelnums: 'img/levelnums.png',
+ success: 'img/success.png',
+ failure: 'img/failure.png',
+ buttons: 'img/buttons.png',
+ menubuttons: 'img/menubuttons.png',
+ menuimage: 'img/menuimage.png',
+ youwon: 'img/youwon.png',
+ levels: {
+ 1: 'img/level/1.png',
+ 2: 'img/level/2.png',
+ 4: 'img/level/4.png',
+ }
+ });
+
+ game.register_sfx({
+ victory: {
+ path: 'sfx/victory.wav',
+ volume: 0.3,
+ },
+ fail: {
+ path: 'sfx/fail.wav',
+ volume: 0.8,
+ },
+ drip: {
+ path: 'sfx/drip.wav',
+ volume: 1,
+ },
+ drag: {
+ path: 'sfx/drag.wav',
+ volume: 0.19,
+ },
+ bubble: {
+ path: 'sfx/bubble.wav',
+ volume: 0.5,
+ },
+ bubble2: {
+ path: 'sfx/bubble2.wav',
+ volume: 0.5,
+ },
+ bubble3: {
+ path: 'sfx/bubble3.wav',
+ volume: 0.5,
+ },
+ bubble4: {
+ path: 'sfx/bubble4.wav',
+ volume: 0.5,
+ },
+ });
+
+ game.register_music({
+ bgm: {
+ path: 'music/sharkgamebeachmusic',
+ volume: 0.1,
+ },
+ });
+
+ game.resources_ready();
+
+ continue_level = parseInt(game.load("continue_level") || "1");
+
+ game.buttons.ingame = zb.buttons.create({
+ img: game.img.buttons,
+ button_w: 24,
+ button_h: 24,
+ buttons: [
+ {
+ /* Undo */
+ x: game.screen_w - 72,
+ y: game.screen_h - 24,
+ id: 0,
+ callback: undo,
+ },
+ {
+ /* Reset */
+ x: game.screen_w - 48,
+ y: game.screen_h - 24,
+ id: 1,
+ callback: reset,
+ },
+ {
+ /* Toggle mute */
+ x: game.screen_w - 24,
+ y: game.screen_h - 24,
+ id: 2,
+ callback: toggle_mute_button,
+ },
+ ],
+ });
+
+ game.buttons.menu = zb.buttons.create({
+ img: game.img.menubuttons,
+ button_w: 72,
+ button_h: 16,
+ buttons: [
+ {
+ /* New game */
+ x: game.screen_w / 2 - 36,
+ y: game.screen_h / 2 + 36,
+ id: 0,
+ callback: new_game_button,
+ },
+ {
+ /* Continue */
+ x: game.screen_w / 2 - 36,
+ y: game.screen_h / 2 + 60,
+ id: 1,
+ callback: continue_button,
+ disabled: continue_level === 1,
+ },
+ ],
+ });
+});
+
+let tileID = {
+ blank: 0,
+ wall: 1,
+};
+
+let creatureID = {
+ fish: 0,
+ shark: 1,
+};
+
+let creature_type_mapping = {
+ 0: creatureID.fish,
+ 1: creatureID.fish,
+ 2: creatureID.fish,
+ 3: creatureID.fish,
+ 4: creatureID.fish,
+ 5: creatureID.shark,
+};
+
+/* ------ timers & static timer values --------- */
+
+const STAND_FRAME_LENGTH = 500;
+const SWIM_FRAME_LENGTH = 200;
+
+const SELECTOR_FRAME_LENGTH = 300;
+
+const BLOCK_MOVE_SPEED = 12;
+
+const SWIM_SPEED = 6;
+const LOSE_SWIM_SPEED = 8;
+const WIN_SWIM_SPEED = 5;
+
+const X_FRAME_LENGTH = 100;
+const MAX_X_FRAME = 12;
+
+const BUBBLE_FRAME_LENGTH = 300;
+const BUBBLE_UP_SPEED = 15;
+
+const SCREEN_MESSAGE_DELAY = 600;
+const SCREEN_MESSAGE_FADEIN_LENGTH = 1000;
+
+const DRIFT_SPEED = 3000;
+
+/* ------- game global state -------- */
+
+let mouse_x = -1, mouse_y = -1, mouse_is_down = false;
+
+let fish = [];
+
+let blocks = [];
+
+let hearts = [];
+
+let bubbles = [];
+
+let decorations = [];
+
+let hovered_block = null;
+
+let hovered_fish = null;
+
+let eaten_fish = null;
+
+let start_drag_x = 0, start_drag_y = 0;
+
+let selector_timer = 0;
+let selector_frame = 0;
+
+let potential_fish_target = null;
+let fish_path = null;
+
+let x_timer = 0;
+let x_frame = 0;
+
+let just_clicked_block = true;
+
+let screen_message_delay = 0;
+let screen_message_alpha = 0;
+
+/* ------- game behavior functions -------- */
+
+function tile_at(x, y) {
+ if (x < 0 || x >= game.level_w) return tileID.wall;
+ if (y < 0 || y >= game.level_h) return tileID.wall;
+
+ let result = map[y * game.level_w + x];
+ return result;
+}
+
+function fish_at(x, y) {
+ let filtered_fish = fish.filter(o => o.x === x && o.y === y);
+ if (filtered_fish.length > 0) {
+ return filtered_fish[0];
+ }
+ return null;
+}
+
+function block_at(x, y) {
+ for (let b of blocks) {
+ for (let t of b.tiles) {
+ if (t[0] === x && t[1] === y) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+function tile_or_block_at(x, y) {
+ return tile_at(x, y) === tileID.wall || block_at(x, y);
+}
+
+function anything_at(x, y, ignore_block) {
+ if (tile_at(x, y) === tileID.wall) {
+ return true;
+ }
+
+ for (let f of fish) {
+ if (f.x === x && f.y === y) {
+ return true;
+ }
+ }
+
+ for (let b of blocks) {
+ if (ignore_block && ignore_block === b) {
+ continue;
+ }
+ for (let t of b.tiles) {
+ if (t[0] === x && t[1] === y) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+function anything_except_fish_at(x, y, ignore_block) {
+ if (tile_at(x, y) === tileID.wall) {
+ return true;
+ }
+
+ for (let f of fish) {
+ if (f.x === x && f.y === y && f.species !== creatureID.fish) {
+ return true;
+ }
+ }
+
+ for (let b of blocks) {
+ if (ignore_block && ignore_block === b) {
+ continue;
+ }
+ for (let t of b.tiles) {
+ if (t[0] === x && t[1] === y) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+function sofc(coords) {
+ return "" + coords[0] + "," + coords[1];
+}
+
+function cofs(string) {
+ return string.split(",").map(a => parseInt(a, 10));
+}
+
+function path_between(start_x, start_y, end_x, end_y, end_can_be_blocked, ignore_fish) {
+ /* Pathfinding, for fish */
+
+ if (anything_at(end_x, end_y) && end_can_be_blocked) {
+ console.log("very no");
+ return null;
+ }
+
+ let queue = [[start_x, start_y, null]];
+ let checked = {};
+
+ while (queue.length > 0) {
+ let next_pos = queue.shift();
+
+ if (checked.hasOwnProperty(sofc(next_pos))) {
+ continue;
+ }
+
+ checked[sofc(next_pos)] = next_pos[2];
+
+ if (next_pos[0] === end_x && next_pos[1] === end_y) {
+ console.log("yes");
+ let path = [];
+ while (next_pos !== null) {
+ path.unshift([next_pos[0], next_pos[1]]);
+ next_pos = checked[sofc(next_pos)];
+ }
+ return path;
+ }
+
+ if ((next_pos[0] !== start_x || next_pos[1] !== start_y)
+ && (ignore_fish || anything_at(next_pos[0], next_pos[1]))
+ && (!ignore_fish || anything_except_fish_at(next_pos[0], next_pos[1]))) {
+ continue;
+ }
+
+ queue.push([next_pos[0] + 1, next_pos[1], [next_pos[0], next_pos[1]]]);
+ queue.push([next_pos[0] - 1, next_pos[1], [next_pos[0], next_pos[1]]]);
+ queue.push([next_pos[0], next_pos[1] + 1, [next_pos[0], next_pos[1]]]);
+ queue.push([next_pos[0], next_pos[1] - 1, [next_pos[0], next_pos[1]]]);
+ }
+
+ console.log("no");
+ return null;
+}
+
+let undo_stack = [];
+
+function create_undo_point() {
+ console.log("cup");
+
+ let blocks_copy = [];
+ for (let b of blocks) {
+ let new_block = {
+ state: 0,
+ tiles: [],
+ target_dx: 0,
+ target_dy: 0,
+ move_fraction: 0,
+ };
+ for (let coord of b.tiles) {
+ new_block.tiles.push( [ coord[0], coord[1] ] );
+ }
+ blocks_copy.push(new_block);
+ }
+
+ undo_stack.push({
+ fish: zb.copy_flat_objlist(fish),
+ blocks: blocks_copy,
+ });
+}
+
+function make_normal_state() {
+ game.state = State.STAND;
+ x_frame = 0;
+ hearts = [];
+ bubbles = [];
+ potential_fish_target = null;
+ hovered_fish = null;
+ screen_message_alpha = 0;
+ screen_message_delay = SCREEN_MESSAGE_DELAY;
+}
+
+function undo() {
+ console.log("undo")
+ if (undo_stack.length > 0) {
+ let undo_point = undo_stack.pop();
+ fish = undo_point.fish;
+ blocks = undo_point.blocks;
+ make_normal_state();
+ }
+}
+
+function reset() {
+ console.log("reset");
+ create_undo_point();
+ game.start_transition(zb.transition.type.fade, 300, function() {
+ load_level();
+ });
+}
+
+function advance_level() {
+ if (!can_continue) return;
+
+ undo_stack = [];
+
+ if (level_number + 1 > Math.max(...Object.keys(levels).map(a => parseInt(a, 10) || 0))) {
+ game.long_transition(zb.transition.type.fade, 1000, function() {
+ reset_save();
+ game.mode = 'youwon';
+ });
+ } else {
+ game.start_transition(zb.transition.type.fade, 1000, function() {
+ level_number ++;
+ load_level();
+ make_normal_state();
+ });
+ }
+}
+
+function skip_to(num) {
+ level_number = num;
+ load_level();
+}
+
+function win_everything() {
+ console.log("WIN!!");
+}
+
+function load_level() {
+ load_level_data(levels[level_number]);
+}
+
+function load_level_data(lvl) {
+ map = lvl.map;
+
+ fish = [];
+ blocks = [];
+
+ make_normal_state();
+
+ decorations = [];
+
+ let fish_type = zb.mod(level_number || 0, KINDS_OF_FISH);
+ if (isNaN(level_number)) {
+ fish_type = 0;
+ }
+ for (let f of lvl.fish) {
+ fish.push({
+ x: f[0],
+ y: f[1],
+ type: fish_type,
+ species: creatureID.fish,
+ frame: 0,
+ timer: Math.random() * STAND_FRAME_LENGTH,
+ drift: Math.random(),
+ });
+ fish_type ++;
+ fish_type %= KINDS_OF_FISH;
+ }
+
+ for (let s of lvl.sharks) {
+ fish.push({
+ x: s[0],
+ y: s[1],
+ type: KINDS_OF_FISH,
+ species: creatureID.shark,
+ frame: 0,
+ timer: Math.random() * STAND_FRAME_LENGTH,
+ drift: Math.random(),
+ });
+ }
+
+ for (let b of lvl.blocks) {
+ let new_block = {
+ state: 0,
+ tiles: [],
+ target_dx: 0,
+ target_dy: 0,
+ move_fraction: 0,
+ };
+ for (let coord of b) {
+ new_block.tiles.push( [ coord[0], coord[1] ] );
+ }
+ blocks.push(new_block);
+ }
+
+ for (let d of lvl.decorations) {
+ decorations.push({
+ x: d[0],
+ y: d[1],
+ type: d[2],
+ });
+ }
+
+ game.state = State.STAND;
+}
+
+function save() {
+ console.log("Saving");
+ let save_data = level_number;
+ if (game.state === State.WIN) {
+ save_data = level_number + 1;
+ }
+ game.save('continue_level', save_data);
+}
+
+function reset_save() {
+ game.save('continue_level', 1);
+}
+
+function win() {
+ //game.sfx.victory.play();
+ console.log("You won!");
+ game.state = State.WIN;
+ game.sfx.victory.play();
+
+ let heart_x = 0;
+ let heart_y = 0;
+ for (let f of fish) {
+ if (f.species === creatureID.fish) {
+ f.hidden = true;
+ heart_x = f.x;
+ heart_y = f.y;
+ }
+ }
+
+ for (let i = 0; i < 20; i++) {
+ create_heart(heart_x * game.tile_size + game.tile_size / 2 + Math.random() * 10 - 5,
+ heart_y * game.tile_size + game.tile_size / 2 + Math.random() * 10 - 5);
+ }
+
+ save();
+
+ window.setTimeout(function() {
+ can_continue = true;
+ }, 350);
+}
+
+function lose() {
+ game.state = State.LOSE;
+ game.sfx.fail.play();
+ x_timer = 0;
+ x_frame = 0;
+ eaten_fish.hidden = true;
+}
+
+function check_status() {
+ /* Check lose: a shark and a fish in the same area */
+ for (let f of fish) {
+ if (f.species === creatureID.shark) {
+ for (let g of fish) {
+ if (g.species === creatureID.fish) {
+ let path = path_between(f.x, f.y, g.x, g.y, false);
+ if (path) {
+ game.state = State.LOSESWIM;
+ hovered_fish = f;
+ eaten_fish = g;
+ fish_path = path;
+ start_swimming(hovered_fish);
+ break;
+ }
+ }
+ }
+ if (game.state === State.LOSESWIM) break;
+ }
+ }
+
+ /* Check win: all fish in same area */
+ /* find first fish and see if it paths to all others */
+ let first_fish = null;
+ for (let f of fish) {
+ if (f.species === creatureID.fish) {
+ first_fish = f;
+ break;
+ }
+ }
+
+ let won = true;
+ for (let f of fish) {
+ if (f === first_fish) continue;
+ if (f.species !== creatureID.fish) continue;
+ let path = path_between(first_fish.x, first_fish.y, f.x, f.y, false, true);
+ if (path === null) {
+ won = false;
+ break;
+ }
+ }
+
+ if (won) {
+ console.log("omg win");
+ game.state = State.CONGREGATE;
+ /* DFS to find spot closest to all fish */
+ for (let f of fish) {
+ if (f.species !== creatureID.fish) continue;
+ f.explored_territory = new Set();
+ f.explored_territory.add(sofc([f.x, f.y]));
+ }
+
+ let found_thing = false;
+ let common_square = null;
+ let bailout = 0;
+ while (!found_thing) {
+ for (let f of fish) {
+ if (f.species !== creatureID.fish) continue;
+ let new_explored_territory = new Set();
+ for (let e of f.explored_territory) {
+ let coords = cofs(e);
+ let neighbors = [
+ [coords[0] + 1, coords[1]],
+ [coords[0] - 1, coords[1]],
+ [coords[0], coords[1] + 1],
+ [coords[0], coords[1] - 1],
+ ];
+ for (let n of neighbors) {
+ if (n[0] < 0 || n[0] >= game.level_w || n[1] < 0 || n[1] >= game.level_h) continue;
+ if (f.explored_territory.has(sofc(n))) continue;
+ if (anything_except_fish_at(n[0], n[1])) continue;
+ new_explored_territory.add(sofc(n));
+ }
+ }
+ f.explored_territory = f.explored_territory.union(new_explored_territory);
+ }
+
+ let common_squares = fish[0].explored_territory;
+ for (let f of fish) {
+ if (f.species !== creatureID.fish) continue;
+ common_squares = common_squares.intersection(f.explored_territory);
+ }
+
+ bailout ++;
+ if (bailout > 1000) found_thing = true;
+
+ if (common_squares.size > 0) {
+ common_square = cofs(Array.from(common_squares)[0]);
+ found_thing = true;
+ }
+ }
+
+ if (common_square === null) {
+ console.error("Assert this should not happen!");
+ }
+
+ /* ok, now we found where all the fish will swim to. and we know they all have a path. so figure out the path for each fish */
+ let max_win_path_length = 0;
+ for (let f of fish) {
+ if (f.species !== creatureID.fish) continue;
+ f.win_path = path_between(f.x, f.y, common_square[0], common_square[1], false, true);
+ if (f.win_path.length > max_win_path_length) {
+ max_win_path_length = f.win_path.length;
+ }
+ }
+
+ /* now we want them to reach it at the same time. so scale each one's speed proportionately to their path length */
+ for (let f of fish) {
+ if (f.species !== creatureID.fish) continue;
+ f.win_swim_speed = WIN_SWIM_SPEED * (f.win_path.length - 1) / (max_win_path_length - 1);
+ start_swimming(f);
+ }
+ }
+}
+
+function start_swimming(fish) {
+ fish.target_x = fish.x;
+ fish.target_y = fish.y;
+ fish.swimming = true;
+ fish.move_fraction = 1;
+}
+
+function create_heart(x, y) {
+ hearts.push({
+ x: Math.round(x),
+ y: Math.round(y),
+ xspeed: Math.random() * 60 - 30,
+ yspeed: Math.random() * -30 - 30,
+ gravity: 50,
+ });
+}
+
+/* ---------- update functions ------------ */
+
+/* MAIN UPDATE FUNCTION */
+function do_update(delta) {
+ update_bubbles(delta);
+
+ if (game.state === State.WIN || game.state === State.LOSE) {
+ if (screen_message_delay > 0) {
+ screen_message_delay -= delta;
+ } else {
+ if (screen_message_alpha < 1) {
+ screen_message_alpha += delta / SCREEN_MESSAGE_FADEIN_LENGTH;
+ } else {
+ screen_message_alpha = 1;
+ }
+ }
+ }
+
+ for (let f of fish) {
+ f.drift += delta / DRIFT_SPEED;
+
+ f.timer += delta;
+
+ let max_time = STAND_FRAME_LENGTH;
+ if (f.swimming) {
+ max_time = SWIM_FRAME_LENGTH;
+ }
+ while (f.timer > max_time) {
+ f.timer -= max_time;
+ f.frame ++;
+ f.frame %= 2;
+ }
+ }
+
+ selector_timer += delta;
+ while (selector_timer > SELECTOR_FRAME_LENGTH) {
+ selector_timer -= SELECTOR_FRAME_LENGTH;
+ selector_frame ++;
+ selector_frame %= 2;
+ }
+
+ if (game.state === State.DRAG) {
+ hovered_block.move_fraction += BLOCK_MOVE_SPEED * delta / 1000;
+ if (hovered_block.move_fraction > 1) {
+ for (let t of hovered_block.tiles) {
+ t[0] += hovered_block.target_dx;
+ t[1] += hovered_block.target_dy;
+ }
+ hovered_block.move_fraction = 0;
+ start_drag_x += hovered_block.target_dx;
+ start_drag_y += hovered_block.target_dy;
+ hovered_block.target_dx = 0;
+ hovered_block.target_dy = 0;
+ if (mouse_is_down) {
+ game.state = State.BLOCK;
+ } else {
+ game.state = State.STAND;
+ }
+
+ /* Check if we won or lost */
+ check_status();
+
+ if (game.state !== State.CONGREGATE && game.state !== State.LOSESWIM) {
+ /* Check if we need to move more */
+ if (mouse_is_down) {
+ handle_mousemove(game, {}, mouse_x, mouse_y);
+ }
+ } else {
+ hovered_block = null;
+ for (let b of blocks) {
+ b.state = 0;
+ }
+ }
+ }
+ } else if (game.state === State.SWIM || game.state === State.LOSESWIM) {
+ let speed = SWIM_SPEED;
+
+ if (game.state === State.LOSESWIM) {
+ speed = LOSE_SWIM_SPEED;
+ }
+
+ let done = update_fish_swim(delta, hovered_fish, fish_path, speed);
+
+ if (done) {
+ if (game.state === State.SWIM) {
+ game.state = State.STAND;
+ } else if (game.state === State.LOSESWIM) {
+ lose();
+ }
+ if (game.state === State.SWIM) {
+ fish = null;
+ }
+ potential_fish_target = null;
+ handle_mousemove(game, {}, mouse_x, mouse_y);
+ }
+ } else if (game.state === State.CONGREGATE) {
+ let all_done = true;
+ for (let f of fish) {
+ if (f.species === creatureID.fish) {
+ let done = update_fish_swim(delta, f, f.win_path, f.win_swim_speed);
+ if (f.win_path.length > 0) {
+ all_done = false;
+ }
+ }
+ }
+
+ if (all_done) {
+ win();
+ }
+ } else if (game.state === State.LOSE) {
+ if (x_frame < MAX_X_FRAME) {
+ x_timer += delta;
+ while (x_timer > X_FRAME_LENGTH) {
+ x_timer -= X_FRAME_LENGTH;
+ x_frame ++;
+ }
+ }
+ } else if (game.state === State.WIN) {
+ update_hearts(delta);
+ }
+}
+
+function update_fish_swim(delta, fish, path, speed) {
+ fish.move_fraction += speed * delta / 1000;
+ if (fish.move_fraction > 1) {
+ fish.x = fish.target_x;
+ fish.y = fish.target_y;
+ fish.move_fraction = 0;
+ if (path.length > 0 && path[0][0] === fish.x && path[0][1] === fish.y) {
+ /* Pop off start coordinate from path... */
+ path.shift();
+ }
+
+ if (path.length == 0) {
+ console.log("Swim done");
+ if (game.state === State.SWIM) {
+ fish.rotate = 0;
+ }
+ fish.swimming = false;
+ return true;
+ } else {
+ fish.target_x = path[0][0];
+ fish.target_y = path[0][1];
+
+ let old_rotate = fish.rotate;
+ let old_hflip = fish.hflip;
+
+ if (fish.target_x > fish.x) {
+ fish.hflip = false;
+ } else if (fish.target_x < fish.x) {
+ fish.hflip = true;
+ }
+
+ if (fish.target_y > fish.y) {
+ fish.rotate = 90;
+ } else if (fish.target_y < fish.y) {
+ fish.rotate = -90;
+ } else {
+ fish.rotate = 0;
+ }
+
+ if (fish.rotate !== old_rotate || fish.hflip !== old_hflip) {
+ game.sfx.drip.play();
+ }
+ }
+ }
+ return false;
+}
+
+function update_hearts(delta) {
+ for (let h of hearts) {
+ h.x += h.xspeed * delta / 1000;
+ h.y += h.yspeed * delta / 1000;
+ h.yspeed += h.gravity * delta / 1000;
+ }
+}
+
+let bubble_sfx_timeout = 0;
+function create_bubble(x, y) {
+ bubbles.push({
+ x: x,
+ y: y,
+ frame: Math.floor(Math.random() * 2),
+ timer: Math.random() * BUBBLE_FRAME_LENGTH,
+ });
+
+ if (Math.random() < 0.8 && bubble_sfx_timeout <= 0) {
+ let decider = Math.random();
+ if (decider < 0.25) {
+ game.sfx.bubble.play();
+ } else if (decider < 0.5) {
+ game.sfx.bubble2.play();
+ } else if (decider < 0.75) {
+ game.sfx.bubble3.play();
+ } else {
+ game.sfx.bubble4.play();
+ }
+ bubble_sfx_timeout = 800;
+ }
+}
+
+function update_bubbles(delta) {
+ bubble_sfx_timeout -= delta;
+
+ for (let f of fish) {
+ if (!f.hasOwnProperty("bubble_timeout")) {
+ f.bubble_timeout = 0;
+ }
+ f.bubble_timeout -= delta;
+
+ if (f.swimming) continue;
+ if (f.hidden) continue;
+ if (f.bubble_timeout > 0) continue;
+
+ if (Math.random() < 0.002) {
+ let x_offset = 0;
+
+ if (f.species === creatureID.fish) {
+ if (f.hflip) {
+ x_offset = 6;
+ } else {
+ x_offset = 18;
+ }
+ } else if (f.species === creatureID.shark) {
+ if (f.hflip) {
+ x_offset = 3;
+ } else {
+ x_offset = 21;
+ }
+ }
+
+ create_bubble(f.x * game.tile_size + x_offset, f.y * game.tile_size + game.tile_size / 2);
+
+ f.bubble_timeout = 1500;
+ }
+ }
+
+ for (let d of decorations) {
+ if (!d.hasOwnProperty("bubble_timeout")) {
+ d.bubble_timeout = 0;
+ }
+ d.bubble_timeout -= delta;
+
+ if (d.bubble_timeout > 0) continue;
+ if (tile_at(d.x, d.y) === tileID.wall) continue;
+
+ if (Math.random() < 0.001) {
+ let x_offset = Math.random() * game.tile_size * 2 / 3 - game.tile_size / 3;
+
+ create_bubble(d.x * game.tile_size + game.tile_size / 2 + x_offset, d.y * game.tile_size + game.tile_size / 2);
+
+ d.bubble_timeout = 3000;
+ }
+ }
+
+ for (let b of bubbles) {
+ b.timer += delta;
+ while (b.timer > BUBBLE_FRAME_LENGTH) {
+ b.timer -= BUBBLE_FRAME_LENGTH;
+ b.frame ++;
+ b.frame %= 2;
+ }
+ b.y -= BUBBLE_UP_SPEED * delta / 1000;
+ if (zb.mod(b.y, game.tile_size) < game.tile_size / 2) {
+ if (tile_or_block_at(Math.floor(b.x / game.tile_size), Math.floor(b.y / game.tile_size))) {
+ b.deleteme = true;
+ }
+ }
+ }
+
+ bubbles = bubbles.filter(b => !b.deleteme);
+}
+
+/* ---------- draw functions ----------- */
+
+/* DRAW */
+function do_draw(ctx) {
+ draw_bubbles(ctx);
+
+ draw_map(ctx);
+
+ if (!isNaN(parseInt(level_number))) {
+ zb.sprite_draw(ctx, game.img.levelnums, game.tile_size, game.tile_size, 0, level_number - 1, 0, (game.level_h - 1) * game.tile_size);
+ }
+
+ draw_decorations(ctx);
+
+ zb.buttons.draw(ctx, game.buttons.ingame);
+
+ if (game.img.levels.hasOwnProperty(level_number)) {
+ zb.screen_draw(ctx, game.img.levels[level_number]);
+ }
+
+ draw_blocks(ctx);
+
+ draw_creatures(ctx);
+
+ draw_hearts(ctx);
+
+ if (potential_fish_target) {
+ zb.sprite_draw(ctx, game.img.selector, 28, 28, 0, zb.mod(selector_frame + 1, 2),
+ potential_fish_target[0] * game.tile_size - 2, potential_fish_target[1] * game.tile_size - 2);
+ }
+
+ if (game.state === State.LOSE) {
+ zb.sprite_draw(ctx, game.img.x, 48, 48, 0, x_frame, hovered_fish.x * game.tile_size - 12, hovered_fish.y * game.tile_size - 12);
+ }
+
+ if (game.state === State.WIN || game.state === State.LOSE) {
+ ctx.save();
+ ctx.globalAlpha = screen_message_alpha;
+ if (game.state === State.WIN) {
+ zb.screen_draw(ctx, game.img.success);
+ } else {
+ zb.screen_draw(ctx, game.img.failure);
+ }
+ ctx.restore();
+ }
+}
+
+function do_draw_menu(ctx) {
+ zb.screen_draw(ctx, game.img.menuimage);
+ zb.buttons.draw(ctx, game.buttons.menu);
+}
+
+function do_draw_youwon(ctx) {
+ zb.screen_draw(ctx, game.img.youwon);
+}
+
+function rpgmaker_tile_format_thing(x, y, tile_check_func) {
+ let u = tile_check_func(x, y - 1);
+ let d = tile_check_func(x, y + 1);
+ let l = tile_check_func(x - 1, y);
+ let r = tile_check_func(x + 1, y);
+ let ul = tile_check_func(x - 1, y - 1);
+ let ur = tile_check_func(x + 1, y - 1);
+ let dl = tile_check_func(x - 1, y + 1);
+ let dr = tile_check_func(x + 1, y + 1);
+ let ulx, uly, urx, ury, dlx, dly, drx, dry;
+
+ /* Draw top left */
+ if (u) {
+ if (l) {
+ if (ul) {
+ /* Open water */
+ ulx = 2; uly = 4;
+ } else {
+ /* Upper left corner */
+ ulx = 2; uly = 0;
+ }
+ } else {
+ /* Left border */
+ ulx = 0; uly = 4;
+ }
+ } else {
+ if (l) {
+ /* Top border */
+ ulx = 2; uly = 2;
+ } else {
+ /* Very corner */
+ ulx = 0; uly = 2;
+ }
+ }
+
+ /* Draw top right */
+ if (u) {
+ if (r) {
+ if (ur) {
+ /* Open water */
+ urx = 1; ury = 4;
+ } else {
+ /* Upper right corner */
+ urx = 3; ury = 0;
+ }
+ } else {
+ /* Right border */
+ urx = 3; ury = 4;
+ }
+ } else {
+ if (r) {
+ /* Top border */
+ urx = 1; ury = 2;
+ } else {
+ /* Very corner */
+ urx = 3; ury = 2;
+ }
+ }
+
+ /* Draw bottom left */
+ if (d) {
+ if (l) {
+ if (dl) {
+ /* Open water */
+ dlx = 2; dly = 3;
+ } else {
+ /* Bottom left corner */
+ dlx = 2; dly = 1;
+ }
+ } else {
+ /* Left border */
+ dlx = 0; dly = 3;
+ }
+ } else {
+ if (l) {
+ /* Bottom border */
+ dlx = 2; dly = 5;
+ } else {
+ /* Very corner */
+ dlx = 0; dly = 5;
+ }
+ }
+
+ /* Draw bottom right */
+ if (d) {
+ if (r) {
+ if (dr) {
+ /* Open water */
+ drx = 1; dry = 3;
+ } else {
+ /* Bottom right corner */
+ drx = 3; dry = 1;
+ }
+ } else {
+ /* Right border */
+ drx = 3; dry = 3;
+ }
+ } else {
+ if (r) {
+ /* Bottom border */
+ drx = 1; dry = 5;
+ } else {
+ /* Very corner */
+ drx = 3; dry = 5;
+ }
+ }
+
+ return { ulx: ulx, uly: uly, urx: urx, ury: ury, dlx: dlx, dly: dly, drx: drx, dry: dry };
+}
+
+function draw_rpgmaker_tile(ctx, img, x, y, rpgmaker_tft, state) {
+ let ts = game.tile_size;
+ let { ulx, uly, urx, ury, dlx, dly, drx, dry } = rpgmaker_tft;
+ zb.sprite_draw(ctx, img, ts / 2, ts / 2, ulx, uly + state * 6, x, y);
+ zb.sprite_draw(ctx, img, ts / 2, ts / 2, urx, ury + state * 6, x + ts / 2, y);
+ zb.sprite_draw(ctx, img, ts / 2, ts / 2, dlx, dly + state * 6, x, y + ts / 2);
+ zb.sprite_draw(ctx, img, ts / 2, ts / 2, drx, dry + state * 6, x + ts / 2, y + ts / 2);
+}
+
+function draw_map(ctx) {
+ for (let y = 0; y < game.level_h; y++) {
+ for (let x = 0; x < game.level_w; x++) {
+ if (tile_at(x, y) === tileID.wall) {
+ let tileinfo = rpgmaker_tile_format_thing(x, y, (x, y) => tile_at(x, y) === tileID.wall);
+ draw_rpgmaker_tile(ctx, game.img.wall_tile, x * game.tile_size, y * game.tile_size, tileinfo, 0);
+ }
+ }
+ }
+}
+
+function draw_blocks(ctx) {
+ for (let block of blocks) {
+ for (let tile of block.tiles) {
+ let tileinfo = rpgmaker_tile_format_thing(tile[0], tile[1], (x, y) => {
+ for (let tile of block.tiles) {
+ if (tile[0] === x && tile[1] === y) {
+ return true;
+ }
+ }
+ return false;
+ });
+ draw_rpgmaker_tile(ctx, game.img.move_tile,
+ tile[0] * game.tile_size + Math.round(block.target_dx * game.tile_size * block.move_fraction),
+ tile[1] * game.tile_size + Math.round(block.target_dy * game.tile_size * block.move_fraction),
+ tileinfo, block.state);
+ }
+ }
+}
+
+function draw_decorations(ctx) {
+ for (let d of decorations) {
+ ctx.save();
+ if (d.type === 2) {
+ ctx.translate(0, 1);
+ }
+ zb.sprite_draw(ctx, game.img.decorations, game.tile_size, game.tile_size, d.type, 0, d.x * game.tile_size, d.y * game.tile_size);
+ ctx.restore();
+ }
+}
+
+function draw_creatures(ctx) {
+ for (let f of fish) {
+ if (f.hidden) continue;
+
+ ctx.save();
+
+ let mf = f.move_fraction || 0;
+ let tx = f.target_x !== undefined ? f.target_x : f.x;
+ let ty = f.target_y !== undefined ? f.target_y : f.y;
+
+ let drift_y_offset = Math.round(1.9 * Math.sin(f.drift * 2 * Math.PI));
+
+ ctx.translate(f.x * game.tile_size * (1 - mf) + tx * game.tile_size * mf,
+ f.y * game.tile_size * (1 - mf) + ty * game.tile_size * mf);
+
+ if (f.hflip) {
+ ctx.translate(game.tile_size, 0);
+ ctx.scale(-1, 1);
+ }
+
+ if (f.vflip) {
+ ctx.translate(0, game.tile_size);
+ ctx.scale(1, -1);
+ }
+
+ if (f.rotate) {
+ ctx.translate(game.tile_size / 2, game.tile_size / 2);
+ ctx.rotate(f.rotate * Math.PI / 180);
+ ctx.translate(-game.tile_size / 2, -game.tile_size / 2);
+ }
+
+ zb.sprite_draw(ctx, game.img.fish, game.tile_size, game.tile_size, f.type, f.frame, 0, drift_y_offset);
+ if (f === hovered_fish && (game.state === State.STAND || game.state === State.FISH)) {
+ zb.sprite_draw(ctx, game.img.selector, 28, 28, 0, selector_frame, -2, -2);
+ }
+
+ ctx.restore();
+ }
+}
+
+function draw_bubbles(ctx) {
+ for (let b of bubbles) {
+ zb.sprite_draw(ctx, game.img.bubble, 5, 11, 0, b.frame, Math.round(b.x - game.img.bubble.width / 2), Math.round(b.y - game.img.bubble.height / 2));
+ }
+}
+
+function draw_hearts(ctx) {
+ for (let h of hearts) {
+ ctx.drawImage(game.img.heart, Math.round(h.x - game.img.heart.width / 2), Math.round(h.y - game.img.heart.height / 2));
+ }
+}
+
+/* ---------- event handlers ------------ */
+
+function handle_gamestart(game) {
+ console.log("Game start!");
+
+ //game.music.bgm_menu.play();
+}
+
+function handle_mousedown(game, e, x, y) {
+ mouse_x = x;
+ mouse_y = y;
+ mouse_is_down = true;
+
+ if (game.mode === 'menu') {
+ if (zb.buttons.click(game.buttons.menu, x, y)) return;
+ } else if (game.mode === 'game') {
+ if (zb.buttons.click(game.buttons.ingame, x, y)) return;
+
+ let tile_x = Math.floor(mouse_x / game.tile_size);
+ let tile_y = Math.floor(mouse_y / game.tile_size);
+
+ if (game.mode === 'game') {
+ //if (zb.buttons.click(buttons.ingame, x, y)) return;
+
+ if (game.state === State.WIN) {
+ return;
+ }
+
+ if (game.state === State.STAND) {
+ if (hovered_block) {
+ hovered_block.state = 2;
+ game.state = State.BLOCK;
+ start_drag_x = tile_x;
+ start_drag_y = tile_y;
+ }
+ }
+ }
+ }
+}
+
+function handle_mouseup(game, e, x, y) {
+ mouse_is_down = false;
+ just_clicked_block = true;
+
+ if (game.mode === 'youwon') {
+ game.long_transition(zb.transition.type.fade, 1000, function() {
+ continue_level = 1;
+ game.buttons.menu.buttons[1].disabled = true;
+ game.mode = 'menu';
+ });
+ } else if (game.mode === 'menu') {
+ zb.buttons.update(game.buttons.menu, x, y);
+ } else if (game.mode === 'game') {
+ zb.buttons.update(game.buttons.ingame, x, y);
+
+ if (game.state === State.WIN) {
+ advance_level();
+ } else if (game.state === State.BLOCK) {
+ game.state = State.STAND;
+ handle_mousemove(game, e, x, y);
+ } else if (game.state === State.FISH) {
+ if (potential_fish_target === null) {
+ game.state = State.STAND;
+ handle_mousemove(game, e, x, y);
+ } else {
+ create_undo_point();
+ game.state = State.SWIM;
+ start_swimming(hovered_fish);
+ }
+ } else if (game.state === State.STAND) {
+ if (hovered_fish) {
+ game.state = State.FISH;
+ }
+ }
+
+ if (won_everything) {
+ return;
+ }
+ }
+}
+
+function handle_mouseleave(game, e) {
+ mouse_is_down = false;
+ if (game.mode === 'game') {
+ if (game.state === State.STAND || game.state === State.BLOCK) {
+ handle_mouseup(game, e, -1, -1);
+ }
+ }
+}
+
+function handle_mousemove(game, e, x, y) {
+ mouse_x = x;
+ mouse_y = y;
+
+ let tile_x = Math.floor(mouse_x / game.tile_size);
+ let tile_y = Math.floor(mouse_y / game.tile_size);
+
+ if (game.mode === 'menu') {
+ zb.buttons.update(game.buttons.menu, mouse_x, mouse_y);
+ } else if (game.mode === 'game') {
+ zb.buttons.update(game.buttons.ingame, mouse_x, mouse_y);
+
+ if (game.state === State.STAND) {
+ hovered_block = null;
+ for (let b of blocks) {
+ b.state = 0;
+ for (let t of b.tiles) {
+ if (t[0] === tile_x && t[1] === tile_y) {
+ b.state = 1;
+ hovered_block = b;
+ break;
+ }
+ }
+ }
+
+ hovered_fish = fish_at(tile_x, tile_y);
+ } else if (game.state === State.FISH) {
+ potential_fish_target = null;
+ fish_path = path_between(hovered_fish.x, hovered_fish.y, tile_x, tile_y, true);
+ if (fish_path) {
+ potential_fish_target = [tile_x, tile_y];
+ }
+ } else if (game.state === State.BLOCK) {
+ if (tile_x !== start_drag_x || tile_y !== start_drag_y) {
+ /* Move block */
+ let dx = 0;
+ let dy = 0;
+ hovered_block.target_dx = 0;
+ hovered_block.target_dy = 0;
+ if (tile_x > start_drag_x) {
+ dx = 1;
+ } else if (tile_x < start_drag_x) {
+ dx = -1;
+ } else if (tile_y > start_drag_y) {
+ dy = 1;
+ } else if (tile_y < start_drag_y) {
+ dy = -1;
+ }
+
+ let free_to_move = true;
+ let mouse_inside_block = false;
+ for (let t of hovered_block.tiles) {
+ if (anything_at(t[0] + dx, t[1] + dy, hovered_block)) {
+ free_to_move = false;
+ }
+
+ if (t[0] === tile_x && t[1] === tile_y) {
+ mouse_inside_block = true;
+ }
+ }
+
+ if (free_to_move) {
+ if (just_clicked_block) {
+ create_undo_point();
+ just_clicked_block = false;
+ }
+ hovered_block.move_fraction = 0;
+ hovered_block.target_dx = dx;
+ hovered_block.target_dy = dy;
+ game.state = State.DRAG;
+ game.sfx.drag.play();
+ } else if (mouse_inside_block) {
+ start_drag_x = tile_x;
+ start_drag_y = tile_y;
+ }
+ }
+ }
+ }
+}
+
+function toggle_mute_button() {
+ if (game.buttons.ingame.buttons[2].id == 2) {
+ game.buttons.ingame.buttons[2].id = 3;
+ } else {
+ game.buttons.ingame.buttons[2].id = 2;
+ }
+ game.toggle_mute();
+}
+
+function handle_keyup(game, e) {
+ if (game.transition.is_transitioning) return;
+
+ // key up
+ switch (e.key) {
+ case 'm':
+ toggle_mute_button();
+ e.preventDefault();
+ break;
+ case 'r':
+ reset();
+ e.preventDefault();
+ break;
+ case 'z':
+ undo();
+ e.preventDefault();
+ break;
+ }
+}
+
+function new_game_button() {
+ game.music.bgm.play();
+ game.start_transition(zb.transition.type.fade, 1000, function() {
+ game.mode = 'game';
+ level_number = 1;
+ load_level();
+ });
+}
+
+function continue_button() {
+ game.music.bgm.play();
+ game.start_transition(zb.transition.type.fade, 1000, function() {
+ game.mode = 'game';
+ level_number = continue_level;
+ load_level();
+ });
+}
--- /dev/null
+<!doctype html>
+<html>
+<head><title>
+ fish shark game
+</title>
+<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, width=100%, target-densitydpi=device-dpi, user-scalable=no" />
+<style>
+* { border: 0; margin: 0; overflow: hidden }
+/*body { height: 100vh }
+canvas { object-fit: contain; width: 100%; height: 100% }*/
+</style>
+</head>
+<body style="background: black">
+ <canvas id="canvas" width="576" height="864" style="max-width:100%" tabindex=1> </canvas>
+</body>
+<script src="levels.js"></script>
+<script src="zucchinibread.js"></script>
+<script src="game.js"></script>
+</html>
--- /dev/null
+var levels={
+ 10: { map: [0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[0,3], ],[[2,3], [2,4], [1,3], [1,4], ],[[3,3], [3,4], [4,3], ],[[4,4], ],[[1,6], [1,7], [2,6], [2,7], ],[[3,7], [4,6], [4,7], [3,6], ],], fish: [[3,1], [1,9], ], sharks: [[5,5], ], decorations: [[6,0,0], [4,1,2], [5,1,2], [7,1,0], [6,2,0], [5,7,3], [6,7,3], [4,9,1], [6,9,1], [1,10,2], [2,10,2], [3,10,2], [5,10,1], [7,10,1], ] },
+ 1: { map: [0,0,0,0,0,0,0,1,1,1,1,1,0,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0,1,1,1,1,0,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,], blocks: [[[4,3], [5,3], [4,2], [5,2], ],[[4,4], [4,5], ],[[4,6], [3,5], [3,6], ],[[5,5], [6,5], ],[[5,7], ],], fish: [[2,0], [1,7], [6,8], ], sharks: [], decorations: [[0,0,2], [1,0,2], [7,2,1], [0,8,2], [1,8,2], [4,8,1], ] },
+ 2: { map: [0,0,1,0,0,0,0,1,0,1,1,0,0,0,0,1,0,1,0,0,0,0,1,1,0,1,0,0,0,0,1,1,0,0,1,1,0,0,1,1,0,1,1,1,0,0,0,1,0,1,1,1,0,0,1,1,0,0,0,0,0,0,1,1,0,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[4,0], [5,0], [5,1], [4,1], ],[[0,1], [0,2], [0,3], [0,4], ],[[4,3], [5,3], [4,2], [5,2], ],[[1,4], ],[[4,4], [5,4], [4,5], ],[[5,5], [6,5], ],], fish: [[1,0], [6,1], ], sharks: [], decorations: [[2,3,2], [3,3,2], [0,8,2], [6,9,0], [5,10,0], [7,10,0], ] },
+ 3: { map: [1,1,0,0,0,0,1,1,0,1,0,0,0,1,1,1,0,0,0,0,0,1,0,1,0,0,0,0,0,1,0,1,0,0,0,1,1,1,0,1,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[3,1], [4,1], [2,1], ],[[4,2], [4,3], ],[[0,7], [1,5], [1,4], [0,6], [0,5], [1,6], ],[[6,4], [6,5], ],[[6,6], [5,5], [5,6], ],], fish: [[5,0], [6,3], [0,9], ], sharks: [], decorations: [[7,0,1], [2,4,2], [5,8,2], [4,9,2], [7,9,1], [6,10,1], ] },
+ 4: { map: [0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,], blocks: [[[4,0], [6,1], [5,1], [5,0], [6,0], [4,1], ],[[4,3], [5,3], [4,2], [5,2], ],[[4,4], [3,4], [5,4], [0,3], [2,3], [3,3], [1,3], ],], fish: [[3,1], [2,6], ], sharks: [[6,3], ], decorations: [[1,0,0], [3,7,3], [4,7,3], [5,7,3], ] },
+ 5: { map: [0,0,0,0,1,1,1,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[3,5], ],[[4,5], [4,6], [3,6], ],[[3,7], [3,8], [3,9], ],], fish: [[5,1], [6,1], [5,2], [6,2], [5,3], [6,3], [5,4], [6,4], [6,5], [4,8], ], sharks: [[1,2], ], decorations: [[4,0,1], [7,6,1], [6,8,1], [4,9,3], [0,10,3], [7,10,1], ] },
+ 6: { map: [0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[2,3], [0,3], [1,3], [3,3], ],[[2,7], [3,7], [2,6], [3,6], [2,5], ],[[5,5], [5,6], [6,5], ],[[4,8], [4,7], [5,7], [5,8], ],], fish: [[2,0], [2,2], ], sharks: [[6,7], [0,10], ], decorations: [[0,0,2], [1,0,2], [7,1,0], [2,10,3], [3,10,3], [5,10,1], ] },
+ 7: { map: [1,0,0,1,0,0,1,1,0,0,0,1,0,0,0,1,0,1,1,1,1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,1,1,1,0,0,0,0,1,1,0,0,0,1,0,0,0,1,0,1,1,1,1,1,0,1,0,0,0,1,0,0,0,1,1,0,0,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[1,3], [1,4], ],[[2,4], [2,5], ],[[4,4], [4,5], ],[[5,4], [5,5], ],], fish: [[2,1], [4,1], [2,8], [4,8], ], sharks: [[3,4], [3,5], ], decorations: [[1,1,2], [5,1,2], [3,3,0], [3,6,0], [1,9,3], [2,9,3], [4,9,3], [5,9,3], ] },
+ 8: { map: [1,1,0,0,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[6,1], [6,2], [5,0], [5,1], ],[[0,2], [1,2], [0,3], ],[[2,3], [1,3], [1,4], ],[[2,4], [2,5], [3,5], ],[[3,7], [4,6], [5,6], [3,6], ],[[3,8], [1,8], [2,9], [3,9], [2,8], ],[[0,9], [0,10], ],], fish: [[6,0], [5,7], [1,9], ], sharks: [[2,2], [5,4], [1,5], ], decorations: [[6,3,2], [4,8,3], [6,8,0], [5,9,0], [6,9,1], [5,10,1], [6,10,0], ] },
+ 9: { map: [0,0,0,0,0,0,1,1,0,0,0,0,1,0,1,1,1,1,1,1,1,0,1,1,1,0,0,0,1,0,1,1,1,0,1,0,1,0,1,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[5,3], [5,4], [5,5], ],[[2,5], [2,6], [1,5], ],[[4,6], [5,6], [3,5], [3,6], ],[[1,10], ],], fish: [[0,0], [1,0], [0,1], [1,1], [0,5], ], sharks: [[1,6], [1,7], [2,7], [3,7], [4,7], [5,7], [1,8], [2,8], [5,8], [1,9], [2,9], [3,9], [4,9], [5,9], ], decorations: [[6,0,1], [7,0,0], [2,1,2], [3,1,2], [6,3,0], [2,4,0], [6,4,1], [6,5,0], [7,6,0], [7,7,1], [6,8,1], [6,9,0], [7,9,1], [6,11,0], [7,11,1], ] },
+ test: { map: [1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,0,0,1,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,], blocks: [[[5,3], [5,4], [4,4], [4,3], ],], fish: [[3,2], [3,6], [3,9], ], sharks: [], decorations: [] },
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="12" tilewidth="24" tileheight="24" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="12">
+ <data encoding="csv">
+12,12,3,1,1,1,1,2,
+2,2,2,2,1,2,1,2,
+1,1,1,2,7,7,1,11,
+1,1,1,2,7,7,1,2,
+2,2,2,2,8,1,2,2,
+2,1,1,6,8,5,5,2,
+1,1,1,6,6,2,1,2,
+1,3,2,1,1,6,1,2,
+12,12,2,2,11,2,3,2,
+2,2,1,1,1,1,2,2,
+2,2,1,1,1,1,2,2,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="12" tilewidth="24" tileheight="24" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="12">
+ <data encoding="csv">
+1,1,1,1,1,1,10,2,
+1,1,1,3,12,12,2,10,
+1,1,1,2,2,2,10,2,
+8,6,6,5,5,1,1,2,
+2,6,6,5,7,1,1,2,
+2,2,2,2,2,4,1,2,
+2,6,6,5,5,1,1,2,
+2,6,6,5,5,13,13,2,
+2,1,1,2,2,2,2,2,
+1,3,1,1,11,2,11,2,
+2,12,12,12,2,11,2,11,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="12" tilewidth="24" tileheight="24" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="12">
+ <data encoding="csv">
+1,3,2,1,6,6,1,2,
+8,2,2,1,6,6,3,2,
+8,2,1,1,7,7,2,2,
+8,2,12,12,7,7,2,2,
+8,5,2,2,5,5,2,2,
+1,2,2,2,5,9,9,2,
+1,2,2,2,1,1,2,2,
+1,1,1,1,1,1,2,2,
+12,2,2,2,2,2,2,2,
+2,1,1,1,1,2,10,2,
+2,1,1,1,1,10,2,10,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="12" tilewidth="24" tileheight="24" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="12">
+ <data encoding="csv">
+2,2,1,1,1,3,2,11,
+1,2,7,7,7,2,2,2,
+1,1,1,1,9,2,1,2,
+1,1,1,1,9,2,3,2,
+1,8,12,2,2,2,5,2,
+8,8,2,1,1,6,5,2,
+8,8,1,2,1,6,6,2,
+8,1,1,2,1,1,2,2,
+1,1,1,1,1,12,2,2,
+3,1,1,1,12,2,2,11,
+2,2,2,2,2,2,11,2,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="12" tilewidth="24" tileheight="24" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="12">
+ <data encoding="csv">
+1,10,1,1,6,6,6,2,
+1,1,1,3,6,6,6,2,
+1,1,1,1,5,5,1,2,
+8,8,8,8,5,5,4,2,
+1,1,1,8,8,8,2,2,
+1,1,1,1,1,1,2,2,
+1,1,3,1,1,1,2,2,
+1,1,1,13,13,13,2,2,
+2,2,2,2,2,2,2,2,
+2,1,1,1,1,1,1,2,
+2,1,1,1,1,1,1,2,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="12" tilewidth="24" tileheight="24" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="12">
+ <data encoding="csv">
+1,1,1,1,11,2,2,2,
+1,1,1,1,2,3,3,2,
+1,4,1,1,2,3,3,2,
+1,1,1,1,2,3,3,2,
+1,1,1,1,2,3,3,2,
+1,1,1,6,8,1,3,2,
+1,1,1,8,8,1,2,11,
+1,1,1,5,2,2,2,2,
+1,1,1,5,3,2,11,2,
+1,1,1,5,13,2,2,2,
+13,1,1,2,2,2,2,11,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="12" tilewidth="24" tileheight="24" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="12">
+ <data encoding="csv">
+12,12,3,1,1,1,1,2,
+2,2,2,2,2,1,1,10,
+1,1,3,1,2,1,1,2,
+6,6,6,6,2,1,1,2,
+1,1,1,1,2,1,1,2,
+1,1,6,1,2,6,6,2,
+1,1,6,6,2,6,1,2,
+1,1,6,6,5,5,4,1,
+1,1,1,1,5,5,2,2,
+1,1,1,1,1,2,2,2,
+4,1,13,13,2,11,2,2,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="12" tilewidth="24" tileheight="24" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="12">
+ <data encoding="csv">
+2,1,1,2,1,1,2,2,
+1,12,3,2,3,12,1,2,
+1,2,2,2,2,2,1,2,
+1,6,1,10,1,1,1,2,
+1,6,5,4,5,6,2,2,
+2,2,5,4,5,6,2,2,
+1,1,1,10,1,1,1,2,
+1,2,2,2,2,2,1,2,
+1,1,3,2,3,1,1,2,
+2,13,13,2,13,13,2,2,
+2,2,2,2,2,2,2,2,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="12" tilewidth="24" tileheight="24" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="12">
+ <data encoding="csv">
+2,2,1,1,1,6,3,2,
+1,1,1,1,2,6,6,2,
+9,9,4,1,2,1,6,2,
+9,7,7,2,1,1,12,2,
+1,7,8,1,1,4,2,2,
+2,4,8,8,1,1,2,2,
+1,1,1,5,5,5,2,2,
+1,1,1,5,1,3,2,2,
+1,8,8,8,13,2,10,2,
+7,3,8,8,2,10,11,2,
+7,2,2,2,2,11,10,2,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="12" tilewidth="24" tileheight="24" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="12">
+ <data encoding="csv">
+3,3,1,1,1,1,11,10,
+3,3,12,12,2,1,2,2,
+2,2,2,2,2,1,2,2,
+2,1,1,1,2,7,10,2,
+2,1,10,1,2,7,11,2,
+3,5,5,9,1,7,10,2,
+2,4,5,9,9,9,2,10,
+2,4,4,4,4,4,2,11,
+2,4,4,1,1,4,11,2,
+2,4,4,4,4,4,10,11,
+2,6,2,2,2,2,2,2,
+2,2,2,2,2,2,10,11
+</data>
+ </layer>
+</map>
--- /dev/null
+#!/usr/bin/python3
+
+import ulvl
+import sys
+
+if len(sys.argv) < 2:
+ print("usage:", sys.argv[0], "<infiles>")
+ sys.exit(1)
+
+screenwidth, screenheight = 8, 12
+
+tilemapping = { 9: 1, 10: 1, 11: 0, 12: 0 }
+
+decomapping = { 9: 0, 10: 1, 11: 2, 12: 3 }
+
+movable_types = [4,5,6,7,8]
+
+fish_code = 2
+shark_code = 3
+
+print("var levels={")
+for filename in sys.argv[1:]:
+ m = ulvl.TMX.load(filename)
+
+ w = m.meta['width']
+ h = m.meta['height']
+
+ movable_blocks = []
+ fish = []
+ sharks = []
+ decorations = []
+
+ used_block_coords = set()
+
+ print('\t', filename.replace('.tmx', '').replace('levels/', ''), end=': { ')
+
+ print('map: [', end='')
+ for y in range(screenheight):
+ for x in range(screenwidth):
+ thing = m.layers[0].tiles[y * w + x] - 1
+
+ if (x, y) in used_block_coords:
+ print('0,', end='')
+ elif thing == fish_code:
+ fish.append((x, y))
+ print('0,', end='')
+ elif thing == shark_code:
+ sharks.append((x, y))
+ print('0,', end='')
+ elif thing in movable_types:
+ block_type = thing
+ block_locs = set()
+ block_locs.add((x, y))
+ used_block_coords.add((x, y))
+ queue = [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]
+ while len(queue) > 0:
+ check_loc = queue.pop(0)
+ if check_loc in block_locs:
+ # we already looked at this. skip!
+ continue
+ if m.layers[0].tiles[check_loc[1] * w + check_loc[0]] - 1 != thing:
+ # this isn't in our block. skip!
+ continue
+ # ok so this is part of our block. add it and add its neighbors to the queue
+ block_locs.add(check_loc)
+ used_block_coords.add(check_loc)
+ queue += [(check_loc[0] - 1, check_loc[1]), (check_loc[0] + 1, check_loc[1]),
+ (check_loc[0], check_loc[1] - 1), (check_loc[0], check_loc[1] + 1)]
+
+ # Save our block
+ movable_blocks.append(list(block_locs))
+ print('0,', end='')
+ else:
+ print(tilemapping.get(thing, thing), end='')
+ if thing in decomapping:
+ decorations.append((x, y, decomapping[thing]))
+ print(',', end='')
+ print('], blocks: [', end='')
+ for b in movable_blocks:
+ print('[', end='')
+ for coords in b:
+ print('[' + str(coords[0]) + ',' + str(coords[1]) + '], ', end='')
+ print('],', end='')
+ print('], fish: [', end='')
+ for f in fish:
+ print('[' + str(f[0]) + ',' + str(f[1]) + '], ', end='')
+ print('], sharks: [', end='')
+ for s in sharks:
+ print('[' + str(s[0]) + ',' + str(s[1]) + '], ', end='')
+ print('], decorations: [', end='')
+ for d in decorations:
+ print('[' + str(d[0]) + ',' + str(d[1]) + ',' + str(d[2]) + '], ', end='')
+ print('] },');
+print("}")
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="12" tilewidth="24" tileheight="24" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="12">
+ <data encoding="csv">
+2,2,2,2,2,2,2,2,
+2,1,1,1,1,1,1,2,
+2,1,1,3,1,1,1,2,
+2,2,2,2,8,8,2,2,
+2,2,2,2,8,8,2,2,
+2,1,1,1,1,1,1,2,
+2,1,1,3,1,1,1,2,
+2,1,1,1,1,1,1,2,
+2,1,1,1,1,1,1,2,
+2,1,1,3,1,1,1,2,
+2,1,1,1,1,1,1,2,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<tileset version="1.2" tiledversion="1.3.2" name="tmptile" tilewidth="24" tileheight="24" tilecount="8" columns="8">
+ <image source="tmptile.png" width="192" height="24"/>
+</tileset>
--- /dev/null
+"use strict";
+
+let game;
+
+let game_started = false;
+
+let just_started = true;
+
+let level_number = 1;
+
+let map;
+
+let buttons = {};
+
+let max_unlocked_level;
+let continue_level;
+
+let how_to_play = false;
+
+let disable_transitions = false;
+let disable_colors = false;
+
+let won_everything = false;
+
+let KINDS_OF_FISH = 5;
+
+/* --------- definitions ---------- */
+
+/* state:
+ * STAND: waiting for input
+ * BLOCK: moving block
+ * DRAG: actually dragging block
+ * FISH: moving fish or shark
+ * CONGREGATE: swim together to win
+ * WIN: level complete
+ * LOSESWIM: shark swimming to eat us
+ * LOSE: got eaten by shark
+ */
+let State = { STAND: 0, BLOCK: 1, DRAG: 2, FISH: 3, SWIM: 4, CONGREGATE: 5, WIN: 6, LOSESWIM: 7, LOSE: 8 };
+
+let UIState = { MENU: 0, LEVELSELECT: 1, OPTIONS: 2, GAME: 3, REALLYQUIT: 4, REALLYDELETE: 5 };
+
+let can_continue = false;
+
+let save_data = 1;
+const SAVE_KEY = "casso.continentalshelf.save"
+
+zb.ready(function() {
+ game = zb.create_game({
+ canvas: 'canvas',
+ canvas_w: 576,
+ canvas_h: 864,
+ draw_scale: 3,
+ tile_size: 24,
+ level_w: 8,
+ level_h: 12,
+ background_color: '#192c4a',
+ run_in_background: true,
+ load_with_progress_bar: false,
+ save_key: SAVE_KEY,
+ state: State.STAND,
+ uistate: UIState.MENU,
+ events: {
+ keyup: handle_keyup,
+ mouseup: handle_mouseup,
+ mouseleave: handle_mouseleave,
+ mousedown: handle_mousedown,
+ mousemove: handle_mousemove,
+ gamestart: handle_gamestart,
+ },
+ modes: {
+ menu: {
+ draw: do_draw_menu,
+ },
+ youwon: {
+ draw: do_draw_youwon,
+ },
+ game: {
+ draw: do_draw,
+ update: do_update,
+ },
+ },
+ mode: 'menu',
+ buttons: {},
+ });
+
+ game.register_images({
+ fish: 'img/fish.png',
+ selector: 'img/selector.png',
+ x: 'img/x.png',
+ heart: 'img/heart.png',
+ bubble: 'img/bubble.png',
+ move_tile: 'img/move_tile.png',
+ wall_tile: 'img/wall_tile.png',
+ decorations: 'img/decoration.png',
+ levelnums: 'img/levelnums.png',
+ success: 'img/success.png',
+ failure: 'img/failure.png',
+ buttons: 'img/buttons.png',
+ menubuttons: 'img/menubuttons.png',
+ menuimage: 'img/menuimage.png',
+ youwon: 'img/youwon.png',
+ levels: {
+ 1: 'img/level/1.png',
+ 2: 'img/level/2.png',
+ 4: 'img/level/4.png',
+ }
+ });
+
+ game.register_sfx({
+ victory: {
+ path: 'sfx/victory.wav',
+ volume: 0.3,
+ },
+ fail: {
+ path: 'sfx/fail.wav',
+ volume: 0.8,
+ },
+ drip: {
+ path: 'sfx/drip.wav',
+ volume: 1,
+ },
+ drag: {
+ path: 'sfx/drag.wav',
+ volume: 0.19,
+ },
+ bubble: {
+ path: 'sfx/bubble.wav',
+ volume: 0.5,
+ },
+ bubble2: {
+ path: 'sfx/bubble2.wav',
+ volume: 0.5,
+ },
+ bubble3: {
+ path: 'sfx/bubble3.wav',
+ volume: 0.5,
+ },
+ bubble4: {
+ path: 'sfx/bubble4.wav',
+ volume: 0.5,
+ },
+ });
+
+ game.register_music({
+ bgm: {
+ path: 'music/sharkgamebeachmusic',
+ volume: 0.1,
+ },
+ });
+
+ game.resources_ready();
+
+ continue_level = parseInt(game.load("continue_level") || "1");
+
+ game.buttons.ingame = zb.buttons.create({
+ img: game.img.buttons,
+ button_w: 24,
+ button_h: 24,
+ buttons: [
+ {
+ /* Undo */
+ x: game.screen_w - 72,
+ y: game.screen_h - 24,
+ id: 0,
+ callback: undo,
+ },
+ {
+ /* Reset */
+ x: game.screen_w - 48,
+ y: game.screen_h - 24,
+ id: 1,
+ callback: reset,
+ },
+ {
+ /* Toggle mute */
+ x: game.screen_w - 24,
+ y: game.screen_h - 24,
+ id: 2,
+ callback: toggle_mute_button,
+ },
+ ],
+ });
+
+ game.buttons.menu = zb.buttons.create({
+ img: game.img.menubuttons,
+ button_w: 72,
+ button_h: 16,
+ buttons: [
+ {
+ /* New game */
+ x: game.screen_w / 2 - 36,
+ y: game.screen_h / 2 + 36,
+ id: 0,
+ callback: new_game_button,
+ },
+ {
+ /* Continue */
+ x: game.screen_w / 2 - 36,
+ y: game.screen_h / 2 + 60,
+ id: 1,
+ callback: continue_button,
+ disabled: continue_level === 1,
+ },
+ ],
+ });
+});
+
+let tileID = {
+ blank: 0,
+ wall: 1,
+};
+
+let creatureID = {
+ fish: 0,
+ shark: 1,
+};
+
+let creature_type_mapping = {
+ 0: creatureID.fish,
+ 1: creatureID.fish,
+ 2: creatureID.fish,
+ 3: creatureID.fish,
+ 4: creatureID.fish,
+ 5: creatureID.shark,
+};
+
+/* ------ timers & static timer values --------- */
+
+const STAND_FRAME_LENGTH = 500;
+const SWIM_FRAME_LENGTH = 200;
+
+const SELECTOR_FRAME_LENGTH = 300;
+
+const BLOCK_MOVE_SPEED = 12;
+
+const SWIM_SPEED = 6;
+const LOSE_SWIM_SPEED = 8;
+const WIN_SWIM_SPEED = 5;
+
+const X_FRAME_LENGTH = 100;
+const MAX_X_FRAME = 12;
+
+const BUBBLE_FRAME_LENGTH = 300;
+const BUBBLE_UP_SPEED = 15;
+
+const SCREEN_MESSAGE_DELAY = 600;
+const SCREEN_MESSAGE_FADEIN_LENGTH = 1000;
+
+const DRIFT_SPEED = 3000;
+
+/* ------- game global state -------- */
+
+let mouse_x = -1, mouse_y = -1, mouse_is_down = false;
+
+let fish = [];
+
+let blocks = [];
+
+let hearts = [];
+
+let bubbles = [];
+
+let decorations = [];
+
+let hovered_block = null;
+
+let hovered_fish = null;
+
+let eaten_fish = null;
+
+let start_drag_x = 0, start_drag_y = 0;
+
+let selector_timer = 0;
+let selector_frame = 0;
+
+let potential_fish_target = null;
+let fish_path = null;
+
+let x_timer = 0;
+let x_frame = 0;
+
+let just_clicked_block = true;
+
+let screen_message_delay = 0;
+let screen_message_alpha = 0;
+
+/* ------- game behavior functions -------- */
+
+function tile_at(x, y) {
+ if (x < 0 || x >= game.level_w) return tileID.wall;
+ if (y < 0 || y >= game.level_h) return tileID.wall;
+
+ let result = map[y * game.level_w + x];
+ return result;
+}
+
+function fish_at(x, y) {
+ let filtered_fish = fish.filter(o => o.x === x && o.y === y);
+ if (filtered_fish.length > 0) {
+ return filtered_fish[0];
+ }
+ return null;
+}
+
+function block_at(x, y) {
+ for (let b of blocks) {
+ for (let t of b.tiles) {
+ if (t[0] === x && t[1] === y) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+function tile_or_block_at(x, y) {
+ return tile_at(x, y) === tileID.wall || block_at(x, y);
+}
+
+function anything_at(x, y, ignore_block) {
+ if (tile_at(x, y) === tileID.wall) {
+ return true;
+ }
+
+ for (let f of fish) {
+ if (f.x === x && f.y === y) {
+ return true;
+ }
+ }
+
+ for (let b of blocks) {
+ if (ignore_block && ignore_block === b) {
+ continue;
+ }
+ for (let t of b.tiles) {
+ if (t[0] === x && t[1] === y) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+function anything_except_fish_at(x, y, ignore_block) {
+ if (tile_at(x, y) === tileID.wall) {
+ return true;
+ }
+
+ for (let f of fish) {
+ if (f.x === x && f.y === y && f.species !== creatureID.fish) {
+ return true;
+ }
+ }
+
+ for (let b of blocks) {
+ if (ignore_block && ignore_block === b) {
+ continue;
+ }
+ for (let t of b.tiles) {
+ if (t[0] === x && t[1] === y) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+function sofc(coords) {
+ return "" + coords[0] + "," + coords[1];
+}
+
+function cofs(string) {
+ return string.split(",").map(a => parseInt(a, 10));
+}
+
+function path_between(start_x, start_y, end_x, end_y, end_can_be_blocked, ignore_fish) {
+ /* Pathfinding, for fish */
+
+ if (anything_at(end_x, end_y) && end_can_be_blocked) {
+ console.log("very no");
+ return null;
+ }
+
+ let queue = [[start_x, start_y, null]];
+ let checked = {};
+
+ while (queue.length > 0) {
+ let next_pos = queue.shift();
+
+ if (checked.hasOwnProperty(sofc(next_pos))) {
+ continue;
+ }
+
+ checked[sofc(next_pos)] = next_pos[2];
+
+ if (next_pos[0] === end_x && next_pos[1] === end_y) {
+ console.log("yes");
+ let path = [];
+ while (next_pos !== null) {
+ path.unshift([next_pos[0], next_pos[1]]);
+ next_pos = checked[sofc(next_pos)];
+ }
+ return path;
+ }
+
+ if ((next_pos[0] !== start_x || next_pos[1] !== start_y)
+ && (ignore_fish || anything_at(next_pos[0], next_pos[1]))
+ && (!ignore_fish || anything_except_fish_at(next_pos[0], next_pos[1]))) {
+ continue;
+ }
+
+ queue.push([next_pos[0] + 1, next_pos[1], [next_pos[0], next_pos[1]]]);
+ queue.push([next_pos[0] - 1, next_pos[1], [next_pos[0], next_pos[1]]]);
+ queue.push([next_pos[0], next_pos[1] + 1, [next_pos[0], next_pos[1]]]);
+ queue.push([next_pos[0], next_pos[1] - 1, [next_pos[0], next_pos[1]]]);
+ }
+
+ console.log("no");
+ return null;
+}
+
+let undo_stack = [];
+
+function create_undo_point() {
+ console.log("cup");
+
+ let blocks_copy = [];
+ for (let b of blocks) {
+ let new_block = {
+ state: 0,
+ tiles: [],
+ target_dx: 0,
+ target_dy: 0,
+ move_fraction: 0,
+ };
+ for (let coord of b.tiles) {
+ new_block.tiles.push( [ coord[0], coord[1] ] );
+ }
+ blocks_copy.push(new_block);
+ }
+
+ undo_stack.push({
+ fish: zb.copy_flat_objlist(fish),
+ blocks: blocks_copy,
+ });
+}
+
+function make_normal_state() {
+ game.state = State.STAND;
+ x_frame = 0;
+ hearts = [];
+ bubbles = [];
+ potential_fish_target = null;
+ hovered_fish = null;
+ screen_message_alpha = 0;
+ screen_message_delay = SCREEN_MESSAGE_DELAY;
+}
+
+function undo() {
+ console.log("undo")
+ if (undo_stack.length > 0) {
+ let undo_point = undo_stack.pop();
+ fish = undo_point.fish;
+ blocks = undo_point.blocks;
+ make_normal_state();
+ }
+}
+
+function reset() {
+ console.log("reset");
+ create_undo_point();
+ game.start_transition(zb.transition.type.fade, 300, function() {
+ load_level();
+ });
+}
+
+function advance_level() {
+ if (!can_continue) return;
+
+ undo_stack = [];
+
+ if (level_number + 1 > Math.max(...Object.keys(levels).map(a => parseInt(a, 10) || 0))) {
+ game.long_transition(zb.transition.type.fade, 1000, function() {
+ reset_save();
+ game.mode = 'youwon';
+ });
+ } else {
+ game.start_transition(zb.transition.type.fade, 1000, function() {
+ level_number ++;
+ load_level();
+ make_normal_state();
+ });
+ }
+}
+
+function skip_to(num) {
+ level_number = num;
+ load_level();
+}
+
+function win_everything() {
+ console.log("WIN!!");
+}
+
+function load_level() {
+ load_level_data(levels[level_number]);
+}
+
+function load_level_data(lvl) {
+ map = lvl.map;
+
+ fish = [];
+ blocks = [];
+
+ make_normal_state();
+
+ decorations = [];
+
+ let fish_type = zb.mod(level_number || 0, KINDS_OF_FISH);
+ if (isNaN(level_number)) {
+ fish_type = 0;
+ }
+ for (let f of lvl.fish) {
+ fish.push({
+ x: f[0],
+ y: f[1],
+ type: fish_type,
+ species: creatureID.fish,
+ frame: 0,
+ timer: Math.random() * STAND_FRAME_LENGTH,
+ drift: Math.random(),
+ });
+ fish_type ++;
+ fish_type %= KINDS_OF_FISH;
+ }
+
+ for (let s of lvl.sharks) {
+ fish.push({
+ x: s[0],
+ y: s[1],
+ type: KINDS_OF_FISH,
+ species: creatureID.shark,
+ frame: 0,
+ timer: Math.random() * STAND_FRAME_LENGTH,
+ drift: Math.random(),
+ });
+ }
+
+ for (let b of lvl.blocks) {
+ let new_block = {
+ state: 0,
+ tiles: [],
+ target_dx: 0,
+ target_dy: 0,
+ move_fraction: 0,
+ };
+ for (let coord of b) {
+ new_block.tiles.push( [ coord[0], coord[1] ] );
+ }
+ blocks.push(new_block);
+ }
+
+ for (let d of lvl.decorations) {
+ decorations.push({
+ x: d[0],
+ y: d[1],
+ type: d[2],
+ });
+ }
+
+ game.state = State.STAND;
+}
+
+function save() {
+ console.log("Saving");
+ let save_data = level_number;
+ if (game.state === State.WIN) {
+ save_data = level_number + 1;
+ }
+ game.save('continue_level', save_data);
+}
+
+function reset_save() {
+ game.save('continue_level', 1);
+}
+
+function win() {
+ //game.sfx.victory.play();
+ console.log("You won!");
+ game.state = State.WIN;
+ game.sfx.victory.play();
+
+ let heart_x = 0;
+ let heart_y = 0;
+ for (let f of fish) {
+ if (f.species === creatureID.fish) {
+ f.hidden = true;
+ heart_x = f.x;
+ heart_y = f.y;
+ }
+ }
+
+ for (let i = 0; i < 20; i++) {
+ create_heart(heart_x * game.tile_size + game.tile_size / 2 + Math.random() * 10 - 5,
+ heart_y * game.tile_size + game.tile_size / 2 + Math.random() * 10 - 5);
+ }
+
+ save();
+
+ window.setTimeout(function() {
+ can_continue = true;
+ }, 350);
+}
+
+function lose() {
+ game.state = State.LOSE;
+ game.sfx.fail.play();
+ x_timer = 0;
+ x_frame = 0;
+ eaten_fish.hidden = true;
+}
+
+function check_status() {
+ /* Check lose: a shark and a fish in the same area */
+ for (let f of fish) {
+ if (f.species === creatureID.shark) {
+ for (let g of fish) {
+ if (g.species === creatureID.fish) {
+ let path = path_between(f.x, f.y, g.x, g.y, false);
+ if (path) {
+ game.state = State.LOSESWIM;
+ hovered_fish = f;
+ eaten_fish = g;
+ fish_path = path;
+ start_swimming(hovered_fish);
+ break;
+ }
+ }
+ }
+ if (game.state === State.LOSESWIM) break;
+ }
+ }
+
+ /* Check win: all fish in same area */
+ /* find first fish and see if it paths to all others */
+ let first_fish = null;
+ for (let f of fish) {
+ if (f.species === creatureID.fish) {
+ first_fish = f;
+ break;
+ }
+ }
+
+ let won = true;
+ for (let f of fish) {
+ if (f === first_fish) continue;
+ if (f.species !== creatureID.fish) continue;
+ let path = path_between(first_fish.x, first_fish.y, f.x, f.y, false, true);
+ if (path === null) {
+ won = false;
+ break;
+ }
+ }
+
+ if (won) {
+ console.log("omg win");
+ game.state = State.CONGREGATE;
+ /* DFS to find spot closest to all fish */
+ for (let f of fish) {
+ if (f.species !== creatureID.fish) continue;
+ f.explored_territory = new Set();
+ f.explored_territory.add(sofc([f.x, f.y]));
+ }
+
+ let found_thing = false;
+ let common_square = null;
+ let bailout = 0;
+ while (!found_thing) {
+ for (let f of fish) {
+ if (f.species !== creatureID.fish) continue;
+ let new_explored_territory = new Set();
+ for (let e of f.explored_territory) {
+ let coords = cofs(e);
+ let neighbors = [
+ [coords[0] + 1, coords[1]],
+ [coords[0] - 1, coords[1]],
+ [coords[0], coords[1] + 1],
+ [coords[0], coords[1] - 1],
+ ];
+ for (let n of neighbors) {
+ if (n[0] < 0 || n[0] >= game.level_w || n[1] < 0 || n[1] >= game.level_h) continue;
+ if (f.explored_territory.has(sofc(n))) continue;
+ if (anything_except_fish_at(n[0], n[1])) continue;
+ new_explored_territory.add(sofc(n));
+ }
+ }
+ f.explored_territory = f.explored_territory.union(new_explored_territory);
+ }
+
+ let common_squares = fish[0].explored_territory;
+ for (let f of fish) {
+ if (f.species !== creatureID.fish) continue;
+ common_squares = common_squares.intersection(f.explored_territory);
+ }
+
+ bailout ++;
+ if (bailout > 1000) found_thing = true;
+
+ if (common_squares.size > 0) {
+ common_square = cofs(Array.from(common_squares)[0]);
+ found_thing = true;
+ }
+ }
+
+ if (common_square === null) {
+ console.error("Assert this should not happen!");
+ }
+
+ /* ok, now we found where all the fish will swim to. and we know they all have a path. so figure out the path for each fish */
+ let max_win_path_length = 0;
+ for (let f of fish) {
+ if (f.species !== creatureID.fish) continue;
+ f.win_path = path_between(f.x, f.y, common_square[0], common_square[1], false, true);
+ if (f.win_path.length > max_win_path_length) {
+ max_win_path_length = f.win_path.length;
+ }
+ }
+
+ /* now we want them to reach it at the same time. so scale each one's speed proportionately to their path length */
+ for (let f of fish) {
+ if (f.species !== creatureID.fish) continue;
+ f.win_swim_speed = WIN_SWIM_SPEED * (f.win_path.length - 1) / (max_win_path_length - 1);
+ start_swimming(f);
+ }
+ }
+}
+
+function start_swimming(fish) {
+ fish.target_x = fish.x;
+ fish.target_y = fish.y;
+ fish.swimming = true;
+ fish.move_fraction = 1;
+}
+
+function create_heart(x, y) {
+ hearts.push({
+ x: Math.round(x),
+ y: Math.round(y),
+ xspeed: Math.random() * 60 - 30,
+ yspeed: Math.random() * -30 - 30,
+ gravity: 50,
+ });
+}
+
+/* ---------- update functions ------------ */
+
+/* MAIN UPDATE FUNCTION */
+function do_update(delta) {
+ update_bubbles(delta);
+
+ if (game.state === State.WIN || game.state === State.LOSE) {
+ if (screen_message_delay > 0) {
+ screen_message_delay -= delta;
+ } else {
+ if (screen_message_alpha < 1) {
+ screen_message_alpha += delta / SCREEN_MESSAGE_FADEIN_LENGTH;
+ } else {
+ screen_message_alpha = 1;
+ }
+ }
+ }
+
+ for (let f of fish) {
+ f.drift += delta / DRIFT_SPEED;
+
+ f.timer += delta;
+
+ let max_time = STAND_FRAME_LENGTH;
+ if (f.swimming) {
+ max_time = SWIM_FRAME_LENGTH;
+ }
+ while (f.timer > max_time) {
+ f.timer -= max_time;
+ f.frame ++;
+ f.frame %= 2;
+ }
+ }
+
+ selector_timer += delta;
+ while (selector_timer > SELECTOR_FRAME_LENGTH) {
+ selector_timer -= SELECTOR_FRAME_LENGTH;
+ selector_frame ++;
+ selector_frame %= 2;
+ }
+
+ if (game.state === State.DRAG) {
+ hovered_block.move_fraction += BLOCK_MOVE_SPEED * delta / 1000;
+ if (hovered_block.move_fraction > 1) {
+ for (let t of hovered_block.tiles) {
+ t[0] += hovered_block.target_dx;
+ t[1] += hovered_block.target_dy;
+ }
+ hovered_block.move_fraction = 0;
+ start_drag_x += hovered_block.target_dx;
+ start_drag_y += hovered_block.target_dy;
+ hovered_block.target_dx = 0;
+ hovered_block.target_dy = 0;
+ if (mouse_is_down) {
+ game.state = State.BLOCK;
+ } else {
+ game.state = State.STAND;
+ }
+
+ /* Check if we won or lost */
+ check_status();
+
+ if (game.state !== State.CONGREGATE && game.state !== State.LOSESWIM) {
+ /* Check if we need to move more */
+ if (mouse_is_down) {
+ handle_mousemove(game, {}, mouse_x, mouse_y);
+ }
+ } else {
+ hovered_block = null;
+ for (let b of blocks) {
+ b.state = 0;
+ }
+ }
+ }
+ } else if (game.state === State.SWIM || game.state === State.LOSESWIM) {
+ let speed = SWIM_SPEED;
+
+ if (game.state === State.LOSESWIM) {
+ speed = LOSE_SWIM_SPEED;
+ }
+
+ let done = update_fish_swim(delta, hovered_fish, fish_path, speed);
+
+ if (done) {
+ if (game.state === State.SWIM) {
+ game.state = State.STAND;
+ } else if (game.state === State.LOSESWIM) {
+ lose();
+ }
+ if (game.state === State.SWIM) {
+ fish = null;
+ }
+ potential_fish_target = null;
+ handle_mousemove(game, {}, mouse_x, mouse_y);
+ }
+ } else if (game.state === State.CONGREGATE) {
+ let all_done = true;
+ for (let f of fish) {
+ if (f.species === creatureID.fish) {
+ let done = update_fish_swim(delta, f, f.win_path, f.win_swim_speed);
+ if (f.win_path.length > 0) {
+ all_done = false;
+ }
+ }
+ }
+
+ if (all_done) {
+ win();
+ }
+ } else if (game.state === State.LOSE) {
+ if (x_frame < MAX_X_FRAME) {
+ x_timer += delta;
+ while (x_timer > X_FRAME_LENGTH) {
+ x_timer -= X_FRAME_LENGTH;
+ x_frame ++;
+ }
+ }
+ } else if (game.state === State.WIN) {
+ update_hearts(delta);
+ }
+}
+
+function update_fish_swim(delta, fish, path, speed) {
+ fish.move_fraction += speed * delta / 1000;
+ if (fish.move_fraction > 1) {
+ fish.x = fish.target_x;
+ fish.y = fish.target_y;
+ fish.move_fraction = 0;
+ if (path.length > 0 && path[0][0] === fish.x && path[0][1] === fish.y) {
+ /* Pop off start coordinate from path... */
+ path.shift();
+ }
+
+ if (path.length == 0) {
+ console.log("Swim done");
+ if (game.state === State.SWIM) {
+ fish.rotate = 0;
+ }
+ fish.swimming = false;
+ return true;
+ } else {
+ fish.target_x = path[0][0];
+ fish.target_y = path[0][1];
+
+ let old_rotate = fish.rotate;
+ let old_hflip = fish.hflip;
+
+ if (fish.target_x > fish.x) {
+ fish.hflip = false;
+ } else if (fish.target_x < fish.x) {
+ fish.hflip = true;
+ }
+
+ if (fish.target_y > fish.y) {
+ fish.rotate = 90;
+ } else if (fish.target_y < fish.y) {
+ fish.rotate = -90;
+ } else {
+ fish.rotate = 0;
+ }
+
+ if (fish.rotate !== old_rotate || fish.hflip !== old_hflip) {
+ game.sfx.drip.play();
+ }
+ }
+ }
+ return false;
+}
+
+function update_hearts(delta) {
+ for (let h of hearts) {
+ h.x += h.xspeed * delta / 1000;
+ h.y += h.yspeed * delta / 1000;
+ h.yspeed += h.gravity * delta / 1000;
+ }
+}
+
+let bubble_sfx_timeout = 0;
+function create_bubble(x, y) {
+ bubbles.push({
+ x: x,
+ y: y,
+ frame: Math.floor(Math.random() * 2),
+ timer: Math.random() * BUBBLE_FRAME_LENGTH,
+ });
+
+ if (Math.random() < 0.8 && bubble_sfx_timeout <= 0) {
+ let decider = Math.random();
+ if (decider < 0.25) {
+ game.sfx.bubble.play();
+ } else if (decider < 0.5) {
+ game.sfx.bubble2.play();
+ } else if (decider < 0.75) {
+ game.sfx.bubble3.play();
+ } else {
+ game.sfx.bubble4.play();
+ }
+ bubble_sfx_timeout = 800;
+ }
+}
+
+function update_bubbles(delta) {
+ bubble_sfx_timeout -= delta;
+
+ for (let f of fish) {
+ if (!f.hasOwnProperty("bubble_timeout")) {
+ f.bubble_timeout = 0;
+ }
+ f.bubble_timeout -= delta;
+
+ if (f.swimming) continue;
+ if (f.hidden) continue;
+ if (f.bubble_timeout > 0) continue;
+
+ if (Math.random() < 0.002) {
+ let x_offset = 0;
+
+ if (f.species === creatureID.fish) {
+ if (f.hflip) {
+ x_offset = 6;
+ } else {
+ x_offset = 18;
+ }
+ } else if (f.species === creatureID.shark) {
+ if (f.hflip) {
+ x_offset = 3;
+ } else {
+ x_offset = 21;
+ }
+ }
+
+ create_bubble(f.x * game.tile_size + x_offset, f.y * game.tile_size + game.tile_size / 2);
+
+ f.bubble_timeout = 1500;
+ }
+ }
+
+ for (let d of decorations) {
+ if (!d.hasOwnProperty("bubble_timeout")) {
+ d.bubble_timeout = 0;
+ }
+ d.bubble_timeout -= delta;
+
+ if (d.bubble_timeout > 0) continue;
+ if (tile_at(d.x, d.y) === tileID.wall) continue;
+
+ if (Math.random() < 0.001) {
+ let x_offset = Math.random() * game.tile_size * 2 / 3 - game.tile_size / 3;
+
+ create_bubble(d.x * game.tile_size + game.tile_size / 2 + x_offset, d.y * game.tile_size + game.tile_size / 2);
+
+ d.bubble_timeout = 3000;
+ }
+ }
+
+ for (let b of bubbles) {
+ b.timer += delta;
+ while (b.timer > BUBBLE_FRAME_LENGTH) {
+ b.timer -= BUBBLE_FRAME_LENGTH;
+ b.frame ++;
+ b.frame %= 2;
+ }
+ b.y -= BUBBLE_UP_SPEED * delta / 1000;
+ if (zb.mod(b.y, game.tile_size) < game.tile_size / 2) {
+ if (tile_or_block_at(Math.floor(b.x / game.tile_size), Math.floor(b.y / game.tile_size))) {
+ b.deleteme = true;
+ }
+ }
+ }
+
+ bubbles = bubbles.filter(b => !b.deleteme);
+}
+
+/* ---------- draw functions ----------- */
+
+/* DRAW */
+function do_draw(ctx) {
+ draw_bubbles(ctx);
+
+ draw_map(ctx);
+
+ if (!isNaN(parseInt(level_number))) {
+ zb.sprite_draw(ctx, game.img.levelnums, game.tile_size, game.tile_size, 0, level_number - 1, 0, (game.level_h - 1) * game.tile_size);
+ }
+
+ draw_decorations(ctx);
+
+ zb.buttons.draw(ctx, game.buttons.ingame);
+
+ if (game.img.levels.hasOwnProperty(level_number)) {
+ zb.screen_draw(ctx, game.img.levels[level_number]);
+ }
+
+ draw_blocks(ctx);
+
+ draw_creatures(ctx);
+
+ draw_hearts(ctx);
+
+ if (potential_fish_target) {
+ zb.sprite_draw(ctx, game.img.selector, 28, 28, 0, zb.mod(selector_frame + 1, 2),
+ potential_fish_target[0] * game.tile_size - 2, potential_fish_target[1] * game.tile_size - 2);
+ }
+
+ if (game.state === State.LOSE) {
+ zb.sprite_draw(ctx, game.img.x, 48, 48, 0, x_frame, hovered_fish.x * game.tile_size - 12, hovered_fish.y * game.tile_size - 12);
+ }
+
+ if (game.state === State.WIN || game.state === State.LOSE) {
+ ctx.save();
+ ctx.globalAlpha = screen_message_alpha;
+ if (game.state === State.WIN) {
+ zb.screen_draw(ctx, game.img.success);
+ } else {
+ zb.screen_draw(ctx, game.img.failure);
+ }
+ ctx.restore();
+ }
+}
+
+function do_draw_menu(ctx) {
+ zb.screen_draw(ctx, game.img.menuimage);
+ zb.buttons.draw(ctx, game.buttons.menu);
+}
+
+function do_draw_youwon(ctx) {
+ zb.screen_draw(ctx, game.img.youwon);
+}
+
+function rpgmaker_tile_format_thing(x, y, tile_check_func) {
+ let u = tile_check_func(x, y - 1);
+ let d = tile_check_func(x, y + 1);
+ let l = tile_check_func(x - 1, y);
+ let r = tile_check_func(x + 1, y);
+ let ul = tile_check_func(x - 1, y - 1);
+ let ur = tile_check_func(x + 1, y - 1);
+ let dl = tile_check_func(x - 1, y + 1);
+ let dr = tile_check_func(x + 1, y + 1);
+ let ulx, uly, urx, ury, dlx, dly, drx, dry;
+
+ /* Draw top left */
+ if (u) {
+ if (l) {
+ if (ul) {
+ /* Open water */
+ ulx = 2; uly = 4;
+ } else {
+ /* Upper left corner */
+ ulx = 2; uly = 0;
+ }
+ } else {
+ /* Left border */
+ ulx = 0; uly = 4;
+ }
+ } else {
+ if (l) {
+ /* Top border */
+ ulx = 2; uly = 2;
+ } else {
+ /* Very corner */
+ ulx = 0; uly = 2;
+ }
+ }
+
+ /* Draw top right */
+ if (u) {
+ if (r) {
+ if (ur) {
+ /* Open water */
+ urx = 1; ury = 4;
+ } else {
+ /* Upper right corner */
+ urx = 3; ury = 0;
+ }
+ } else {
+ /* Right border */
+ urx = 3; ury = 4;
+ }
+ } else {
+ if (r) {
+ /* Top border */
+ urx = 1; ury = 2;
+ } else {
+ /* Very corner */
+ urx = 3; ury = 2;
+ }
+ }
+
+ /* Draw bottom left */
+ if (d) {
+ if (l) {
+ if (dl) {
+ /* Open water */
+ dlx = 2; dly = 3;
+ } else {
+ /* Bottom left corner */
+ dlx = 2; dly = 1;
+ }
+ } else {
+ /* Left border */
+ dlx = 0; dly = 3;
+ }
+ } else {
+ if (l) {
+ /* Bottom border */
+ dlx = 2; dly = 5;
+ } else {
+ /* Very corner */
+ dlx = 0; dly = 5;
+ }
+ }
+
+ /* Draw bottom right */
+ if (d) {
+ if (r) {
+ if (dr) {
+ /* Open water */
+ drx = 1; dry = 3;
+ } else {
+ /* Bottom right corner */
+ drx = 3; dry = 1;
+ }
+ } else {
+ /* Right border */
+ drx = 3; dry = 3;
+ }
+ } else {
+ if (r) {
+ /* Bottom border */
+ drx = 1; dry = 5;
+ } else {
+ /* Very corner */
+ drx = 3; dry = 5;
+ }
+ }
+
+ return { ulx: ulx, uly: uly, urx: urx, ury: ury, dlx: dlx, dly: dly, drx: drx, dry: dry };
+}
+
+function draw_rpgmaker_tile(ctx, img, x, y, rpgmaker_tft, state) {
+ let ts = game.tile_size;
+ let { ulx, uly, urx, ury, dlx, dly, drx, dry } = rpgmaker_tft;
+ zb.sprite_draw(ctx, img, ts / 2, ts / 2, ulx, uly + state * 6, x, y);
+ zb.sprite_draw(ctx, img, ts / 2, ts / 2, urx, ury + state * 6, x + ts / 2, y);
+ zb.sprite_draw(ctx, img, ts / 2, ts / 2, dlx, dly + state * 6, x, y + ts / 2);
+ zb.sprite_draw(ctx, img, ts / 2, ts / 2, drx, dry + state * 6, x + ts / 2, y + ts / 2);
+}
+
+function draw_map(ctx) {
+ for (let y = 0; y < game.level_h; y++) {
+ for (let x = 0; x < game.level_w; x++) {
+ if (tile_at(x, y) === tileID.wall) {
+ let tileinfo = rpgmaker_tile_format_thing(x, y, (x, y) => tile_at(x, y) === tileID.wall);
+ draw_rpgmaker_tile(ctx, game.img.wall_tile, x * game.tile_size, y * game.tile_size, tileinfo, 0);
+ }
+ }
+ }
+}
+
+function draw_blocks(ctx) {
+ for (let block of blocks) {
+ for (let tile of block.tiles) {
+ let tileinfo = rpgmaker_tile_format_thing(tile[0], tile[1], (x, y) => {
+ for (let tile of block.tiles) {
+ if (tile[0] === x && tile[1] === y) {
+ return true;
+ }
+ }
+ return false;
+ });
+ draw_rpgmaker_tile(ctx, game.img.move_tile,
+ tile[0] * game.tile_size + Math.round(block.target_dx * game.tile_size * block.move_fraction),
+ tile[1] * game.tile_size + Math.round(block.target_dy * game.tile_size * block.move_fraction),
+ tileinfo, block.state);
+ }
+ }
+}
+
+function draw_decorations(ctx) {
+ for (let d of decorations) {
+ ctx.save();
+ if (d.type === 2) {
+ ctx.translate(0, 1);
+ }
+ zb.sprite_draw(ctx, game.img.decorations, game.tile_size, game.tile_size, d.type, 0, d.x * game.tile_size, d.y * game.tile_size);
+ ctx.restore();
+ }
+}
+
+function draw_creatures(ctx) {
+ for (let f of fish) {
+ if (f.hidden) continue;
+
+ ctx.save();
+
+ let mf = f.move_fraction || 0;
+ let tx = f.target_x !== undefined ? f.target_x : f.x;
+ let ty = f.target_y !== undefined ? f.target_y : f.y;
+
+ let drift_y_offset = Math.round(1.9 * Math.sin(f.drift * 2 * Math.PI));
+
+ ctx.translate(f.x * game.tile_size * (1 - mf) + tx * game.tile_size * mf,
+ f.y * game.tile_size * (1 - mf) + ty * game.tile_size * mf);
+
+ if (f.hflip) {
+ ctx.translate(game.tile_size, 0);
+ ctx.scale(-1, 1);
+ }
+
+ if (f.vflip) {
+ ctx.translate(0, game.tile_size);
+ ctx.scale(1, -1);
+ }
+
+ if (f.rotate) {
+ ctx.translate(game.tile_size / 2, game.tile_size / 2);
+ ctx.rotate(f.rotate * Math.PI / 180);
+ ctx.translate(-game.tile_size / 2, -game.tile_size / 2);
+ }
+
+ zb.sprite_draw(ctx, game.img.fish, game.tile_size, game.tile_size, f.type, f.frame, 0, drift_y_offset);
+ if (f === hovered_fish && (game.state === State.STAND || game.state === State.FISH)) {
+ zb.sprite_draw(ctx, game.img.selector, 28, 28, 0, selector_frame, -2, -2);
+ }
+
+ ctx.restore();
+ }
+}
+
+function draw_bubbles(ctx) {
+ for (let b of bubbles) {
+ zb.sprite_draw(ctx, game.img.bubble, 5, 11, 0, b.frame, Math.round(b.x - game.img.bubble.width / 2), Math.round(b.y - game.img.bubble.height / 2));
+ }
+}
+
+function draw_hearts(ctx) {
+ for (let h of hearts) {
+ ctx.drawImage(game.img.heart, Math.round(h.x - game.img.heart.width / 2), Math.round(h.y - game.img.heart.height / 2));
+ }
+}
+
+/* ---------- event handlers ------------ */
+
+function handle_gamestart(game) {
+ console.log("Game start!");
+
+ //game.music.bgm_menu.play();
+}
+
+function handle_mousedown(game, e, x, y) {
+ mouse_x = x;
+ mouse_y = y;
+ mouse_is_down = true;
+
+ if (game.mode === 'menu') {
+ if (zb.buttons.click(game.buttons.menu, x, y)) return;
+ } else if (game.mode === 'game') {
+ if (zb.buttons.click(game.buttons.ingame, x, y)) return;
+
+ let tile_x = Math.floor(mouse_x / game.tile_size);
+ let tile_y = Math.floor(mouse_y / game.tile_size);
+
+ if (game.mode === 'game') {
+ //if (zb.buttons.click(buttons.ingame, x, y)) return;
+
+ if (game.state === State.WIN) {
+ return;
+ }
+
+ if (game.state === State.STAND) {
+ if (hovered_block) {
+ hovered_block.state = 2;
+ game.state = State.BLOCK;
+ start_drag_x = tile_x;
+ start_drag_y = tile_y;
+ }
+ }
+ }
+ }
+}
+
+function handle_mouseup(game, e, x, y) {
+ mouse_is_down = false;
+ just_clicked_block = true;
+
+ if (game.mode === 'youwon') {
+ game.long_transition(zb.transition.type.fade, 1000, function() {
+ continue_level = 1;
+ game.buttons.menu.buttons[1].disabled = true;
+ game.mode = 'menu';
+ });
+ } else if (game.mode === 'menu') {
+ zb.buttons.update(game.buttons.menu, x, y);
+ } else if (game.mode === 'game') {
+ zb.buttons.update(game.buttons.ingame, x, y);
+
+ if (game.state === State.WIN) {
+ advance_level();
+ } else if (game.state === State.BLOCK) {
+ game.state = State.STAND;
+ handle_mousemove(game, e, x, y);
+ } else if (game.state === State.FISH) {
+ if (potential_fish_target === null) {
+ game.state = State.STAND;
+ handle_mousemove(game, e, x, y);
+ } else {
+ create_undo_point();
+ game.state = State.SWIM;
+ start_swimming(hovered_fish);
+ }
+ } else if (game.state === State.STAND) {
+ if (hovered_fish) {
+ game.state = State.FISH;
+ }
+ }
+
+ if (won_everything) {
+ return;
+ }
+ }
+}
+
+function handle_mouseleave(game, e) {
+ mouse_is_down = false;
+ if (game.mode === 'game') {
+ if (game.state === State.STAND || game.state === State.BLOCK) {
+ handle_mouseup(game, e, -1, -1);
+ }
+ }
+}
+
+function handle_mousemove(game, e, x, y) {
+ mouse_x = x;
+ mouse_y = y;
+
+ let tile_x = Math.floor(mouse_x / game.tile_size);
+ let tile_y = Math.floor(mouse_y / game.tile_size);
+
+ if (game.mode === 'menu') {
+ zb.buttons.update(game.buttons.menu, mouse_x, mouse_y);
+ } else if (game.mode === 'game') {
+ zb.buttons.update(game.buttons.ingame, mouse_x, mouse_y);
+
+ if (game.state === State.STAND) {
+ hovered_block = null;
+ for (let b of blocks) {
+ b.state = 0;
+ for (let t of b.tiles) {
+ if (t[0] === tile_x && t[1] === tile_y) {
+ b.state = 1;
+ hovered_block = b;
+ break;
+ }
+ }
+ }
+
+ hovered_fish = fish_at(tile_x, tile_y);
+ } else if (game.state === State.FISH) {
+ potential_fish_target = null;
+ fish_path = path_between(hovered_fish.x, hovered_fish.y, tile_x, tile_y, true);
+ if (fish_path) {
+ potential_fish_target = [tile_x, tile_y];
+ }
+ } else if (game.state === State.BLOCK) {
+ if (tile_x !== start_drag_x || tile_y !== start_drag_y) {
+ /* Move block */
+ let dx = 0;
+ let dy = 0;
+ hovered_block.target_dx = 0;
+ hovered_block.target_dy = 0;
+ if (tile_x > start_drag_x) {
+ dx = 1;
+ } else if (tile_x < start_drag_x) {
+ dx = -1;
+ } else if (tile_y > start_drag_y) {
+ dy = 1;
+ } else if (tile_y < start_drag_y) {
+ dy = -1;
+ }
+
+ let free_to_move = true;
+ let mouse_inside_block = false;
+ for (let t of hovered_block.tiles) {
+ if (anything_at(t[0] + dx, t[1] + dy, hovered_block)) {
+ free_to_move = false;
+ }
+
+ if (t[0] === tile_x && t[1] === tile_y) {
+ mouse_inside_block = true;
+ }
+ }
+
+ if (free_to_move) {
+ if (just_clicked_block) {
+ create_undo_point();
+ just_clicked_block = false;
+ }
+ hovered_block.move_fraction = 0;
+ hovered_block.target_dx = dx;
+ hovered_block.target_dy = dy;
+ game.state = State.DRAG;
+ game.sfx.drag.play();
+ } else if (mouse_inside_block) {
+ start_drag_x = tile_x;
+ start_drag_y = tile_y;
+ }
+ }
+ }
+ }
+}
+
+function toggle_mute_button() {
+ if (game.buttons.ingame.buttons[2].id == 2) {
+ game.buttons.ingame.buttons[2].id = 3;
+ } else {
+ game.buttons.ingame.buttons[2].id = 2;
+ }
+ game.toggle_mute();
+}
+
+function handle_keyup(game, e) {
+ if (game.transition.is_transitioning) return;
+
+ // key up
+ switch (e.key) {
+ case 'm':
+ toggle_mute_button();
+ e.preventDefault();
+ break;
+ case 'r':
+ reset();
+ e.preventDefault();
+ break;
+ case 'z':
+ undo();
+ e.preventDefault();
+ break;
+ }
+}
+
+function new_game_button() {
+ game.music.bgm.play();
+ game.start_transition(zb.transition.type.fade, 1000, function() {
+ game.mode = 'game';
+ level_number = 1;
+ load_level();
+ });
+}
+
+function continue_button() {
+ game.music.bgm.play();
+ game.start_transition(zb.transition.type.fade, 1000, function() {
+ game.mode = 'game';
+ level_number = continue_level;
+ load_level();
+ });
+}
--- /dev/null
+<!doctype html>
+<html>
+<head><title>
+ fish shark game
+</title>
+<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, width=100%, target-densitydpi=device-dpi, user-scalable=no" />
+<style>
+* { border: 0; margin: 0; overflow: hidden }
+/*body { height: 100vh }
+canvas { object-fit: contain; width: 100%; height: 100% }*/
+</style>
+</head>
+<body style="background: black">
+ <canvas id="canvas" width="576" height="864" style="max-width:100%" tabindex=1> </canvas>
+</body>
+<script src="levels.js"></script>
+<script src="zucchinibread.js"></script>
+<script src="game.js"></script>
+</html>
--- /dev/null
+var levels={
+ 10: { map: [0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[0,3], ],[[2,3], [2,4], [1,3], [1,4], ],[[3,3], [3,4], [4,3], ],[[4,4], ],[[1,6], [1,7], [2,6], [2,7], ],[[3,7], [4,6], [4,7], [3,6], ],], fish: [[3,1], [1,9], ], sharks: [[5,5], ], decorations: [[6,0,0], [4,1,2], [5,1,2], [7,1,0], [6,2,0], [5,7,3], [6,7,3], [4,9,1], [6,9,1], [1,10,2], [2,10,2], [3,10,2], [5,10,1], [7,10,1], ] },
+ 1: { map: [0,0,0,0,0,0,0,1,1,1,1,1,0,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0,1,1,1,1,0,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,], blocks: [[[4,3], [5,3], [4,2], [5,2], ],[[4,4], [4,5], ],[[4,6], [3,5], [3,6], ],[[5,5], [6,5], ],[[5,7], ],], fish: [[2,0], [1,7], [6,8], ], sharks: [], decorations: [[0,0,2], [1,0,2], [7,2,1], [0,8,2], [1,8,2], [4,8,1], ] },
+ 2: { map: [0,0,1,0,0,0,0,1,0,1,1,0,0,0,0,1,0,1,0,0,0,0,1,1,0,1,0,0,0,0,1,1,0,0,1,1,0,0,1,1,0,1,1,1,0,0,0,1,0,1,1,1,0,0,1,1,0,0,0,0,0,0,1,1,0,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[4,0], [5,0], [5,1], [4,1], ],[[0,1], [0,2], [0,3], [0,4], ],[[4,3], [5,3], [4,2], [5,2], ],[[1,4], ],[[4,4], [5,4], [4,5], ],[[5,5], [6,5], ],], fish: [[1,0], [6,1], ], sharks: [], decorations: [[2,3,2], [3,3,2], [0,8,2], [6,9,0], [5,10,0], [7,10,0], ] },
+ 3: { map: [1,1,0,0,0,0,1,1,0,1,0,0,0,1,1,1,0,0,0,0,0,1,0,1,0,0,0,0,0,1,0,1,0,0,0,1,1,1,0,1,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[3,1], [4,1], [2,1], ],[[4,2], [4,3], ],[[0,7], [1,5], [1,4], [0,6], [0,5], [1,6], ],[[6,4], [6,5], ],[[6,6], [5,5], [5,6], ],], fish: [[5,0], [6,3], [0,9], ], sharks: [], decorations: [[7,0,1], [2,4,2], [5,8,2], [4,9,2], [7,9,1], [6,10,1], ] },
+ 4: { map: [0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,], blocks: [[[4,0], [6,1], [5,1], [5,0], [6,0], [4,1], ],[[4,3], [5,3], [4,2], [5,2], ],[[4,4], [3,4], [5,4], [0,3], [2,3], [3,3], [1,3], ],], fish: [[3,1], [2,6], ], sharks: [[6,3], ], decorations: [[1,0,0], [3,7,3], [4,7,3], [5,7,3], ] },
+ 5: { map: [0,0,0,0,1,1,1,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[3,5], ],[[4,5], [4,6], [3,6], ],[[3,7], [3,8], [3,9], ],], fish: [[5,1], [6,1], [5,2], [6,2], [5,3], [6,3], [5,4], [6,4], [6,5], [4,8], ], sharks: [[1,2], ], decorations: [[4,0,1], [7,6,1], [6,8,1], [4,9,3], [0,10,3], [7,10,1], ] },
+ 6: { map: [0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[2,3], [0,3], [1,3], [3,3], ],[[2,7], [3,7], [2,6], [3,6], [2,5], ],[[5,5], [5,6], [6,5], ],[[4,8], [4,7], [5,7], [5,8], ],], fish: [[2,0], [2,2], ], sharks: [[6,7], [0,10], ], decorations: [[0,0,2], [1,0,2], [7,1,0], [2,10,3], [3,10,3], [5,10,1], ] },
+ 7: { map: [1,0,0,1,0,0,1,1,0,0,0,1,0,0,0,1,0,1,1,1,1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,1,1,1,0,0,0,0,1,1,0,0,0,1,0,0,0,1,0,1,1,1,1,1,0,1,0,0,0,1,0,0,0,1,1,0,0,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[1,3], [1,4], ],[[2,4], [2,5], ],[[4,4], [4,5], ],[[5,4], [5,5], ],], fish: [[2,1], [4,1], [2,8], [4,8], ], sharks: [[3,4], [3,5], ], decorations: [[1,1,2], [5,1,2], [3,3,0], [3,6,0], [1,9,3], [2,9,3], [4,9,3], [5,9,3], ] },
+ 8: { map: [1,1,0,0,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[6,1], [6,2], [5,0], [5,1], ],[[0,2], [1,2], [0,3], ],[[2,3], [1,3], [1,4], ],[[2,4], [2,5], [3,5], ],[[3,7], [4,6], [5,6], [3,6], ],[[3,8], [1,8], [2,9], [3,9], [2,8], ],[[0,9], [0,10], ],], fish: [[6,0], [5,7], [1,9], ], sharks: [[2,2], [5,4], [1,5], ], decorations: [[6,3,2], [4,8,3], [6,8,0], [5,9,0], [6,9,1], [5,10,1], [6,10,0], ] },
+ 9: { map: [0,0,0,0,0,0,1,1,0,0,0,0,1,0,1,1,1,1,1,1,1,0,1,1,1,0,0,0,1,0,1,1,1,0,1,0,1,0,1,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[5,3], [5,4], [5,5], ],[[2,5], [2,6], [1,5], ],[[4,6], [5,6], [3,5], [3,6], ],[[1,10], ],], fish: [[0,0], [1,0], [0,1], [1,1], [0,5], ], sharks: [[1,6], [1,7], [2,7], [3,7], [4,7], [5,7], [1,8], [2,8], [5,8], [1,9], [2,9], [3,9], [4,9], [5,9], ], decorations: [[6,0,1], [7,0,0], [2,1,2], [3,1,2], [6,3,0], [2,4,0], [6,4,1], [6,5,0], [7,6,0], [7,7,1], [6,8,1], [6,9,0], [7,9,1], [6,11,0], [7,11,1], ] },
+ test: { map: [1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,0,0,1,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,], blocks: [[[5,3], [5,4], [4,4], [4,3], ],], fish: [[3,2], [3,6], [3,9], ], sharks: [], decorations: [] },
+}
--- /dev/null
+"use strict";
+
+/* Please forgive my strictly procedural/functional style here.
+ * Thanks for reading though! */
+
+let zb = (function() {
+ /* ---- Util ---- */
+
+ function mod(x, n) {
+ return ((x%n)+n)%n;
+ }
+
+ function sgn(x) {
+ if (x === 0) {
+ return 0;
+ } else {
+ return x / Math.abs(x);
+ }
+ }
+
+ function as_hex(num, length) {
+ let result = Math.round(num).toString(16);
+
+ if (length) {
+ while (result.length < length) {
+ result = "0" + result;
+ }
+ }
+
+ return result;
+ }
+
+ function copy_list(list) {
+ let newlist = [];
+ for (let x of list) {
+ newlist.push(x);
+ }
+ return newlist;
+ }
+
+ function copy_flat_objlist(list) {
+ let newlist = [];
+ for (let x of list) {
+ newlist.push({ ...x });
+ }
+ return newlist;
+ }
+
+ /* ---- Resource loading / loading screen ---- */
+
+ let _audiocheck = document.createElement('audio');
+
+ let _SFX_ARRAY_SIZE = 10;
+
+ /* Returns a callback that should be called when a resource finishes loading. */
+ function _register_resource(name, game, callback) {
+ game._total_things_to_load ++;
+ console.log("Loading", name + ". Things to load:", game._total_things_to_load);
+ return function() {
+ if (!game.ready_to_go) {
+ game._things_loaded ++;
+ console.log("Loaded", name + ". Things loaded:", game._things_loaded, "/", game._total_things_to_load);
+ if (game.load_with_progress_bar) {
+ if (game._progressbar_img_loaded) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(
+ game.img._progressbar,
+ 0,
+ 0,
+ Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width),
+ game.img._progressbar.height,
+ Math.round(game.canvas_w / 2 / game.draw_scale - game.img._progressbar.width / 2),
+ Math.round(game.canvas_h / 2 / game.draw_scale - game.img._progressbar.height / 2),
+ Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width),
+ game.img._progressbar.height,
+ );
+ game.ctx.global.restore();
+ }
+ }
+ _check_if_loaded(game);
+ if (callback) {
+ callback();
+ }
+ }
+ }
+ }
+
+ /* Check if audio failed to load bc file doesn't exist and print error message. */
+ /* TODO add the same thing for an image */
+ function _sound_resource_error(name, game, audio) {
+ return function(e) {
+ if (audio === undefined) {
+ console.error("Error loading resource, skipping:", name);
+ game._things_loaded ++;
+ _check_if_loaded(game);
+ }
+ }
+ }
+
+ /* Check if the game has loaded everything and if so show the 'click to start' image */
+ function _check_if_loaded(game) {
+ if (game.ready_to_go) return;
+
+ if (game._things_loaded >= game._total_things_to_load) {
+ console.log("Ready");
+ game.ready_to_go = true;
+ game._on_ready();
+ }
+ }
+
+ function _register_sfx(sfxdata, game) {
+ for (let key in sfxdata) {
+ let sfx_size = sfxdata[key].copies || _SFX_ARRAY_SIZE;
+ let sfx_array = new Array(sfx_size);
+ for (let i = 0; i < sfx_size; i++) {
+ sfx_array[i] = new Audio(sfxdata[key].path);
+ sfx_array[i].addEventListener('canplaythrough',
+ _register_resource(sfxdata[key].path + '#' + (i+1), game), false);
+ sfx_array[i].addEventListener('error',
+ _sound_resource_error(sfxdata[key].path + '#' + (i+1), game), false, sfx_array[i]);
+ if (sfxdata[key].hasOwnProperty('volume')) {
+ sfx_array[i].volume = sfxdata[key].volume;
+ }
+ }
+ game.sfx[key] = {
+ _array: sfx_array,
+ _index: 0,
+ play: function() {
+ if (!game.muted) {
+ this._array[this._index].currentTime = 0;
+ this._array[this._index].play();
+ this._index ++;
+ this._index = mod(this._index, _SFX_ARRAY_SIZE);
+ }
+ }
+ }
+ }
+ }
+
+ function _register_music(musicdata, game) {
+ for (let key in musicdata) {
+ let musicpath;
+ if (musicdata[key].path.match(/\.[a-z]{3}$/)) {
+ musicpath = musicdata[key].path;
+ } else if (_audiocheck.canPlayType('audio/mpeg')) {
+ musicpath = musicdata[key].path + '.mp3';
+ } else if (_audiocheck.canPlayType('audio/ogg')) {
+ musicpath = musicdata[key].path + '.ogg';
+ } else {
+ console.log("No supported music type known :(");
+ return;
+ }
+ let music = new Audio(musicpath);
+ if (musicdata[key].hasOwnProperty('volume')) {
+ music.volume = musicdata[key].volume;
+ }
+
+ if (!musicdata[key].hasOwnProperty('loop') || musicdata[key].loop) {
+ /* We loop by default unless 'loop: false' is specified. */
+ music.loop = true;
+ }
+ music.addEventListener('canplaythrough', _register_resource(musicpath, game), false);
+ music.addEventListener('error', _sound_resource_error(musicpath, game, music), false);
+
+ /* Handle music changes when muted, so when we unmute,
+ * the correct music will be playing. */
+ music._og_play = music.play;
+ music.play = function() {
+ if (game.muted) {
+ music.was_playing = true;
+ } else {
+ music._og_play();
+ }
+ }
+
+ music._og_pause = music.pause;
+ music.pause = function() {
+ if (game.muted) {
+ music.was_playing = false;
+ } else {
+ music._og_pause();
+ }
+ }
+
+ game.music[key] = music;
+ }
+ }
+
+ function _register_images(imgdata, game) {
+ function _recursive_load_images(pathmap) {
+ let result = {};
+ for (let key in pathmap) {
+ if (typeof pathmap[key] === 'object') {
+ result[key] = _recursive_load_images(pathmap[key]);
+ } else {
+ result[key] = new Image();
+ result[key].onload = _register_resource(pathmap[key], game);
+ result[key].src = pathmap[key];
+ }
+ }
+ return result;
+ }
+
+ let loaded_imgs = _recursive_load_images(imgdata);
+ for (let k in loaded_imgs) {
+ game.img[k] = loaded_imgs[k];
+ }
+ }
+
+ function _create_canvas_context(canvas) {
+ let new_canvas = document.createElement('canvas');
+ new_canvas.width = canvas.width;
+ new_canvas.height = canvas.height;
+
+ let new_ctx = new_canvas.getContext('2d');
+ new_ctx.imageSmoothingEnabled = false;
+ new_ctx.webkitImageSmoothingEnabled = false;
+ new_ctx.mozImageSmoothingEnabled = false;
+
+ return new_ctx;
+ }
+
+ function create_game(params) {
+ let game_props = {...params};
+
+ game_props.frame_rate = params.frame_rate || 60;
+
+ game_props.canvas_w = params.canvas_w || 640;
+ game_props.canvas_h = params.canvas_h || 480;
+ game_props.draw_scale = params.draw_scale || 4;
+ game_props.top_border = params.top_border || 0;
+ game_props.bottom_border = params.bottom_border || 8;
+ game_props.left_border = params.left_border || 0;
+ game_props.right_border = params.right_border || 0;
+
+ game_props.tile_size = params.tile_size || 16;
+ game_props.level_w = params.level_w || 10;
+ game_props.level_h = params.level_h || 7;
+
+ game_props.screen_w = game_props.canvas_w / game_props.draw_scale;
+ game_props.screen_h = game_props.canvas_h / game_props.draw_scale;
+
+ game_props.background_color = params.background_color || '#000000';
+
+ let canvas = document.getElementById(game_props.canvas);
+
+ let global_ctx = canvas.getContext('2d');
+ global_ctx.imageSmoothingEnabled = false;
+ global_ctx.webkitImageSmoothingEnabled = false;
+ global_ctx.mozImageSmoothingEnabled = false;
+
+ let mask_canvas = document.createElement('canvas');
+ mask_canvas.width = canvas.width;
+ mask_canvas.height = canvas.height;
+ let mask_ctx = mask_canvas.getContext('2d');
+ mask_ctx.imageSmoothingEnabled = false;
+ mask_ctx.webkitImageSmoothingEnabled = false;
+ mask_ctx.mozImageSmoothingEnabled = false;
+
+ let copy_canvas = document.createElement('canvas');
+ copy_canvas.width = canvas.width;
+ copy_canvas.height = canvas.height;
+ let copy_ctx = copy_canvas.getContext('2d');
+ copy_ctx.imageSmoothingEnabled = false;
+ copy_ctx.webkitImageSmoothingEnabled = false;
+ copy_ctx.mozImageSmoothingEnabled = false;
+
+ let draw_canvas = document.createElement('canvas');
+ draw_canvas.width = canvas.width;
+ draw_canvas.height = canvas.height;
+ let draw_ctx = draw_canvas.getContext('2d');
+ draw_ctx.imageSmoothingEnabled = false;
+ draw_ctx.webkitImageSmoothingEnabled = false;
+ draw_ctx.mozImageSmoothingEnabled = false;
+
+ let game = {
+ /* General properties */
+ ...game_props,
+
+ canvas: canvas,
+
+ /* Loading */
+ ready_to_go: false,
+ _total_things_to_load: 1,
+ _things_loaded: 0,
+ resources_ready: function() {
+ this._things_loaded ++;
+ console.log("Finished enumerating resources to load. Things loaded:",
+ this._things_loaded, "/", this._total_things_to_load);
+ _check_if_loaded(this);
+ },
+ _on_ready: function() {
+ this.ready_to_go = true;
+ if (game.img._clicktostart && game.img._clicktostart.complete) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(game.img._clicktostart, 0, 0);
+ game.ctx.global.restore();
+ }
+ },
+ playing: false,
+ play: function() {
+ this.playing = true;
+ if (this.events.gamestart) {
+ this.events.gamestart(this);
+ }
+ _loop(this);
+ },
+ _norun: false,
+
+ /* Audio */
+ sfx: {},
+ music: {},
+ muted: false,
+ mute: function() {
+ _mute(this);
+ },
+ unmute: function() {
+ _unmute(this);
+ },
+ toggle_mute: function() {
+ if (this.muted) {
+ _unmute(this);
+ } else {
+ _mute(this);
+ }
+ },
+ register_sfx: function(sfxdata) {
+ _register_sfx(sfxdata, this);
+ },
+ register_music: function(musicdata) {
+ _register_music(musicdata, this);
+ },
+
+ /* Drawing */
+ ctx: {
+ global: global_ctx, /* context for the actual real canvas */
+ mask: mask_ctx, /* context for drawing the transition mask, gets scaled up */
+ copy: copy_ctx, /* context for copying the old screen on transition */
+ draw: draw_ctx, /* context for drawing the real level */
+ },
+ create_canvas_context: function() {
+ /* Function to return a fresh screen-sized canvas context, if we need it. */
+ return _create_canvas_context(this.canvas);
+ },
+ img: {},
+ register_images: function(imgdata) {
+ _register_images(imgdata, this);
+ },
+
+ /* Save */
+ save: function(key, data) {
+ _save(game, key, data);
+ },
+ load: function(key) {
+ return _load(game, key);
+ },
+
+ /* Transition system */
+ transition: _transition,
+ start_transition: function(type, length, callback, on_done) {
+ _start_transition(this, type, length, callback, on_done);
+ },
+ long_transition: function(type, length, callback, on_done) {
+ _long_transition(this, type, length, callback, on_done);
+ },
+
+ /* Input */
+ touchmode: false,
+
+ /* Mode */
+ change_mode: function(newmode) {
+ _change_mode(game, newmode);
+ }
+ };
+
+ window.requestAnimFrame = (function() {
+ return window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ function(callback, element) {
+ window.setTimeout(callback, 1000/game.frame_rate);
+ };
+ })();
+
+ /* Register event listeners */
+ for (let ev in params.events) {
+ /* Register any other events I guess */
+ canvas['on' + ev] = function(e) {
+ if (game.playing && !game._norun) {
+ params.events[ev](game, e);
+ }
+ }
+ }
+
+ /* Override with special events */
+ canvas.onmousedown = function(e) {
+ if (!game._norun) {
+ _handle_mousedown(game, e);
+ }
+ }
+
+ canvas.onmousemove = function(e) {
+ if (!game._norun) {
+ _handle_mousemove(game, e);
+ }
+ }
+
+ canvas.onmouseup = function(e) {
+ if (!game._norun) {
+ _handle_mouseup(game, e);
+ }
+ }
+
+ canvas.ontouchstart = function(e) {
+ if (!game._norun) {
+ game.touchmode = true;
+ _handle_touchstart(game, e);
+ }
+ e.preventDefault();
+ }
+
+ canvas.ontouchmove = function(e) {
+ if (!game._norun) {
+ game.touchmode = true;
+ _handle_touchmove(game, e);
+ }
+ e.preventDefault();
+ }
+
+ canvas.ontouchend = function(e) {
+ if (!game._norun) {
+ game.touchmode = true;
+ _handle_touchend(game, e);
+ }
+ e.preventDefault();
+ }
+
+ canvas.onkeydown = function(e) {
+ if (!game._norun && game.events.keydown) {
+ game.events.keydown(game, e);
+ e.preventDefault();
+ }
+ }
+
+ canvas.onkeyup = function(e) {
+ if (!game._norun && game.events.keyup) {
+ game.events.keyup(game, e);
+ e.preventDefault();
+ }
+ }
+
+ canvas.onblur = function(e) {
+ if (game.playing && !game.run_in_background) {
+ game._norun = true;
+ _stop_music(game);
+ }
+ }
+
+ canvas.onfocus = function(e) {
+ if (game.playing && !game.run_in_background) {
+ game._norun = false;
+ if (!game.muted) {
+ _start_music(game);
+ }
+ _loop(game);
+ }
+ }
+
+ /* Set loading stuff */
+ let loading_img = new Image();
+ loading_img.onload = _register_resource('loading.png', game, function() {
+ if (!game.ready_to_go) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(loading_img, 0, 0);
+ game.ctx.global.restore();
+ }
+ });
+ loading_img.src = 'loading.png';
+
+ if (game.load_with_progress_bar) {
+ game._progressbar_img_loaded = false;
+ game._progressbar_width = 0;
+ game._progressbar_height = 0;
+
+ let progressbar_img = new Image();
+ progressbar_img.onload = _register_resource('progressbar.png', game, function() {
+ game._progressbar_img_loaded = true;
+ game._progressbar_width = progressbar_img.width;
+ game._progressbar_height = progressbar_img.height;
+ if (!game.ready_to_go) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(loading_img, 0, 0);
+ game.ctx.global.restore();
+ }
+ });
+ progressbar_img.src = 'progressbar.png';
+ game.img._progressbar = progressbar_img;
+ }
+
+ if (!game.run_in_background) {
+ let pause_img = new Image();
+ pause_img.onload = _register_resource('pause.png', game);
+ pause_img.src = 'pause.png';
+ game.img._pause = pause_img;
+ }
+
+ if (game.hasOwnProperty('save_key')) {
+ let saveerror_img = new Image();
+ saveerror_img.onload = _register_resource('saveerror.png', game);
+ saveerror_img.src = 'saveerror.png';
+ game.img._saveerror = saveerror_img;
+ }
+
+ let clicktostart_img = new Image();
+ clicktostart_img.onload = _register_resource('clicktostart.png', game, function() {
+ if (game.ready_to_go) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(game.img._clicktostart, 0, 0);
+ game.ctx.global.restore();
+ }
+ });
+ clicktostart_img.src = 'clicktostart.png';
+ game.img._clicktostart = clicktostart_img;
+
+ return game;
+ }
+
+ /* ---- Audio ---- */
+
+ function _stop_music(game) {
+ if (game.muted) return;
+
+ for (let m in game.music) {
+ if (!game.music[m].paused) {
+ game.music[m].was_playing = true;
+ game.music[m].pause();
+ } else {
+ game.music[m].was_playing = false;
+ }
+ }
+ }
+
+ function _start_music(game, unmuting) {
+ if (game.muted && !unmuting) return;
+
+ for (let m in game.music) {
+ if (game.music[m].was_playing) {
+ game.music[m].play();
+ }
+ }
+ }
+
+ function _mute(game) {
+ _stop_music(game);
+ game.muted = true;
+ }
+
+ function _unmute(game) {
+ game.muted = false;
+ _start_music(game);
+ }
+
+ /* ---- Game update stuff ---- */
+
+ let _last_frame_time;
+ let _timedelta = 0;
+ function _loop(game, timestamp) {
+ if (timestamp == undefined) {
+ timestamp = 0;
+ _last_frame_time = timestamp;
+ }
+ _timedelta += timestamp - _last_frame_time;
+ _last_frame_time = timestamp;
+
+ while (_timedelta >= 1000 / game.frame_rate) {
+ _update(game, 1000 / game.frame_rate);
+ _timedelta -= 1000 / game.frame_rate;
+ }
+ _draw(game);
+
+ if (!game._norun) {
+ requestAnimFrame(function(timestamp) {
+ _loop(game, timestamp);
+ });
+ }
+ }
+
+ function _update(game, delta) {
+ if (game.update_func) {
+ game.update_func(delta);
+ }
+
+ if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].update) {
+ game.modes[game.mode].update(delta);
+ }
+
+ if (game.transition.is_transitioning) {
+ game.transition.timer += delta;
+ if (game.transition.timer > game.transition.end_time) {
+ _finish_transition(game);
+ }
+ }
+ }
+
+ function _change_mode(game, newmode) {
+ game.mode = newmode;
+ }
+
+ /* ---- UI ---- */
+
+ function create_buttons(button_data) {
+ for (let b of button_data.buttons) {
+ b.state = 0;
+ }
+
+ return button_data;
+ }
+
+ /* Call with a button set and given x / y mouse coordinates.
+ * Will update the button set in place.
+ * Should generally be called in mouse move and mouse up event handlers. */
+ function update_buttons(button_data, x, y) {
+ for (let button of button_data.buttons) {
+ if (button.state === 2) continue;
+ if (button.disabled) continue;
+
+ if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) {
+ if (!game.touchmode) {
+ button.state = 1;
+ }
+ } else if (button.state !== 2) {
+ button.state = 0;
+ }
+ }
+ }
+
+ /* Call with a button set and given x / y mouse coordinates.
+ * If a button is clicked, triggers its callback and returns true.
+ * Otherwise, returns false.
+ * Should be called in mouse down event handler. */
+ function click_button(button_data, x, y) {
+ for (let button of button_data.buttons) {
+ if (button.disabled) continue;
+
+ if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) {
+ button.state = 2;
+ game._clicked_button = button;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function _handle_clicked_button(game) {
+ if (game._clicked_button) {
+ game._clicked_button.state = 0;
+ _draw(game);
+ if (game._clicked_button.callback) {
+ game._clicked_button.callback();
+ }
+ game._clicked_button = null;
+ return;
+ }
+ }
+
+ /* ---- Mouse ---- */
+
+ function _handle_mousedown(game, e) {
+ if (!game.playing) return;
+
+ game.touchmode = false;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (e.button === 0 && game.events.mousedown) {
+ game.events.mousedown(game, e, x, y);
+ }
+ }
+
+ function _handle_mouseup(game, e) {
+ if (!game.playing && game.ready_to_go) {
+ /* Click to start */
+ if (game.hasOwnProperty('save_key') && !game._show_save_error) {
+ /* Test if save/load is working */
+ try {
+ console.log("Test saving");
+ localStorage.setItem(game.save_key + ".test", "savetest");
+ let savetest = localStorage.getItem(game.save_key + ".test");
+ if (savetest !== "savetest") {
+ throw new Error("oops");
+ }
+ } catch (e) {
+ /* If error saving, show save error message first */
+ /* We set _show_save_error so that the second time the user clicks,
+ * the game will actually start */
+ console.log("- SAVE FAILED -");
+ game._show_save_error = true;
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(game.img._saveerror, 0, 0);
+ game.ctx.global.restore();
+ return;
+ }
+ }
+ game.play();
+ return;
+ }
+
+ game.touchmode = false;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+
+ _handle_clicked_button(game);
+
+ if (e.button === 0 && game.events.mouseup) {
+ game.events.mouseup(game, e, x, y);
+ }
+ }
+
+ function _handle_mousemove(game, e) {
+ game.touchmode = false;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ }
+
+ /* ---- Touch (defaults to same as mouse) ---- */
+
+ let _last_touch_event;
+
+ function _handle_touchstart(game, e) {
+ if (!game.playing) return;
+
+ game.touchmode = true;
+
+ if (e.touches) e = e.touches[0];
+
+ _last_touch_event = e;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (game.events.touchstart) {
+ game.events.touchstart(game, e, x, y);
+ } else if (game.events.mousedown) {
+ if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ game.events.mousedown(game, e, x, y);
+ }
+ }
+
+ function _handle_touchend(game, e) {
+ if (!game.playing && game.ready_to_go) {
+ /* Click to start */
+ game.play();
+ return;
+ }
+
+ game.touchmode = true;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((_last_touch_event.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((_last_touch_event.clientY - rect.top) / rect.height * game.screen_h) - 1;
+
+ _handle_clicked_button(game);
+
+ if (game.events.touchend) {
+ game.events.touchend(game, e, x, y);
+ } else if (game.events.mouseup) {
+ if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ game.events.mouseup(game, e, x, y);
+ }
+ }
+
+ function _handle_touchmove(game, e) {
+ if (!game.playing) return;
+
+ game.touchmode = true;
+
+ if (e.touches) e = e.touches[0];
+
+ _last_touch_event = e;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (game.events.touchmove) {
+ game.events.touchmove(game, e, x, y);
+ } else if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ }
+
+ /* ---- Drawing stuff ---- */
+
+ function _draw(game) {
+ game.ctx.draw.save();
+
+ game.ctx.draw.fillStyle = game.background_color;
+
+ game.ctx.draw.beginPath();
+ game.ctx.draw.rect(0, 0, game.screen_w, game.screen_h);
+ game.ctx.draw.fill();
+
+ if (game.draw_func) {
+ game.draw_func(game.ctx.draw);
+ }
+
+ if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw) {
+ game.modes[game.mode].draw(game.ctx.draw);
+ }
+
+ if (game.transition.mid_long) {
+ game.ctx.draw.fillStyle = game.transition.color;
+ game.ctx.draw.fillRect(-1, -1, game.canvas_w + 5, game.canvas_h + 5);
+ }
+
+ game.ctx.draw.restore();
+
+ if (game.transition.is_transitioning) {
+ _draw_transition(game, game.ctx.mask);
+ } else {
+ game.ctx.mask.drawImage(game.ctx.draw.canvas, 0, 0);
+ }
+
+ if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw_after_transition) {
+ game.modes[game.mode].draw_after_transition(game.ctx.mask);
+ }
+
+
+ game.ctx.global.fillStyle = 'rgb(0, 0, 0)';
+ game.ctx.global.beginPath();
+ game.ctx.global.rect(0, 0, game.screen_w * game.draw_scale, game.screen_h * game.draw_scale);
+ game.ctx.global.fill();
+
+ game.ctx.global.save();
+
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+
+ game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0);
+
+ if (game._norun) {
+ game.ctx.global.drawImage(game.img._pause, 0, 0);
+ }
+
+ game.ctx.global.restore();
+ }
+
+ /* Draw a particular section of an image/spritesheet,
+ * without having to do as much math or type the destination size */
+ function sprite_draw(ctx, img, section_w, section_h, section_x, section_y, dest_x, dest_y) {
+ ctx.drawImage(img,
+ section_w * section_x, section_h * section_y, section_w, section_h,
+ dest_x, dest_y, section_w, section_h)
+ }
+
+ /* Draw an image over the whole screen lol */
+ function screen_draw(ctx, img) {
+ ctx.drawImage(img, 0, 0);
+ }
+
+ /* Draw a set of buttons. */
+ function button_draw(ctx, button_data) {
+ for (let button of button_data.buttons) {
+ if (!button.state) button.state = 0;
+ let button_state = button.state;
+ if (button.disabled) button_state = 3;
+ sprite_draw(ctx, button_data.img, button_data.button_w, button_data.button_h, button.id, button_state, button.x, button.y);
+ }
+ }
+
+ /* ---- Save ---- */
+
+ function _save(game, key, data) {
+ let save_data;
+
+ if (!game.hasOwnProperty('save_key')) {
+ throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object");
+ }
+
+ try {
+ save_data = localStorage.getItem(game.save_key) || "{}";
+ } catch (e) {
+ console.error("Saving is disabled; cannot save " + key, e);
+ return;
+ }
+
+ try {
+ save_data = JSON.parse(save_data);
+ } catch (e) {
+ console.error("Cannot parse stored save data (when trying to save " + key + ")", e);
+ return;
+ }
+
+ save_data[key] = JSON.stringify(data);
+
+ try {
+ save_data = JSON.stringify(save_data);
+ } catch (e) {
+ console.error("Failed to stringify save data (while trying to save " + key + ")", e);
+ return;
+ }
+
+ try {
+ localStorage.setItem(game.save_key, save_data);
+ } catch (e) {
+ console.error("Failed to save to localStorage (while saving key " + key + ")", e);
+ return;
+ }
+ }
+
+ function _load(game, key) {
+ let save_data;
+
+ if (!game.hasOwnProperty('save_key')) {
+ throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object");
+ }
+
+ try {
+ save_data = localStorage.getItem(game.save_key);
+ } catch (e) {
+ console.error("Loading is disabled; cannot load " + key, e);
+ return;
+ }
+
+ try {
+ save_data = JSON.parse(save_data);
+ } catch (e) {
+ console.error("Cannot parse stored save data (when trying to load " + key + ")", e);
+ return;
+ }
+
+ if (!save_data) {
+ return null;
+ }
+
+ return save_data[key];
+ }
+
+ /* ---- Transition stuff ---- */
+
+ let _transition = {
+ is_transitioning: false,
+ timer: 0,
+ color: '#000000',
+ w: 20,
+ h: 14,
+ dir_invert_v: false,
+ dir_invert_h: false,
+ invert_shape: true,
+ mid_long: false,
+ done_func: null,
+ type: _draw_fade_transition,
+ nodraw: false,
+ end_time: 100,
+ }
+
+ function _long_transition(game, type, length, callback) {
+ if (game.transition.is_transitioning) return;
+
+ _draw(game);
+
+ game.transition.invert_shape = false;
+ _internal_start_transition(game, type, length, function() {
+ game.transition.mid_long = true;
+ }, function() {
+ game.transition.invert_shape = true;
+ game.transition.is_transitioning = true;
+ let tdiv = game.transition.dir_invert_v;
+ let tdih = game.transition.dir_invert_h;
+ _internal_start_transition(game, type, length, function() {
+ game.transition.mid_long = false;
+ callback();
+ game.transition.dir_invert_v = tdiv;
+ game.transition.dir_invert_h = tdih;
+ });
+ });
+ }
+
+ function _start_transition(game, type, length, callback, on_done) {
+ if (game.transition.is_transitioning) return;
+ if (!game.transition.nodraw) _draw(game);
+
+ _internal_start_transition(game, type, length, callback, on_done);
+ }
+
+ function _internal_start_transition(game, type, length, callback, on_done) {
+ if (on_done) {
+ game.transition.done_func = on_done;
+ }
+
+ game.transition.timer = 0;
+ game.transition.type = type;
+ game.transition.end_time = length;
+
+ game.ctx.copy.drawImage(game.ctx.draw.canvas, 0, 0);
+
+ callback();
+
+ game.transition.is_transitioning = true;
+ }
+
+ function _finish_transition(game) {
+ game.transition.is_transitioning = false;
+ game.transition.timer = 0;
+
+ if (game.transition.done_func) {
+ setTimeout(function() {
+ game.transition.done_func();
+ game.transition.done_func = null;
+ }, 400);
+ }
+ }
+
+ function _draw_transition(game, ctx) {
+ let frac = game.transition.timer / game.transition.end_time;
+
+ ctx.save();
+ ctx.clearRect(0, 0, game.screen_w, game.screen_h);
+ game.transition.type(game, ctx, game.ctx.copy, game.ctx.draw, frac, game.transition.invert_shape);
+ ctx.restore();
+ }
+
+ function _draw_slide_down_transition(game, ctx, oldc, newc, frac, reverse) {
+ let offset = frac * game.screen_h;
+
+ ctx.drawImage(oldc.canvas, 0, -offset);
+ ctx.drawImage(newc.canvas, 0, game.screen_h - offset);
+ }
+
+ function _draw_slide_up_transition(game, ctx, oldc, newc, frac, reverse) {
+ let offset = frac * game.screen_h;
+
+ ctx.drawImage(oldc.canvas, 0, offset);
+ ctx.drawImage(newc.canvas, 0, - game.screen_h + offset);
+ }
+
+ function _draw_fade_transition(game, ctx, oldc, newc, frac, reverse) {
+ ctx.globalAlpha = 1;
+ ctx.drawImage(oldc.canvas, 0, 0);
+ ctx.globalAlpha = frac;
+ ctx.drawImage(newc.canvas, 0, 0);
+ }
+
+ /* ---- former contents of ready.js ---- */
+
+ // Due to Timo Huovinen
+ // https://stackoverflow.com/questions/799981/document-ready-equivalent-without-jquery
+ var ready = (function(){
+
+ var readyList,
+ DOMContentLoaded,
+ class2type = {};
+ class2type["[object Boolean]"] = "boolean";
+ class2type["[object Number]"] = "number";
+ class2type["[object String]"] = "string";
+ class2type["[object Function]"] = "function";
+ class2type["[object Array]"] = "array";
+ class2type["[object Date]"] = "date";
+ class2type["[object RegExp]"] = "regexp";
+ class2type["[object Object]"] = "object";
+
+ var ReadyObj = {
+ // Is the DOM ready to be used? Set to true once it occurs.
+ isReady: false,
+ // A counter to track how many items to wait for before
+ // the ready event fires. See #6781
+ readyWait: 1,
+ // Hold (or release) the ready event
+ holdReady: function( hold ) {
+ if ( hold ) {
+ ReadyObj.readyWait++;
+ } else {
+ ReadyObj.ready( true );
+ }
+ },
+ // Handle when the DOM is ready
+ ready: function( wait ) {
+ // Either a released hold or an DOMready/load event and not yet ready
+ if ( (wait === true && !--ReadyObj.readyWait) || (wait !== true && !ReadyObj.isReady) ) {
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( !document.body ) {
+ return setTimeout( ReadyObj.ready, 1 );
+ }
+
+ // Remember that the DOM is ready
+ ReadyObj.isReady = true;
+ // If a normal DOM Ready event fired, decrement, and wait if need be
+ if ( wait !== true && --ReadyObj.readyWait > 0 ) {
+ return;
+ }
+ // If there are functions bound, to execute
+ readyList.resolveWith( document, [ ReadyObj ] );
+
+ // Trigger any bound ready events
+ //if ( ReadyObj.fn.trigger ) {
+ // ReadyObj( document ).trigger( "ready" ).unbind( "ready" );
+ //}
+ }
+ },
+ bindReady: function() {
+ if ( readyList ) {
+ return;
+ }
+ readyList = ReadyObj._Deferred();
+
+ // Catch cases where $(document).ready() is called after the
+ // browser event has already occurred.
+ if ( document.readyState === "complete" ) {
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
+ return setTimeout( ReadyObj.ready, 1 );
+ }
+
+ // Mozilla, Opera and webkit nightlies currently support this event
+ if ( document.addEventListener ) {
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+ // A fallback to window.onload, that will always work
+ window.addEventListener( "load", ReadyObj.ready, false );
+
+ // If IE event model is used
+ } else if ( document.attachEvent ) {
+ // ensure firing before onload,
+ // maybe late but safe also for iframes
+ document.attachEvent( "onreadystatechange", DOMContentLoaded );
+
+ // A fallback to window.onload, that will always work
+ window.attachEvent( "onload", ReadyObj.ready );
+
+ // If IE and not a frame
+ // continually check to see if the document is ready
+ var toplevel = false;
+
+ try {
+ toplevel = window.frameElement == null;
+ } catch(e) {}
+
+ if ( document.documentElement.doScroll && toplevel ) {
+ doScrollCheck();
+ }
+ }
+ },
+ _Deferred: function() {
+ var // callbacks list
+ callbacks = [],
+ // stored [ context , args ]
+ fired,
+ // to avoid firing when already doing so
+ firing,
+ // flag to know if the deferred has been cancelled
+ cancelled,
+ // the deferred itself
+ deferred = {
+
+ // done( f1, f2, ...)
+ done: function() {
+ if ( !cancelled ) {
+ var args = arguments,
+ i,
+ length,
+ elem,
+ type,
+ _fired;
+ if ( fired ) {
+ _fired = fired;
+ fired = 0;
+ }
+ for ( i = 0, length = args.length; i < length; i++ ) {
+ elem = args[ i ];
+ type = ReadyObj.type( elem );
+ if ( type === "array" ) {
+ deferred.done.apply( deferred, elem );
+ } else if ( type === "function" ) {
+ callbacks.push( elem );
+ }
+ }
+ if ( _fired ) {
+ deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
+ }
+ }
+ return this;
+ },
+
+ // resolve with given context and args
+ resolveWith: function( context, args ) {
+ if ( !cancelled && !fired && !firing ) {
+ // make sure args are available (#8421)
+ args = args || [];
+ firing = 1;
+ try {
+ while( callbacks[ 0 ] ) {
+ callbacks.shift().apply( context, args );//shifts a callback, and applies it to document
+ }
+ }
+ finally {
+ fired = [ context, args ];
+ firing = 0;
+ }
+ }
+ return this;
+ },
+
+ // resolve with this as context and given arguments
+ resolve: function() {
+ deferred.resolveWith( this, arguments );
+ return this;
+ },
+
+ // Has this deferred been resolved?
+ isResolved: function() {
+ return !!( firing || fired );
+ },
+
+ // Cancel
+ cancel: function() {
+ cancelled = 1;
+ callbacks = [];
+ return this;
+ }
+ };
+
+ return deferred;
+ },
+ type: function( obj ) {
+ return obj == null ?
+ String( obj ) :
+ class2type[ Object.prototype.toString.call(obj) ] || "object";
+ }
+ }
+ // The DOM ready check for Internet Explorer
+ function doScrollCheck() {
+ if ( ReadyObj.isReady ) {
+ return;
+ }
+
+ try {
+ // If IE is used, use the trick by Diego Perini
+ // http://javascript.nwbox.com/IEContentLoaded/
+ document.documentElement.doScroll("left");
+ } catch(e) {
+ setTimeout( doScrollCheck, 1 );
+ return;
+ }
+
+ // and execute any waiting functions
+ ReadyObj.ready();
+ }
+ // Cleanup functions for the document ready method
+ if ( document.addEventListener ) {
+ DOMContentLoaded = function() {
+ document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+ ReadyObj.ready();
+ };
+
+ } else if ( document.attachEvent ) {
+ DOMContentLoaded = function() {
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( document.readyState === "complete" ) {
+ document.detachEvent( "onreadystatechange", DOMContentLoaded );
+ ReadyObj.ready();
+ }
+ };
+ }
+ function ready( fn ) {
+ // Attach the listeners
+ ReadyObj.bindReady();
+
+ var type = ReadyObj.type( fn );
+
+ // Add the callback
+ readyList.done( fn );//readyList is result of _Deferred()
+ }
+ return ready;
+ })();
+
+ return {
+ create_game: create_game,
+
+ ready: ready,
+ mod: mod,
+ sgn: sgn,
+ as_hex: as_hex,
+ copy_list: copy_list,
+ copy_flat_objlist: copy_flat_objlist,
+
+ sprite_draw: sprite_draw,
+ screen_draw: screen_draw,
+
+ buttons: {
+ create: create_buttons,
+ update: update_buttons,
+ click: click_button,
+ draw: button_draw,
+ },
+
+ transition: {
+ type: {
+ fade: _draw_fade_transition,
+ slide_up: _draw_slide_up_transition,
+ slide_down: _draw_slide_down_transition,
+ },
+ },
+ }
+})();
--- /dev/null
+FontStruct Non-Commercial Software End User License Agreement 1.0
+
+This software end user license agreement (hereinafter “Agreement”) is made
+between you or, if you represent a legal entity, that legal entity (hereinafter
+“You”) and “broomweed”, the copyright holder and designer (hereinafter
+“Font Designer”) of the font software named “sharkfont” (hereinafter
+“Font Software”).
+
+1. BINDING AGREEMENT
+This Agreement is a binding agreement between You and the Font Designer and
+shall set forth the terms and conditions under which You can use the Font
+Software. For the purposes of this Agreement, Font Software shall mean the Font
+Software, and any parts or copies of it.
+
+2. GRANT OF NON-EXCLUSIVE AND LICENCE
+The Font Designer hereby grants You a non-exclusive, non-transferable licence to
+use the Font Software provided that You comply with all terms and conditions of
+this Agreement.
+
+2.1 PERSONAL USE
+You may use the Fonts Software only for Your own Personal Purposes and for Your
+Work. For the purposes of this Agreement, “Personal Purposes” shall mean any
+private use of the Font Software by you. “Work” shall mean any work or
+result created by You or on Your behalf with the Font Software. For the
+avoidance of doubt, the use for Personal Purposes does not allow any use of the
+Font Software and/or any Work for commercial purposes (see Section 2.2) and/or
+as a web-font (see Section 2.6).
+
+2.2 NO COMMERCIAL USE
+Neither the Font Software, nor any of its individual components or any Work, may
+be used by You for any form of profit-making or otherwise commercial projects,
+without express prior written permission from the Font Designer.
+
+2.3 NO SALE OR DISTRIBUTION
+You may not sell, rent, license, sublicense, distribute, redistribute, give-away
+or make available (in any other way) the Font Software alone or as part of any
+collection, product or service to any third party.
+
+2.4 NO MODIFICATION
+You may not modify, adapt, rename, translate, reverse engineer, decompile,
+disassemble, alter, or attempt to discover the source code of the Font Software.
+
+2.5 EMBEDDING
+You may embed the Font Software in documents, applications or devices either as
+a rasterized representation of the Font Software (e.g., a GIF or JPEG) or as a
+subset of the Font Software as long as the document, application or device is
+distributed in a secure format that permits only the viewing and printing but
+not the editing of the text.
+
+2.6 NO USE AS A WEBFONT
+You may not make the Font Software accessible on a web server in order to enable
+a web browser to render the content of Websites using the respective Font
+Software.
+
+2.7 BACKUP
+You may make backup copies of the Font Software for archival purposes only,
+provided that You retain exclusive custody and control over such copies. Any
+backup copy of the Font Software must contain the same copyright, trademark, and
+other proprietary information as the original.
+
+2.8 PRINT SHOPS AND SERVICE BUREAUS
+You may provide a copy of the Font Software used in Your Work to a print shop,
+service bureau, etc. for printing or otherwise outputting Your Work. Afterwards,
+the print shop, service bureau, etc. must destroy and/or delete all copies of
+the Font Software.
+
+3. ATTRIBUTION
+If You make Your Work publicly accessible in any form, You must keep intact all
+copyright notices for the Font Software and display, reasonable to the medium or
+means You are utilizing for Your Work, an attribution notice (hereinafter “The
+Attribution Notice”) comprising of:
+
+the name of the Font Designer or pseudonym, if applicable (“broomweed”);
+the title of the Font Software (“sharkfont”); and
+to the extent reasonably practicable, the URI
+(https://fontstruct.com/fontstructions/show/2657823) from where the Font was
+originally downloaded.
+
+The Attribution Notice may be implemented in any reasonable manner. For the
+avoidance of doubt, You may only use the credit required by this Section for the
+purpose of attribution in the manner set out above and, by exercising Your
+rights under this License, You may not implicitly or explicitly assert or imply
+any connection with, sponsorship or endorsement by the Font Designer of You or
+Your Work.
+
+4. YOUR REPRESENTATIONS AND INDEMNIFICATIONS
+You represent, warrant, and covenant that You shall use the Font Software only
+in accordance with the terms of this Agreement and keep the Font Designer fully
+indemnified against all actions, claims, costs, proceedings and damages
+whatsoever incurred by any breach of any of the aforesaid representations,
+warranties of this Agreement.
+
+5. OWNERSHIP AND RIGHTS TO THE FONT SOFTWARE
+The Font Designer reserves all rights not expressly granted to You in this
+Agreement (hereinafter “Reserved Rights”). The Font Software is protected by
+copyright and other intellectual property laws and treaties. You acknowledge
+that the Font Designer shall be the sole owner of the Font Software, of the
+copyright and all Reserved Rights of whatever kind and nature in the Font
+Software.
+
+6. WARRANTY
+The Font Software is made available “as is” WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT.THE FONT DESIGNER DOES NOT AND
+CANNOT GUARANTEE THE PERFORMANCE OR RESULTS YOU MAY OBTAIN BY USING THE FONT
+SOFTWARE.
+THE FONT DESIGNER SHALL IN NO EVENT BE LIABLE TO YOU FOR ANY CONSEQUENTIAL,
+INCIDENTAL OR SPECIAL DAMAGES, INCLUDING ANY LOST PROFITS OR LOST SAVINGS, OR
+FOR ANY CLAIM BY ANY THIRD PARTY, ARISING OUT OF THE USE OR INABILITY TO USE THE
+FONT SOFTWARE.
+
+7. TERMINATION / CONDITION PRECEDENT
+You may use the Font Software only as set forth in this Agreement. Any use of
+the Font Software that is not expressly allowed in this Agreement will
+automatically terminate Your rights to use the Font Software under this
+Agreement. If Your right to use the Font Software is terminated You shall
+immediately stop using the Font Software, call back or change any Work that is
+used in the public, and delete/destroy any copies of the Font Software. The
+termination of the Agreement shall not affect the right of the Font Designer to
+claim damages or other rights.
+
+8. WAIVER
+A waiver by either Party of any term or condition of this Agreement shall not be
+deemed or construed to be a waiver of such term or condition for the future or
+any subsequent breach thereof. All remedies, rights, undertakings, obligations
+and agreements contained in this Agreement shall be cumulative and none of them
+shall be in limitation of any other remedy, right, undertaking, obligation or
+agreement of either Party.
+
+9. GOVERNING LAW, PLACE OF JURISDICTION
+This Agreement shall be construed and shall take effect in accordance with the
+laws of the Federal Republic of Germany excluding the United Nations Convention
+on Contracts for the International Sales of Goods (CISG). To the extent
+permitted by law, the Courts of Berlin, Germany shall have exclusive
+jurisdiction to resolve any dispute which may arise.
+
+10. ENTIRE AGREEMENT
+This Agreement contains the entire agreement between the Parties. No variation
+of any of the terms or conditions of this Agreement may be made unless such
+variation is agreed in writing and signed by You and the Font Designer.
+
+11. SEVERABILITY
+If any provision of this Agreement is adjudged by a court to be invalid or
+unenforceable, such provision shall in no way affect any other provision of this
+Agreement, the application of such provision in any other circumstance or the
+validity or enforceability of this Agreement. The Parties will negotiate in
+good faith about a provision that will replace the invalid or unenforceable
+provision.
+
+FontStruct Notice: FontStruct no Party to this Agreement
+
+Please note that FontStruct (www.fontstruct.com) is not a party to this
+Agreement between You and the Font Designer.
+
+FontStruct makes no warranty of any kind in connection with the Font Software,
+its use or the results that You may obtain by using the Font Software.
+
+FontStruct shall in no event be liable to You or any third party for any damages
+whatsoever arising out of or relating to this Agreement or the use of the Font
+Software, including but not limited to incidental, special damages, any lost
+profits or lost savings or any claim made by a third party.
+
--- /dev/null
+The font file in this archive was created using Fontstruct the free, online
+font-building tool.\r
+This font was created by “broomweed”.\r
+This font has a homepage where this archive and other versions may be found:
+https://fontstruct.com/fontstructions/show/2657823\r
+[ancestry]\r
+Try Fontstruct at https://fontstruct.com\r
+It’s easy and it’s fun.\r
+\r
+Fontstruct is copyright ©2025 Rob Meek\r
+\r
+LEGAL NOTICE:\r
+In using this font you must comply with the licensing terms described in the
+file “license.txt” included with this archive.\r
+If you redistribute the font file in this archive, it must be accompanied by all
+the other files from this archive, including this one.\r
--- /dev/null
+"use strict";
+
+/* Please forgive my strictly procedural/functional style here.
+ * Thanks for reading though! */
+
+let zb = (function() {
+ /* ---- Util ---- */
+
+ function mod(x, n) {
+ return ((x%n)+n)%n;
+ }
+
+ function sgn(x) {
+ if (x === 0) {
+ return 0;
+ } else {
+ return x / Math.abs(x);
+ }
+ }
+
+ function as_hex(num, length) {
+ let result = Math.round(num).toString(16);
+
+ if (length) {
+ while (result.length < length) {
+ result = "0" + result;
+ }
+ }
+
+ return result;
+ }
+
+ function copy_list(list) {
+ let newlist = [];
+ for (let x of list) {
+ newlist.push(x);
+ }
+ return newlist;
+ }
+
+ function copy_flat_objlist(list) {
+ let newlist = [];
+ for (let x of list) {
+ newlist.push({ ...x });
+ }
+ return newlist;
+ }
+
+ /* ---- Resource loading / loading screen ---- */
+
+ let _audiocheck = document.createElement('audio');
+
+ let _SFX_ARRAY_SIZE = 10;
+
+ /* Returns a callback that should be called when a resource finishes loading. */
+ function _register_resource(name, game, callback) {
+ game._total_things_to_load ++;
+ console.log("Loading", name + ". Things to load:", game._total_things_to_load);
+ return function() {
+ if (!game.ready_to_go) {
+ game._things_loaded ++;
+ console.log("Loaded", name + ". Things loaded:", game._things_loaded, "/", game._total_things_to_load);
+ if (game.load_with_progress_bar) {
+ if (game._progressbar_img_loaded) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(
+ game.img._progressbar,
+ 0,
+ 0,
+ Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width),
+ game.img._progressbar.height,
+ Math.round(game.canvas_w / 2 / game.draw_scale - game.img._progressbar.width / 2),
+ Math.round(game.canvas_h / 2 / game.draw_scale - game.img._progressbar.height / 2),
+ Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width),
+ game.img._progressbar.height,
+ );
+ game.ctx.global.restore();
+ }
+ }
+ _check_if_loaded(game);
+ if (callback) {
+ callback();
+ }
+ }
+ }
+ }
+
+ /* Check if audio failed to load bc file doesn't exist and print error message. */
+ /* TODO add the same thing for an image */
+ function _sound_resource_error(name, game, audio) {
+ return function(e) {
+ if (audio === undefined) {
+ console.error("Error loading resource, skipping:", name);
+ game._things_loaded ++;
+ _check_if_loaded(game);
+ }
+ }
+ }
+
+ /* Check if the game has loaded everything and if so show the 'click to start' image */
+ function _check_if_loaded(game) {
+ if (game.ready_to_go) return;
+
+ if (game._things_loaded >= game._total_things_to_load) {
+ console.log("Ready");
+ game.ready_to_go = true;
+ game._on_ready();
+ }
+ }
+
+ function _register_sfx(sfxdata, game) {
+ for (let key in sfxdata) {
+ let sfx_size = sfxdata[key].copies || _SFX_ARRAY_SIZE;
+ let sfx_array = new Array(sfx_size);
+ for (let i = 0; i < sfx_size; i++) {
+ sfx_array[i] = new Audio(sfxdata[key].path);
+ sfx_array[i].addEventListener('canplaythrough',
+ _register_resource(sfxdata[key].path + '#' + (i+1), game), false);
+ sfx_array[i].addEventListener('error',
+ _sound_resource_error(sfxdata[key].path + '#' + (i+1), game), false, sfx_array[i]);
+ if (sfxdata[key].hasOwnProperty('volume')) {
+ sfx_array[i].volume = sfxdata[key].volume;
+ }
+ }
+ game.sfx[key] = {
+ _array: sfx_array,
+ _index: 0,
+ play: function() {
+ if (!game.muted) {
+ this._array[this._index].currentTime = 0;
+ this._array[this._index].play();
+ this._index ++;
+ this._index = mod(this._index, _SFX_ARRAY_SIZE);
+ }
+ }
+ }
+ }
+ }
+
+ function _register_music(musicdata, game) {
+ for (let key in musicdata) {
+ let musicpath;
+ if (musicdata[key].path.match(/\.[a-z]{3}$/)) {
+ musicpath = musicdata[key].path;
+ } else if (_audiocheck.canPlayType('audio/mpeg')) {
+ musicpath = musicdata[key].path + '.mp3';
+ } else if (_audiocheck.canPlayType('audio/ogg')) {
+ musicpath = musicdata[key].path + '.ogg';
+ } else {
+ console.log("No supported music type known :(");
+ return;
+ }
+ let music = new Audio(musicpath);
+ if (musicdata[key].hasOwnProperty('volume')) {
+ music.volume = musicdata[key].volume;
+ }
+
+ if (!musicdata[key].hasOwnProperty('loop') || musicdata[key].loop) {
+ /* We loop by default unless 'loop: false' is specified. */
+ music.loop = true;
+ }
+ music.addEventListener('canplaythrough', _register_resource(musicpath, game), false);
+ music.addEventListener('error', _sound_resource_error(musicpath, game, music), false);
+
+ /* Handle music changes when muted, so when we unmute,
+ * the correct music will be playing. */
+ music._og_play = music.play;
+ music.play = function() {
+ if (game.muted) {
+ music.was_playing = true;
+ } else {
+ music._og_play();
+ }
+ }
+
+ music._og_pause = music.pause;
+ music.pause = function() {
+ if (game.muted) {
+ music.was_playing = false;
+ } else {
+ music._og_pause();
+ }
+ }
+
+ game.music[key] = music;
+ }
+ }
+
+ function _register_images(imgdata, game) {
+ function _recursive_load_images(pathmap) {
+ let result = {};
+ for (let key in pathmap) {
+ if (typeof pathmap[key] === 'object') {
+ result[key] = _recursive_load_images(pathmap[key]);
+ } else {
+ result[key] = new Image();
+ result[key].onload = _register_resource(pathmap[key], game);
+ result[key].src = pathmap[key];
+ }
+ }
+ return result;
+ }
+
+ let loaded_imgs = _recursive_load_images(imgdata);
+ for (let k in loaded_imgs) {
+ game.img[k] = loaded_imgs[k];
+ }
+ }
+
+ function _create_canvas_context(canvas) {
+ let new_canvas = document.createElement('canvas');
+ new_canvas.width = canvas.width;
+ new_canvas.height = canvas.height;
+
+ let new_ctx = new_canvas.getContext('2d');
+ new_ctx.imageSmoothingEnabled = false;
+ new_ctx.webkitImageSmoothingEnabled = false;
+ new_ctx.mozImageSmoothingEnabled = false;
+
+ return new_ctx;
+ }
+
+ function create_game(params) {
+ let game_props = {...params};
+
+ game_props.frame_rate = params.frame_rate || 60;
+
+ game_props.canvas_w = params.canvas_w || 640;
+ game_props.canvas_h = params.canvas_h || 480;
+ game_props.draw_scale = params.draw_scale || 4;
+ game_props.top_border = params.top_border || 0;
+ game_props.bottom_border = params.bottom_border || 8;
+ game_props.left_border = params.left_border || 0;
+ game_props.right_border = params.right_border || 0;
+
+ game_props.tile_size = params.tile_size || 16;
+ game_props.level_w = params.level_w || 10;
+ game_props.level_h = params.level_h || 7;
+
+ game_props.screen_w = game_props.canvas_w / game_props.draw_scale;
+ game_props.screen_h = game_props.canvas_h / game_props.draw_scale;
+
+ game_props.background_color = params.background_color || '#000000';
+
+ let canvas = document.getElementById(game_props.canvas);
+
+ let global_ctx = canvas.getContext('2d');
+ global_ctx.imageSmoothingEnabled = false;
+ global_ctx.webkitImageSmoothingEnabled = false;
+ global_ctx.mozImageSmoothingEnabled = false;
+
+ let mask_canvas = document.createElement('canvas');
+ mask_canvas.width = canvas.width;
+ mask_canvas.height = canvas.height;
+ let mask_ctx = mask_canvas.getContext('2d');
+ mask_ctx.imageSmoothingEnabled = false;
+ mask_ctx.webkitImageSmoothingEnabled = false;
+ mask_ctx.mozImageSmoothingEnabled = false;
+
+ let copy_canvas = document.createElement('canvas');
+ copy_canvas.width = canvas.width;
+ copy_canvas.height = canvas.height;
+ let copy_ctx = copy_canvas.getContext('2d');
+ copy_ctx.imageSmoothingEnabled = false;
+ copy_ctx.webkitImageSmoothingEnabled = false;
+ copy_ctx.mozImageSmoothingEnabled = false;
+
+ let draw_canvas = document.createElement('canvas');
+ draw_canvas.width = canvas.width;
+ draw_canvas.height = canvas.height;
+ let draw_ctx = draw_canvas.getContext('2d');
+ draw_ctx.imageSmoothingEnabled = false;
+ draw_ctx.webkitImageSmoothingEnabled = false;
+ draw_ctx.mozImageSmoothingEnabled = false;
+
+ let game = {
+ /* General properties */
+ ...game_props,
+
+ canvas: canvas,
+
+ /* Loading */
+ ready_to_go: false,
+ _total_things_to_load: 1,
+ _things_loaded: 0,
+ resources_ready: function() {
+ this._things_loaded ++;
+ console.log("Finished enumerating resources to load. Things loaded:",
+ this._things_loaded, "/", this._total_things_to_load);
+ _check_if_loaded(this);
+ },
+ _on_ready: function() {
+ this.ready_to_go = true;
+ if (game.img._clicktostart && game.img._clicktostart.complete) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(game.img._clicktostart, 0, 0);
+ game.ctx.global.restore();
+ }
+ },
+ playing: false,
+ play: function() {
+ this.playing = true;
+ if (this.events.gamestart) {
+ this.events.gamestart(this);
+ }
+ _loop(this);
+ },
+ _norun: false,
+
+ /* Audio */
+ sfx: {},
+ music: {},
+ muted: false,
+ mute: function() {
+ _mute(this);
+ },
+ unmute: function() {
+ _unmute(this);
+ },
+ toggle_mute: function() {
+ if (this.muted) {
+ _unmute(this);
+ } else {
+ _mute(this);
+ }
+ },
+ register_sfx: function(sfxdata) {
+ _register_sfx(sfxdata, this);
+ },
+ register_music: function(musicdata) {
+ _register_music(musicdata, this);
+ },
+
+ /* Drawing */
+ ctx: {
+ global: global_ctx, /* context for the actual real canvas */
+ mask: mask_ctx, /* context for drawing the transition mask, gets scaled up */
+ copy: copy_ctx, /* context for copying the old screen on transition */
+ draw: draw_ctx, /* context for drawing the real level */
+ },
+ create_canvas_context: function() {
+ /* Function to return a fresh screen-sized canvas context, if we need it. */
+ return _create_canvas_context(this.canvas);
+ },
+ img: {},
+ register_images: function(imgdata) {
+ _register_images(imgdata, this);
+ },
+
+ /* Save */
+ save: function(key, data) {
+ _save(game, key, data);
+ },
+ load: function(key) {
+ return _load(game, key);
+ },
+
+ /* Transition system */
+ transition: _transition,
+ start_transition: function(type, length, callback, on_done) {
+ _start_transition(this, type, length, callback, on_done);
+ },
+ long_transition: function(type, length, callback, on_done) {
+ _long_transition(this, type, length, callback, on_done);
+ },
+
+ /* Input */
+ touchmode: false,
+
+ /* Mode */
+ change_mode: function(newmode) {
+ _change_mode(game, newmode);
+ }
+ };
+
+ window.requestAnimFrame = (function() {
+ return window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ function(callback, element) {
+ window.setTimeout(callback, 1000/game.frame_rate);
+ };
+ })();
+
+ /* Register event listeners */
+ for (let ev in params.events) {
+ /* Register any other events I guess */
+ canvas['on' + ev] = function(e) {
+ if (game.playing && !game._norun) {
+ params.events[ev](game, e);
+ }
+ }
+ }
+
+ /* Override with special events */
+ canvas.onmousedown = function(e) {
+ if (!game._norun) {
+ _handle_mousedown(game, e);
+ }
+ }
+
+ canvas.onmousemove = function(e) {
+ if (!game._norun) {
+ _handle_mousemove(game, e);
+ }
+ }
+
+ canvas.onmouseup = function(e) {
+ if (!game._norun) {
+ _handle_mouseup(game, e);
+ }
+ }
+
+ canvas.ontouchstart = function(e) {
+ if (!game._norun) {
+ game.touchmode = true;
+ _handle_touchstart(game, e);
+ }
+ e.preventDefault();
+ }
+
+ canvas.ontouchmove = function(e) {
+ if (!game._norun) {
+ game.touchmode = true;
+ _handle_touchmove(game, e);
+ }
+ e.preventDefault();
+ }
+
+ canvas.ontouchend = function(e) {
+ if (!game._norun) {
+ game.touchmode = true;
+ _handle_touchend(game, e);
+ }
+ e.preventDefault();
+ }
+
+ canvas.onkeydown = function(e) {
+ if (!game._norun && game.events.keydown) {
+ game.events.keydown(game, e);
+ e.preventDefault();
+ }
+ }
+
+ canvas.onkeyup = function(e) {
+ if (!game._norun && game.events.keyup) {
+ game.events.keyup(game, e);
+ e.preventDefault();
+ }
+ }
+
+ canvas.onblur = function(e) {
+ if (game.playing && !game.run_in_background) {
+ game._norun = true;
+ _stop_music(game);
+ }
+ }
+
+ canvas.onfocus = function(e) {
+ if (game.playing && !game.run_in_background) {
+ game._norun = false;
+ if (!game.muted) {
+ _start_music(game);
+ }
+ _loop(game);
+ }
+ }
+
+ /* Set loading stuff */
+ let loading_img = new Image();
+ loading_img.onload = _register_resource('loading.png', game, function() {
+ if (!game.ready_to_go) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(loading_img, 0, 0);
+ game.ctx.global.restore();
+ }
+ });
+ loading_img.src = 'loading.png';
+
+ if (game.load_with_progress_bar) {
+ game._progressbar_img_loaded = false;
+ game._progressbar_width = 0;
+ game._progressbar_height = 0;
+
+ let progressbar_img = new Image();
+ progressbar_img.onload = _register_resource('progressbar.png', game, function() {
+ game._progressbar_img_loaded = true;
+ game._progressbar_width = progressbar_img.width;
+ game._progressbar_height = progressbar_img.height;
+ if (!game.ready_to_go) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(loading_img, 0, 0);
+ game.ctx.global.restore();
+ }
+ });
+ progressbar_img.src = 'progressbar.png';
+ game.img._progressbar = progressbar_img;
+ }
+
+ if (!game.run_in_background) {
+ let pause_img = new Image();
+ pause_img.onload = _register_resource('pause.png', game);
+ pause_img.src = 'pause.png';
+ game.img._pause = pause_img;
+ }
+
+ if (game.hasOwnProperty('save_key')) {
+ let saveerror_img = new Image();
+ saveerror_img.onload = _register_resource('saveerror.png', game);
+ saveerror_img.src = 'saveerror.png';
+ game.img._saveerror = saveerror_img;
+ }
+
+ let clicktostart_img = new Image();
+ clicktostart_img.onload = _register_resource('clicktostart.png', game, function() {
+ if (game.ready_to_go) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(game.img._clicktostart, 0, 0);
+ game.ctx.global.restore();
+ }
+ });
+ clicktostart_img.src = 'clicktostart.png';
+ game.img._clicktostart = clicktostart_img;
+
+ return game;
+ }
+
+ /* ---- Audio ---- */
+
+ function _stop_music(game) {
+ if (game.muted) return;
+
+ for (let m in game.music) {
+ if (!game.music[m].paused) {
+ game.music[m].was_playing = true;
+ game.music[m].pause();
+ } else {
+ game.music[m].was_playing = false;
+ }
+ }
+ }
+
+ function _start_music(game, unmuting) {
+ if (game.muted && !unmuting) return;
+
+ for (let m in game.music) {
+ if (game.music[m].was_playing) {
+ game.music[m].play();
+ }
+ }
+ }
+
+ function _mute(game) {
+ _stop_music(game);
+ game.muted = true;
+ }
+
+ function _unmute(game) {
+ game.muted = false;
+ _start_music(game);
+ }
+
+ /* ---- Game update stuff ---- */
+
+ let _last_frame_time;
+ let _timedelta = 0;
+ function _loop(game, timestamp) {
+ if (timestamp == undefined) {
+ timestamp = 0;
+ _last_frame_time = timestamp;
+ }
+ _timedelta += timestamp - _last_frame_time;
+ _last_frame_time = timestamp;
+
+ while (_timedelta >= 1000 / game.frame_rate) {
+ _update(game, 1000 / game.frame_rate);
+ _timedelta -= 1000 / game.frame_rate;
+ }
+ _draw(game);
+
+ if (!game._norun) {
+ requestAnimFrame(function(timestamp) {
+ _loop(game, timestamp);
+ });
+ }
+ }
+
+ function _update(game, delta) {
+ if (game.update_func) {
+ game.update_func(delta);
+ }
+
+ if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].update) {
+ game.modes[game.mode].update(delta);
+ }
+
+ if (game.transition.is_transitioning) {
+ game.transition.timer += delta;
+ if (game.transition.timer > game.transition.end_time) {
+ _finish_transition(game);
+ }
+ }
+ }
+
+ function _change_mode(game, newmode) {
+ game.mode = newmode;
+ }
+
+ /* ---- UI ---- */
+
+ function create_buttons(button_data) {
+ for (let b of button_data.buttons) {
+ b.state = 0;
+ }
+
+ return button_data;
+ }
+
+ /* Call with a button set and given x / y mouse coordinates.
+ * Will update the button set in place.
+ * Should generally be called in mouse move and mouse up event handlers. */
+ function update_buttons(button_data, x, y) {
+ for (let button of button_data.buttons) {
+ if (button.state === 2) continue;
+ if (button.disabled) continue;
+
+ if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) {
+ if (!game.touchmode) {
+ button.state = 1;
+ }
+ } else if (button.state !== 2) {
+ button.state = 0;
+ }
+ }
+ }
+
+ /* Call with a button set and given x / y mouse coordinates.
+ * If a button is clicked, triggers its callback and returns true.
+ * Otherwise, returns false.
+ * Should be called in mouse down event handler. */
+ function click_button(button_data, x, y) {
+ for (let button of button_data.buttons) {
+ if (button.disabled) continue;
+
+ if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) {
+ button.state = 2;
+ game._clicked_button = button;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function _handle_clicked_button(game) {
+ if (game._clicked_button) {
+ game._clicked_button.state = 0;
+ _draw(game);
+ if (game._clicked_button.callback) {
+ game._clicked_button.callback();
+ }
+ game._clicked_button = null;
+ return;
+ }
+ }
+
+ /* ---- Mouse ---- */
+
+ function _handle_mousedown(game, e) {
+ if (!game.playing) return;
+
+ game.touchmode = false;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (e.button === 0 && game.events.mousedown) {
+ game.events.mousedown(game, e, x, y);
+ }
+ }
+
+ function _handle_mouseup(game, e) {
+ if (!game.playing && game.ready_to_go) {
+ /* Click to start */
+ if (game.hasOwnProperty('save_key') && !game._show_save_error) {
+ /* Test if save/load is working */
+ try {
+ console.log("Test saving");
+ localStorage.setItem(game.save_key + ".test", "savetest");
+ let savetest = localStorage.getItem(game.save_key + ".test");
+ if (savetest !== "savetest") {
+ throw new Error("oops");
+ }
+ } catch (e) {
+ /* If error saving, show save error message first */
+ /* We set _show_save_error so that the second time the user clicks,
+ * the game will actually start */
+ console.log("- SAVE FAILED -");
+ game._show_save_error = true;
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(game.img._saveerror, 0, 0);
+ game.ctx.global.restore();
+ return;
+ }
+ }
+ game.play();
+ return;
+ }
+
+ game.touchmode = false;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+
+ _handle_clicked_button(game);
+
+ if (e.button === 0 && game.events.mouseup) {
+ game.events.mouseup(game, e, x, y);
+ }
+ }
+
+ function _handle_mousemove(game, e) {
+ game.touchmode = false;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ }
+
+ /* ---- Touch (defaults to same as mouse) ---- */
+
+ let _last_touch_event;
+
+ function _handle_touchstart(game, e) {
+ if (!game.playing) return;
+
+ game.touchmode = true;
+
+ if (e.touches) e = e.touches[0];
+
+ _last_touch_event = e;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (game.events.touchstart) {
+ game.events.touchstart(game, e, x, y);
+ } else if (game.events.mousedown) {
+ if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ game.events.mousedown(game, e, x, y);
+ }
+ }
+
+ function _handle_touchend(game, e) {
+ if (!game.playing && game.ready_to_go) {
+ /* Click to start */
+ game.play();
+ return;
+ }
+
+ game.touchmode = true;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((_last_touch_event.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((_last_touch_event.clientY - rect.top) / rect.height * game.screen_h) - 1;
+
+ _handle_clicked_button(game);
+
+ if (game.events.touchend) {
+ game.events.touchend(game, e, x, y);
+ } else if (game.events.mouseup) {
+ if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ game.events.mouseup(game, e, x, y);
+ }
+ }
+
+ function _handle_touchmove(game, e) {
+ if (!game.playing) return;
+
+ game.touchmode = true;
+
+ if (e.touches) e = e.touches[0];
+
+ _last_touch_event = e;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (game.events.touchmove) {
+ game.events.touchmove(game, e, x, y);
+ } else if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ }
+
+ /* ---- Drawing stuff ---- */
+
+ function _draw(game) {
+ game.ctx.draw.save();
+
+ game.ctx.draw.fillStyle = game.background_color;
+
+ game.ctx.draw.beginPath();
+ game.ctx.draw.rect(0, 0, game.screen_w, game.screen_h);
+ game.ctx.draw.fill();
+
+ if (game.draw_func) {
+ game.draw_func(game.ctx.draw);
+ }
+
+ if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw) {
+ game.modes[game.mode].draw(game.ctx.draw);
+ }
+
+ if (game.transition.mid_long) {
+ game.ctx.draw.fillStyle = game.transition.color;
+ game.ctx.draw.fillRect(-1, -1, game.canvas_w + 5, game.canvas_h + 5);
+ }
+
+ game.ctx.draw.restore();
+
+ if (game.transition.is_transitioning) {
+ _draw_transition(game, game.ctx.mask);
+ } else {
+ game.ctx.mask.drawImage(game.ctx.draw.canvas, 0, 0);
+ }
+
+ if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw_after_transition) {
+ game.modes[game.mode].draw_after_transition(game.ctx.mask);
+ }
+
+
+ game.ctx.global.fillStyle = 'rgb(0, 0, 0)';
+ game.ctx.global.beginPath();
+ game.ctx.global.rect(0, 0, game.screen_w * game.draw_scale, game.screen_h * game.draw_scale);
+ game.ctx.global.fill();
+
+ game.ctx.global.save();
+
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+
+ game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0);
+
+ if (game._norun) {
+ game.ctx.global.drawImage(game.img._pause, 0, 0);
+ }
+
+ game.ctx.global.restore();
+ }
+
+ /* Draw a particular section of an image/spritesheet,
+ * without having to do as much math or type the destination size */
+ function sprite_draw(ctx, img, section_w, section_h, section_x, section_y, dest_x, dest_y) {
+ ctx.drawImage(img,
+ section_w * section_x, section_h * section_y, section_w, section_h,
+ dest_x, dest_y, section_w, section_h)
+ }
+
+ /* Draw an image over the whole screen lol */
+ function screen_draw(ctx, img) {
+ ctx.drawImage(img, 0, 0);
+ }
+
+ /* Draw a set of buttons. */
+ function button_draw(ctx, button_data) {
+ for (let button of button_data.buttons) {
+ if (!button.state) button.state = 0;
+ let button_state = button.state;
+ if (button.disabled) button_state = 3;
+ sprite_draw(ctx, button_data.img, button_data.button_w, button_data.button_h, button.id, button_state, button.x, button.y);
+ }
+ }
+
+ /* ---- Save ---- */
+
+ function _save(game, key, data) {
+ let save_data;
+
+ if (!game.hasOwnProperty('save_key')) {
+ throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object");
+ }
+
+ try {
+ save_data = localStorage.getItem(game.save_key) || "{}";
+ } catch (e) {
+ console.error("Saving is disabled; cannot save " + key, e);
+ return;
+ }
+
+ try {
+ save_data = JSON.parse(save_data);
+ } catch (e) {
+ console.error("Cannot parse stored save data (when trying to save " + key + ")", e);
+ return;
+ }
+
+ save_data[key] = JSON.stringify(data);
+
+ try {
+ save_data = JSON.stringify(save_data);
+ } catch (e) {
+ console.error("Failed to stringify save data (while trying to save " + key + ")", e);
+ return;
+ }
+
+ try {
+ localStorage.setItem(game.save_key, save_data);
+ } catch (e) {
+ console.error("Failed to save to localStorage (while saving key " + key + ")", e);
+ return;
+ }
+ }
+
+ function _load(game, key) {
+ let save_data;
+
+ if (!game.hasOwnProperty('save_key')) {
+ throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object");
+ }
+
+ try {
+ save_data = localStorage.getItem(game.save_key);
+ } catch (e) {
+ console.error("Loading is disabled; cannot load " + key, e);
+ return;
+ }
+
+ try {
+ save_data = JSON.parse(save_data);
+ } catch (e) {
+ console.error("Cannot parse stored save data (when trying to load " + key + ")", e);
+ return;
+ }
+
+ if (!save_data) {
+ return null;
+ }
+
+ return save_data[key];
+ }
+
+ /* ---- Transition stuff ---- */
+
+ let _transition = {
+ is_transitioning: false,
+ timer: 0,
+ color: '#000000',
+ w: 20,
+ h: 14,
+ dir_invert_v: false,
+ dir_invert_h: false,
+ invert_shape: true,
+ mid_long: false,
+ done_func: null,
+ type: _draw_fade_transition,
+ nodraw: false,
+ end_time: 100,
+ }
+
+ function _long_transition(game, type, length, callback) {
+ if (game.transition.is_transitioning) return;
+
+ _draw(game);
+
+ game.transition.invert_shape = false;
+ _internal_start_transition(game, type, length, function() {
+ game.transition.mid_long = true;
+ }, function() {
+ game.transition.invert_shape = true;
+ game.transition.is_transitioning = true;
+ let tdiv = game.transition.dir_invert_v;
+ let tdih = game.transition.dir_invert_h;
+ _internal_start_transition(game, type, length, function() {
+ game.transition.mid_long = false;
+ callback();
+ game.transition.dir_invert_v = tdiv;
+ game.transition.dir_invert_h = tdih;
+ });
+ });
+ }
+
+ function _start_transition(game, type, length, callback, on_done) {
+ if (game.transition.is_transitioning) return;
+ if (!game.transition.nodraw) _draw(game);
+
+ _internal_start_transition(game, type, length, callback, on_done);
+ }
+
+ function _internal_start_transition(game, type, length, callback, on_done) {
+ if (on_done) {
+ game.transition.done_func = on_done;
+ }
+
+ game.transition.timer = 0;
+ game.transition.type = type;
+ game.transition.end_time = length;
+
+ game.ctx.copy.drawImage(game.ctx.draw.canvas, 0, 0);
+
+ callback();
+
+ game.transition.is_transitioning = true;
+ }
+
+ function _finish_transition(game) {
+ game.transition.is_transitioning = false;
+ game.transition.timer = 0;
+
+ if (game.transition.done_func) {
+ setTimeout(function() {
+ game.transition.done_func();
+ game.transition.done_func = null;
+ }, 400);
+ }
+ }
+
+ function _draw_transition(game, ctx) {
+ let frac = game.transition.timer / game.transition.end_time;
+
+ ctx.save();
+ ctx.clearRect(0, 0, game.screen_w, game.screen_h);
+ game.transition.type(game, ctx, game.ctx.copy, game.ctx.draw, frac, game.transition.invert_shape);
+ ctx.restore();
+ }
+
+ function _draw_slide_down_transition(game, ctx, oldc, newc, frac, reverse) {
+ let offset = frac * game.screen_h;
+
+ ctx.drawImage(oldc.canvas, 0, -offset);
+ ctx.drawImage(newc.canvas, 0, game.screen_h - offset);
+ }
+
+ function _draw_slide_up_transition(game, ctx, oldc, newc, frac, reverse) {
+ let offset = frac * game.screen_h;
+
+ ctx.drawImage(oldc.canvas, 0, offset);
+ ctx.drawImage(newc.canvas, 0, - game.screen_h + offset);
+ }
+
+ function _draw_fade_transition(game, ctx, oldc, newc, frac, reverse) {
+ ctx.globalAlpha = 1;
+ ctx.drawImage(oldc.canvas, 0, 0);
+ ctx.globalAlpha = frac;
+ ctx.drawImage(newc.canvas, 0, 0);
+ }
+
+ /* ---- former contents of ready.js ---- */
+
+ // Due to Timo Huovinen
+ // https://stackoverflow.com/questions/799981/document-ready-equivalent-without-jquery
+ var ready = (function(){
+
+ var readyList,
+ DOMContentLoaded,
+ class2type = {};
+ class2type["[object Boolean]"] = "boolean";
+ class2type["[object Number]"] = "number";
+ class2type["[object String]"] = "string";
+ class2type["[object Function]"] = "function";
+ class2type["[object Array]"] = "array";
+ class2type["[object Date]"] = "date";
+ class2type["[object RegExp]"] = "regexp";
+ class2type["[object Object]"] = "object";
+
+ var ReadyObj = {
+ // Is the DOM ready to be used? Set to true once it occurs.
+ isReady: false,
+ // A counter to track how many items to wait for before
+ // the ready event fires. See #6781
+ readyWait: 1,
+ // Hold (or release) the ready event
+ holdReady: function( hold ) {
+ if ( hold ) {
+ ReadyObj.readyWait++;
+ } else {
+ ReadyObj.ready( true );
+ }
+ },
+ // Handle when the DOM is ready
+ ready: function( wait ) {
+ // Either a released hold or an DOMready/load event and not yet ready
+ if ( (wait === true && !--ReadyObj.readyWait) || (wait !== true && !ReadyObj.isReady) ) {
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( !document.body ) {
+ return setTimeout( ReadyObj.ready, 1 );
+ }
+
+ // Remember that the DOM is ready
+ ReadyObj.isReady = true;
+ // If a normal DOM Ready event fired, decrement, and wait if need be
+ if ( wait !== true && --ReadyObj.readyWait > 0 ) {
+ return;
+ }
+ // If there are functions bound, to execute
+ readyList.resolveWith( document, [ ReadyObj ] );
+
+ // Trigger any bound ready events
+ //if ( ReadyObj.fn.trigger ) {
+ // ReadyObj( document ).trigger( "ready" ).unbind( "ready" );
+ //}
+ }
+ },
+ bindReady: function() {
+ if ( readyList ) {
+ return;
+ }
+ readyList = ReadyObj._Deferred();
+
+ // Catch cases where $(document).ready() is called after the
+ // browser event has already occurred.
+ if ( document.readyState === "complete" ) {
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
+ return setTimeout( ReadyObj.ready, 1 );
+ }
+
+ // Mozilla, Opera and webkit nightlies currently support this event
+ if ( document.addEventListener ) {
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+ // A fallback to window.onload, that will always work
+ window.addEventListener( "load", ReadyObj.ready, false );
+
+ // If IE event model is used
+ } else if ( document.attachEvent ) {
+ // ensure firing before onload,
+ // maybe late but safe also for iframes
+ document.attachEvent( "onreadystatechange", DOMContentLoaded );
+
+ // A fallback to window.onload, that will always work
+ window.attachEvent( "onload", ReadyObj.ready );
+
+ // If IE and not a frame
+ // continually check to see if the document is ready
+ var toplevel = false;
+
+ try {
+ toplevel = window.frameElement == null;
+ } catch(e) {}
+
+ if ( document.documentElement.doScroll && toplevel ) {
+ doScrollCheck();
+ }
+ }
+ },
+ _Deferred: function() {
+ var // callbacks list
+ callbacks = [],
+ // stored [ context , args ]
+ fired,
+ // to avoid firing when already doing so
+ firing,
+ // flag to know if the deferred has been cancelled
+ cancelled,
+ // the deferred itself
+ deferred = {
+
+ // done( f1, f2, ...)
+ done: function() {
+ if ( !cancelled ) {
+ var args = arguments,
+ i,
+ length,
+ elem,
+ type,
+ _fired;
+ if ( fired ) {
+ _fired = fired;
+ fired = 0;
+ }
+ for ( i = 0, length = args.length; i < length; i++ ) {
+ elem = args[ i ];
+ type = ReadyObj.type( elem );
+ if ( type === "array" ) {
+ deferred.done.apply( deferred, elem );
+ } else if ( type === "function" ) {
+ callbacks.push( elem );
+ }
+ }
+ if ( _fired ) {
+ deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
+ }
+ }
+ return this;
+ },
+
+ // resolve with given context and args
+ resolveWith: function( context, args ) {
+ if ( !cancelled && !fired && !firing ) {
+ // make sure args are available (#8421)
+ args = args || [];
+ firing = 1;
+ try {
+ while( callbacks[ 0 ] ) {
+ callbacks.shift().apply( context, args );//shifts a callback, and applies it to document
+ }
+ }
+ finally {
+ fired = [ context, args ];
+ firing = 0;
+ }
+ }
+ return this;
+ },
+
+ // resolve with this as context and given arguments
+ resolve: function() {
+ deferred.resolveWith( this, arguments );
+ return this;
+ },
+
+ // Has this deferred been resolved?
+ isResolved: function() {
+ return !!( firing || fired );
+ },
+
+ // Cancel
+ cancel: function() {
+ cancelled = 1;
+ callbacks = [];
+ return this;
+ }
+ };
+
+ return deferred;
+ },
+ type: function( obj ) {
+ return obj == null ?
+ String( obj ) :
+ class2type[ Object.prototype.toString.call(obj) ] || "object";
+ }
+ }
+ // The DOM ready check for Internet Explorer
+ function doScrollCheck() {
+ if ( ReadyObj.isReady ) {
+ return;
+ }
+
+ try {
+ // If IE is used, use the trick by Diego Perini
+ // http://javascript.nwbox.com/IEContentLoaded/
+ document.documentElement.doScroll("left");
+ } catch(e) {
+ setTimeout( doScrollCheck, 1 );
+ return;
+ }
+
+ // and execute any waiting functions
+ ReadyObj.ready();
+ }
+ // Cleanup functions for the document ready method
+ if ( document.addEventListener ) {
+ DOMContentLoaded = function() {
+ document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+ ReadyObj.ready();
+ };
+
+ } else if ( document.attachEvent ) {
+ DOMContentLoaded = function() {
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( document.readyState === "complete" ) {
+ document.detachEvent( "onreadystatechange", DOMContentLoaded );
+ ReadyObj.ready();
+ }
+ };
+ }
+ function ready( fn ) {
+ // Attach the listeners
+ ReadyObj.bindReady();
+
+ var type = ReadyObj.type( fn );
+
+ // Add the callback
+ readyList.done( fn );//readyList is result of _Deferred()
+ }
+ return ready;
+ })();
+
+ return {
+ create_game: create_game,
+
+ ready: ready,
+ mod: mod,
+ sgn: sgn,
+ as_hex: as_hex,
+ copy_list: copy_list,
+ copy_flat_objlist: copy_flat_objlist,
+
+ sprite_draw: sprite_draw,
+ screen_draw: screen_draw,
+
+ buttons: {
+ create: create_buttons,
+ update: update_buttons,
+ click: click_button,
+ draw: button_draw,
+ },
+
+ transition: {
+ type: {
+ fade: _draw_fade_transition,
+ slide_up: _draw_slide_up_transition,
+ slide_down: _draw_slide_down_transition,
+ },
+ },
+ }
+})();