Initial commit
authorcassowarii <cassowarii@users.noreply.github.com>
Mon, 7 Apr 2025 03:45:00 +0000 (20:45 -0700)
committercassowarii <cassowarii@users.noreply.github.com>
Mon, 7 Apr 2025 03:45:00 +0000 (20:45 -0700)
116 files changed:
.gitignore [new file with mode: 0644]
ase/bubble.aseprite [new file with mode: 0644]
ase/buttons.aseprite [new file with mode: 0644]
ase/decoration.aseprite [new file with mode: 0644]
ase/failure.aseprite [new file with mode: 0644]
ase/fish.aseprite [new file with mode: 0644]
ase/fish.gif [new file with mode: 0644]
ase/heart.aseprite [new file with mode: 0644]
ase/level1.aseprite [new file with mode: 0644]
ase/level2.aseprite [new file with mode: 0644]
ase/level4.aseprite [new file with mode: 0644]
ase/levelnums.aseprite [new file with mode: 0644]
ase/menubuttons.aseprite [new file with mode: 0644]
ase/menuimage.aseprite [new file with mode: 0644]
ase/move_tile.aseprite [new file with mode: 0644]
ase/palette.aseprite [new file with mode: 0644]
ase/selector.aseprite [new file with mode: 0644]
ase/success.aseprite [new file with mode: 0644]
ase/wall_tile.aseprite [new file with mode: 0644]
ase/x.aseprite [new file with mode: 0644]
ase/youwon.aseprite [new file with mode: 0644]
clicktostart.png [new file with mode: 0644]
cover.png [new file with mode: 0644]
game.js [new file with mode: 0644]
img/bubble.png [new file with mode: 0644]
img/buttons.png [new file with mode: 0644]
img/decoration.png [new file with mode: 0644]
img/failure.png [new file with mode: 0644]
img/fish.png [new file with mode: 0644]
img/heart.png [new file with mode: 0644]
img/level/1.png [new file with mode: 0644]
img/level/2.png [new file with mode: 0644]
img/level/4.png [new file with mode: 0644]
img/levelnums.png [new file with mode: 0644]
img/menubuttons.png [new file with mode: 0644]
img/menuimage.png [new file with mode: 0644]
img/move_tile.png [new file with mode: 0644]
img/selector.png [new file with mode: 0644]
img/success.png [new file with mode: 0644]
img/wall_tile.png [new file with mode: 0644]
img/x.png [new file with mode: 0644]
img/youwon.png [new file with mode: 0644]
index.html [new file with mode: 0644]
levels.js [new file with mode: 0644]
levels/1.tmx [new file with mode: 0644]
levels/10.tmx [new file with mode: 0644]
levels/2.tmx [new file with mode: 0644]
levels/3.tmx [new file with mode: 0644]
levels/4.tmx [new file with mode: 0644]
levels/5.tmx [new file with mode: 0644]
levels/6.tmx [new file with mode: 0644]
levels/7.tmx [new file with mode: 0644]
levels/8.tmx [new file with mode: 0644]
levels/9.tmx [new file with mode: 0644]
levels/mapcompile.py [new file with mode: 0755]
levels/test.tmx [new file with mode: 0644]
levels/tmptile.png [new file with mode: 0644]
levels/tmptile.tsx [new file with mode: 0644]
loading.png [new file with mode: 0644]
music/sharkgamebeachmusic.mp3 [new file with mode: 0755]
packaging.zip [new file with mode: 0644]
packaging/clicktostart.png [new file with mode: 0644]
packaging/game.js [new file with mode: 0644]
packaging/img/bubble.png [new file with mode: 0644]
packaging/img/buttons.png [new file with mode: 0644]
packaging/img/decoration.png [new file with mode: 0644]
packaging/img/failure.png [new file with mode: 0644]
packaging/img/fish.png [new file with mode: 0644]
packaging/img/heart.png [new file with mode: 0644]
packaging/img/level/1.png [new file with mode: 0644]
packaging/img/level/2.png [new file with mode: 0644]
packaging/img/level/4.png [new file with mode: 0644]
packaging/img/levelnums.png [new file with mode: 0644]
packaging/img/menubuttons.png [new file with mode: 0644]
packaging/img/menuimage.png [new file with mode: 0644]
packaging/img/move_tile.png [new file with mode: 0644]
packaging/img/selector.png [new file with mode: 0644]
packaging/img/success.png [new file with mode: 0644]
packaging/img/wall_tile.png [new file with mode: 0644]
packaging/img/x.png [new file with mode: 0644]
packaging/img/youwon.png [new file with mode: 0644]
packaging/index.html [new file with mode: 0644]
packaging/levels.js [new file with mode: 0644]
packaging/loading.png [new file with mode: 0644]
packaging/music/sharkgamebeachmusic.mp3 [new file with mode: 0755]
packaging/saveerror.png [new file with mode: 0644]
packaging/sfx/bubble.wav [new file with mode: 0644]
packaging/sfx/bubble2.wav [new file with mode: 0644]
packaging/sfx/bubble3.wav [new file with mode: 0644]
packaging/sfx/bubble4.wav [new file with mode: 0644]
packaging/sfx/drag.wav [new file with mode: 0644]
packaging/sfx/drip.wav [new file with mode: 0644]
packaging/sfx/fail.wav [new file with mode: 0644]
packaging/sfx/victory.wav [new file with mode: 0644]
packaging/zucchinibread.js [new file with mode: 0644]
pause.png [new file with mode: 0644]
progressbar.png [new file with mode: 0644]
saveerror.png [new file with mode: 0644]
sfx/bubble.wav [new file with mode: 0644]
sfx/bubble2.wav [new file with mode: 0644]
sfx/bubble3.wav [new file with mode: 0644]
sfx/bubble4.wav [new file with mode: 0644]
sfx/drag.wav [new file with mode: 0644]
sfx/drip.wav [new file with mode: 0644]
sfx/fail.wav [new file with mode: 0644]
sfx/victory.wav [new file with mode: 0644]
sfx_raw/2025_04_06_08_58_05_1.mp3 [new file with mode: 0644]
sfx_raw/2025_04_06_08_58_35_1.mp3 [new file with mode: 0644]
sfx_raw/2025_04_06_09_41_49_1.mp3 [new file with mode: 0644]
sfx_raw/fail.wav [new file with mode: 0644]
sfx_raw/fanfare.wav [new file with mode: 0644]
sharkfont.zip [new file with mode: 0644]
sharkfont/license.txt [new file with mode: 0644]
sharkfont/readme.txt [new file with mode: 0644]
sharkfont/sharkfont.ttf [new file with mode: 0644]
zucchinibread.js [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..37dbf1a
--- /dev/null
@@ -0,0 +1,3 @@
+*.xcf
+*.swp
+*.swo
diff --git a/ase/bubble.aseprite b/ase/bubble.aseprite
new file mode 100644 (file)
index 0000000..fe81ac3
Binary files /dev/null and b/ase/bubble.aseprite differ
diff --git a/ase/buttons.aseprite b/ase/buttons.aseprite
new file mode 100644 (file)
index 0000000..6b00531
Binary files /dev/null and b/ase/buttons.aseprite differ
diff --git a/ase/decoration.aseprite b/ase/decoration.aseprite
new file mode 100644 (file)
index 0000000..70bcbad
Binary files /dev/null and b/ase/decoration.aseprite differ
diff --git a/ase/failure.aseprite b/ase/failure.aseprite
new file mode 100644 (file)
index 0000000..b3388f6
Binary files /dev/null and b/ase/failure.aseprite differ
diff --git a/ase/fish.aseprite b/ase/fish.aseprite
new file mode 100644 (file)
index 0000000..ddc99d2
Binary files /dev/null and b/ase/fish.aseprite differ
diff --git a/ase/fish.gif b/ase/fish.gif
new file mode 100644 (file)
index 0000000..3572850
Binary files /dev/null and b/ase/fish.gif differ
diff --git a/ase/heart.aseprite b/ase/heart.aseprite
new file mode 100644 (file)
index 0000000..795366e
Binary files /dev/null and b/ase/heart.aseprite differ
diff --git a/ase/level1.aseprite b/ase/level1.aseprite
new file mode 100644 (file)
index 0000000..7eca6e6
Binary files /dev/null and b/ase/level1.aseprite differ
diff --git a/ase/level2.aseprite b/ase/level2.aseprite
new file mode 100644 (file)
index 0000000..2983b55
Binary files /dev/null and b/ase/level2.aseprite differ
diff --git a/ase/level4.aseprite b/ase/level4.aseprite
new file mode 100644 (file)
index 0000000..b012708
Binary files /dev/null and b/ase/level4.aseprite differ
diff --git a/ase/levelnums.aseprite b/ase/levelnums.aseprite
new file mode 100644 (file)
index 0000000..8fe4ab8
Binary files /dev/null and b/ase/levelnums.aseprite differ
diff --git a/ase/menubuttons.aseprite b/ase/menubuttons.aseprite
new file mode 100644 (file)
index 0000000..5d91746
Binary files /dev/null and b/ase/menubuttons.aseprite differ
diff --git a/ase/menuimage.aseprite b/ase/menuimage.aseprite
new file mode 100644 (file)
index 0000000..3e75531
Binary files /dev/null and b/ase/menuimage.aseprite differ
diff --git a/ase/move_tile.aseprite b/ase/move_tile.aseprite
new file mode 100644 (file)
index 0000000..cf9e289
Binary files /dev/null and b/ase/move_tile.aseprite differ
diff --git a/ase/palette.aseprite b/ase/palette.aseprite
new file mode 100644 (file)
index 0000000..73483ec
Binary files /dev/null and b/ase/palette.aseprite differ
diff --git a/ase/selector.aseprite b/ase/selector.aseprite
new file mode 100644 (file)
index 0000000..f0b30f6
Binary files /dev/null and b/ase/selector.aseprite differ
diff --git a/ase/success.aseprite b/ase/success.aseprite
new file mode 100644 (file)
index 0000000..d4f42c7
Binary files /dev/null and b/ase/success.aseprite differ
diff --git a/ase/wall_tile.aseprite b/ase/wall_tile.aseprite
new file mode 100644 (file)
index 0000000..91e6e4b
Binary files /dev/null and b/ase/wall_tile.aseprite differ
diff --git a/ase/x.aseprite b/ase/x.aseprite
new file mode 100644 (file)
index 0000000..d643e7f
Binary files /dev/null and b/ase/x.aseprite differ
diff --git a/ase/youwon.aseprite b/ase/youwon.aseprite
new file mode 100644 (file)
index 0000000..1973508
Binary files /dev/null and b/ase/youwon.aseprite differ
diff --git a/clicktostart.png b/clicktostart.png
new file mode 100644 (file)
index 0000000..9454614
Binary files /dev/null and b/clicktostart.png differ
diff --git a/cover.png b/cover.png
new file mode 100644 (file)
index 0000000..4ebdd5e
Binary files /dev/null and b/cover.png differ
diff --git a/game.js b/game.js
new file mode 100644 (file)
index 0000000..dfa9f2b
--- /dev/null
+++ b/game.js
@@ -0,0 +1,1514 @@
+"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();
+    });
+}
diff --git a/img/bubble.png b/img/bubble.png
new file mode 100644 (file)
index 0000000..d601231
Binary files /dev/null and b/img/bubble.png differ
diff --git a/img/buttons.png b/img/buttons.png
new file mode 100644 (file)
index 0000000..86d6565
Binary files /dev/null and b/img/buttons.png differ
diff --git a/img/decoration.png b/img/decoration.png
new file mode 100644 (file)
index 0000000..89582fd
Binary files /dev/null and b/img/decoration.png differ
diff --git a/img/failure.png b/img/failure.png
new file mode 100644 (file)
index 0000000..967da35
Binary files /dev/null and b/img/failure.png differ
diff --git a/img/fish.png b/img/fish.png
new file mode 100644 (file)
index 0000000..ad1235a
Binary files /dev/null and b/img/fish.png differ
diff --git a/img/heart.png b/img/heart.png
new file mode 100644 (file)
index 0000000..143ece0
Binary files /dev/null and b/img/heart.png differ
diff --git a/img/level/1.png b/img/level/1.png
new file mode 100644 (file)
index 0000000..bffdafb
Binary files /dev/null and b/img/level/1.png differ
diff --git a/img/level/2.png b/img/level/2.png
new file mode 100644 (file)
index 0000000..01c3882
Binary files /dev/null and b/img/level/2.png differ
diff --git a/img/level/4.png b/img/level/4.png
new file mode 100644 (file)
index 0000000..9e70f32
Binary files /dev/null and b/img/level/4.png differ
diff --git a/img/levelnums.png b/img/levelnums.png
new file mode 100644 (file)
index 0000000..7107a49
Binary files /dev/null and b/img/levelnums.png differ
diff --git a/img/menubuttons.png b/img/menubuttons.png
new file mode 100644 (file)
index 0000000..9165a92
Binary files /dev/null and b/img/menubuttons.png differ
diff --git a/img/menuimage.png b/img/menuimage.png
new file mode 100644 (file)
index 0000000..5623295
Binary files /dev/null and b/img/menuimage.png differ
diff --git a/img/move_tile.png b/img/move_tile.png
new file mode 100644 (file)
index 0000000..0cc83ae
Binary files /dev/null and b/img/move_tile.png differ
diff --git a/img/selector.png b/img/selector.png
new file mode 100644 (file)
index 0000000..6b74208
Binary files /dev/null and b/img/selector.png differ
diff --git a/img/success.png b/img/success.png
new file mode 100644 (file)
index 0000000..025419c
Binary files /dev/null and b/img/success.png differ
diff --git a/img/wall_tile.png b/img/wall_tile.png
new file mode 100644 (file)
index 0000000..8ac813d
Binary files /dev/null and b/img/wall_tile.png differ
diff --git a/img/x.png b/img/x.png
new file mode 100644 (file)
index 0000000..07171aa
Binary files /dev/null and b/img/x.png differ
diff --git a/img/youwon.png b/img/youwon.png
new file mode 100644 (file)
index 0000000..7471f14
Binary files /dev/null and b/img/youwon.png differ
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..1ae5796
--- /dev/null
@@ -0,0 +1,19 @@
+<!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>
diff --git a/levels.js b/levels.js
new file mode 100644 (file)
index 0000000..7796217
--- /dev/null
+++ b/levels.js
@@ -0,0 +1,13 @@
+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: [] },
+}
diff --git a/levels/1.tmx b/levels/1.tmx
new file mode 100644 (file)
index 0000000..b1f7efc
--- /dev/null
@@ -0,0 +1,20 @@
+<?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>
diff --git a/levels/10.tmx b/levels/10.tmx
new file mode 100644 (file)
index 0000000..b450c62
--- /dev/null
@@ -0,0 +1,20 @@
+<?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>
diff --git a/levels/2.tmx b/levels/2.tmx
new file mode 100644 (file)
index 0000000..888160a
--- /dev/null
@@ -0,0 +1,20 @@
+<?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>
diff --git a/levels/3.tmx b/levels/3.tmx
new file mode 100644 (file)
index 0000000..31b4151
--- /dev/null
@@ -0,0 +1,20 @@
+<?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>
diff --git a/levels/4.tmx b/levels/4.tmx
new file mode 100644 (file)
index 0000000..11bc8b3
--- /dev/null
@@ -0,0 +1,20 @@
+<?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>
diff --git a/levels/5.tmx b/levels/5.tmx
new file mode 100644 (file)
index 0000000..d76e7f7
--- /dev/null
@@ -0,0 +1,20 @@
+<?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>
diff --git a/levels/6.tmx b/levels/6.tmx
new file mode 100644 (file)
index 0000000..4c29414
--- /dev/null
@@ -0,0 +1,20 @@
+<?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>
diff --git a/levels/7.tmx b/levels/7.tmx
new file mode 100644 (file)
index 0000000..e17291f
--- /dev/null
@@ -0,0 +1,20 @@
+<?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>
diff --git a/levels/8.tmx b/levels/8.tmx
new file mode 100644 (file)
index 0000000..b1689fe
--- /dev/null
@@ -0,0 +1,20 @@
+<?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>
diff --git a/levels/9.tmx b/levels/9.tmx
new file mode 100644 (file)
index 0000000..2bb7307
--- /dev/null
@@ -0,0 +1,20 @@
+<?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>
diff --git a/levels/mapcompile.py b/levels/mapcompile.py
new file mode 100755 (executable)
index 0000000..820231c
--- /dev/null
@@ -0,0 +1,94 @@
+#!/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("}")
diff --git a/levels/test.tmx b/levels/test.tmx
new file mode 100644 (file)
index 0000000..b86cf3e
--- /dev/null
@@ -0,0 +1,20 @@
+<?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>
diff --git a/levels/tmptile.png b/levels/tmptile.png
new file mode 100644 (file)
index 0000000..f4b4b2f
Binary files /dev/null and b/levels/tmptile.png differ
diff --git a/levels/tmptile.tsx b/levels/tmptile.tsx
new file mode 100644 (file)
index 0000000..4ae6535
--- /dev/null
@@ -0,0 +1,4 @@
+<?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>
diff --git a/loading.png b/loading.png
new file mode 100644 (file)
index 0000000..9552ba1
Binary files /dev/null and b/loading.png differ
diff --git a/music/sharkgamebeachmusic.mp3 b/music/sharkgamebeachmusic.mp3
new file mode 100755 (executable)
index 0000000..6390123
Binary files /dev/null and b/music/sharkgamebeachmusic.mp3 differ
diff --git a/packaging.zip b/packaging.zip
new file mode 100644 (file)
index 0000000..32eab08
Binary files /dev/null and b/packaging.zip differ
diff --git a/packaging/clicktostart.png b/packaging/clicktostart.png
new file mode 100644 (file)
index 0000000..9454614
Binary files /dev/null and b/packaging/clicktostart.png differ
diff --git a/packaging/game.js b/packaging/game.js
new file mode 100644 (file)
index 0000000..dfa9f2b
--- /dev/null
@@ -0,0 +1,1514 @@
+"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();
+    });
+}
diff --git a/packaging/img/bubble.png b/packaging/img/bubble.png
new file mode 100644 (file)
index 0000000..d601231
Binary files /dev/null and b/packaging/img/bubble.png differ
diff --git a/packaging/img/buttons.png b/packaging/img/buttons.png
new file mode 100644 (file)
index 0000000..86d6565
Binary files /dev/null and b/packaging/img/buttons.png differ
diff --git a/packaging/img/decoration.png b/packaging/img/decoration.png
new file mode 100644 (file)
index 0000000..89582fd
Binary files /dev/null and b/packaging/img/decoration.png differ
diff --git a/packaging/img/failure.png b/packaging/img/failure.png
new file mode 100644 (file)
index 0000000..967da35
Binary files /dev/null and b/packaging/img/failure.png differ
diff --git a/packaging/img/fish.png b/packaging/img/fish.png
new file mode 100644 (file)
index 0000000..ad1235a
Binary files /dev/null and b/packaging/img/fish.png differ
diff --git a/packaging/img/heart.png b/packaging/img/heart.png
new file mode 100644 (file)
index 0000000..143ece0
Binary files /dev/null and b/packaging/img/heart.png differ
diff --git a/packaging/img/level/1.png b/packaging/img/level/1.png
new file mode 100644 (file)
index 0000000..bffdafb
Binary files /dev/null and b/packaging/img/level/1.png differ
diff --git a/packaging/img/level/2.png b/packaging/img/level/2.png
new file mode 100644 (file)
index 0000000..01c3882
Binary files /dev/null and b/packaging/img/level/2.png differ
diff --git a/packaging/img/level/4.png b/packaging/img/level/4.png
new file mode 100644 (file)
index 0000000..9e70f32
Binary files /dev/null and b/packaging/img/level/4.png differ
diff --git a/packaging/img/levelnums.png b/packaging/img/levelnums.png
new file mode 100644 (file)
index 0000000..7107a49
Binary files /dev/null and b/packaging/img/levelnums.png differ
diff --git a/packaging/img/menubuttons.png b/packaging/img/menubuttons.png
new file mode 100644 (file)
index 0000000..9165a92
Binary files /dev/null and b/packaging/img/menubuttons.png differ
diff --git a/packaging/img/menuimage.png b/packaging/img/menuimage.png
new file mode 100644 (file)
index 0000000..5623295
Binary files /dev/null and b/packaging/img/menuimage.png differ
diff --git a/packaging/img/move_tile.png b/packaging/img/move_tile.png
new file mode 100644 (file)
index 0000000..0cc83ae
Binary files /dev/null and b/packaging/img/move_tile.png differ
diff --git a/packaging/img/selector.png b/packaging/img/selector.png
new file mode 100644 (file)
index 0000000..6b74208
Binary files /dev/null and b/packaging/img/selector.png differ
diff --git a/packaging/img/success.png b/packaging/img/success.png
new file mode 100644 (file)
index 0000000..025419c
Binary files /dev/null and b/packaging/img/success.png differ
diff --git a/packaging/img/wall_tile.png b/packaging/img/wall_tile.png
new file mode 100644 (file)
index 0000000..8ac813d
Binary files /dev/null and b/packaging/img/wall_tile.png differ
diff --git a/packaging/img/x.png b/packaging/img/x.png
new file mode 100644 (file)
index 0000000..07171aa
Binary files /dev/null and b/packaging/img/x.png differ
diff --git a/packaging/img/youwon.png b/packaging/img/youwon.png
new file mode 100644 (file)
index 0000000..7471f14
Binary files /dev/null and b/packaging/img/youwon.png differ
diff --git a/packaging/index.html b/packaging/index.html
new file mode 100644 (file)
index 0000000..1ae5796
--- /dev/null
@@ -0,0 +1,19 @@
+<!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>
diff --git a/packaging/levels.js b/packaging/levels.js
new file mode 100644 (file)
index 0000000..7796217
--- /dev/null
@@ -0,0 +1,13 @@
+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: [] },
+}
diff --git a/packaging/loading.png b/packaging/loading.png
new file mode 100644 (file)
index 0000000..9552ba1
Binary files /dev/null and b/packaging/loading.png differ
diff --git a/packaging/music/sharkgamebeachmusic.mp3 b/packaging/music/sharkgamebeachmusic.mp3
new file mode 100755 (executable)
index 0000000..6390123
Binary files /dev/null and b/packaging/music/sharkgamebeachmusic.mp3 differ
diff --git a/packaging/saveerror.png b/packaging/saveerror.png
new file mode 100644 (file)
index 0000000..ab259b9
Binary files /dev/null and b/packaging/saveerror.png differ
diff --git a/packaging/sfx/bubble.wav b/packaging/sfx/bubble.wav
new file mode 100644 (file)
index 0000000..c8a37fa
Binary files /dev/null and b/packaging/sfx/bubble.wav differ
diff --git a/packaging/sfx/bubble2.wav b/packaging/sfx/bubble2.wav
new file mode 100644 (file)
index 0000000..7b173be
Binary files /dev/null and b/packaging/sfx/bubble2.wav differ
diff --git a/packaging/sfx/bubble3.wav b/packaging/sfx/bubble3.wav
new file mode 100644 (file)
index 0000000..d640ee8
Binary files /dev/null and b/packaging/sfx/bubble3.wav differ
diff --git a/packaging/sfx/bubble4.wav b/packaging/sfx/bubble4.wav
new file mode 100644 (file)
index 0000000..e137be1
Binary files /dev/null and b/packaging/sfx/bubble4.wav differ
diff --git a/packaging/sfx/drag.wav b/packaging/sfx/drag.wav
new file mode 100644 (file)
index 0000000..89fb719
Binary files /dev/null and b/packaging/sfx/drag.wav differ
diff --git a/packaging/sfx/drip.wav b/packaging/sfx/drip.wav
new file mode 100644 (file)
index 0000000..c17ef2a
Binary files /dev/null and b/packaging/sfx/drip.wav differ
diff --git a/packaging/sfx/fail.wav b/packaging/sfx/fail.wav
new file mode 100644 (file)
index 0000000..028864b
Binary files /dev/null and b/packaging/sfx/fail.wav differ
diff --git a/packaging/sfx/victory.wav b/packaging/sfx/victory.wav
new file mode 100644 (file)
index 0000000..679eaaf
Binary files /dev/null and b/packaging/sfx/victory.wav differ
diff --git a/packaging/zucchinibread.js b/packaging/zucchinibread.js
new file mode 100644 (file)
index 0000000..eed4706
--- /dev/null
@@ -0,0 +1,1321 @@
+"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,
+            },
+        },
+    }
+})();
diff --git a/pause.png b/pause.png
new file mode 100644 (file)
index 0000000..7479e89
Binary files /dev/null and b/pause.png differ
diff --git a/progressbar.png b/progressbar.png
new file mode 100644 (file)
index 0000000..7479e89
Binary files /dev/null and b/progressbar.png differ
diff --git a/saveerror.png b/saveerror.png
new file mode 100644 (file)
index 0000000..ab259b9
Binary files /dev/null and b/saveerror.png differ
diff --git a/sfx/bubble.wav b/sfx/bubble.wav
new file mode 100644 (file)
index 0000000..c8a37fa
Binary files /dev/null and b/sfx/bubble.wav differ
diff --git a/sfx/bubble2.wav b/sfx/bubble2.wav
new file mode 100644 (file)
index 0000000..7b173be
Binary files /dev/null and b/sfx/bubble2.wav differ
diff --git a/sfx/bubble3.wav b/sfx/bubble3.wav
new file mode 100644 (file)
index 0000000..d640ee8
Binary files /dev/null and b/sfx/bubble3.wav differ
diff --git a/sfx/bubble4.wav b/sfx/bubble4.wav
new file mode 100644 (file)
index 0000000..e137be1
Binary files /dev/null and b/sfx/bubble4.wav differ
diff --git a/sfx/drag.wav b/sfx/drag.wav
new file mode 100644 (file)
index 0000000..89fb719
Binary files /dev/null and b/sfx/drag.wav differ
diff --git a/sfx/drip.wav b/sfx/drip.wav
new file mode 100644 (file)
index 0000000..c17ef2a
Binary files /dev/null and b/sfx/drip.wav differ
diff --git a/sfx/fail.wav b/sfx/fail.wav
new file mode 100644 (file)
index 0000000..028864b
Binary files /dev/null and b/sfx/fail.wav differ
diff --git a/sfx/victory.wav b/sfx/victory.wav
new file mode 100644 (file)
index 0000000..679eaaf
Binary files /dev/null and b/sfx/victory.wav differ
diff --git a/sfx_raw/2025_04_06_08_58_05_1.mp3 b/sfx_raw/2025_04_06_08_58_05_1.mp3
new file mode 100644 (file)
index 0000000..3136fd2
Binary files /dev/null and b/sfx_raw/2025_04_06_08_58_05_1.mp3 differ
diff --git a/sfx_raw/2025_04_06_08_58_35_1.mp3 b/sfx_raw/2025_04_06_08_58_35_1.mp3
new file mode 100644 (file)
index 0000000..757b0d7
Binary files /dev/null and b/sfx_raw/2025_04_06_08_58_35_1.mp3 differ
diff --git a/sfx_raw/2025_04_06_09_41_49_1.mp3 b/sfx_raw/2025_04_06_09_41_49_1.mp3
new file mode 100644 (file)
index 0000000..e280633
Binary files /dev/null and b/sfx_raw/2025_04_06_09_41_49_1.mp3 differ
diff --git a/sfx_raw/fail.wav b/sfx_raw/fail.wav
new file mode 100644 (file)
index 0000000..b9cd3dc
Binary files /dev/null and b/sfx_raw/fail.wav differ
diff --git a/sfx_raw/fanfare.wav b/sfx_raw/fanfare.wav
new file mode 100644 (file)
index 0000000..f763f9d
Binary files /dev/null and b/sfx_raw/fanfare.wav differ
diff --git a/sharkfont.zip b/sharkfont.zip
new file mode 100644 (file)
index 0000000..9ef9ea1
Binary files /dev/null and b/sharkfont.zip differ
diff --git a/sharkfont/license.txt b/sharkfont/license.txt
new file mode 100644 (file)
index 0000000..6fe4768
--- /dev/null
@@ -0,0 +1,163 @@
+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.
+
diff --git a/sharkfont/readme.txt b/sharkfont/readme.txt
new file mode 100644 (file)
index 0000000..4667a2f
--- /dev/null
@@ -0,0 +1,16 @@
+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
diff --git a/sharkfont/sharkfont.ttf b/sharkfont/sharkfont.ttf
new file mode 100644 (file)
index 0000000..bf1d333
Binary files /dev/null and b/sharkfont/sharkfont.ttf differ
diff --git a/zucchinibread.js b/zucchinibread.js
new file mode 100644 (file)
index 0000000..eed4706
--- /dev/null
@@ -0,0 +1,1321 @@
+"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,
+            },
+        },
+    }
+})();