initial commit
authorcassowarii <2374677+cassowarii@users.noreply.github.com>
Sun, 19 Apr 2026 06:34:15 +0000 (23:34 -0700)
committercassowarii <2374677+cassowarii@users.noreply.github.com>
Sun, 19 Apr 2026 06:34:15 +0000 (23:34 -0700)
30 files changed:
.gitignore [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/connector.png [new file with mode: 0644]
img/gameover.png [new file with mode: 0644]
img/lightning.png [new file with mode: 0644]
img/lightningbutton.png [new file with mode: 0644]
img/lightningnumbers.png [new file with mode: 0644]
img/playbutton.png [new file with mode: 0644]
img/replaybutton.png [new file with mode: 0644]
img/scorenumbers.png [new file with mode: 0644]
img/selector.png [new file with mode: 0644]
img/text.png [new file with mode: 0644]
img/tiles.png [new file with mode: 0644]
img/timebar_inside.png [new file with mode: 0644]
img/timebar_outside.png [new file with mode: 0644]
img/title.png [new file with mode: 0644]
index.html [new file with mode: 0644]
loading.png [new file with mode: 0644]
music/suitgamesong.mp3 [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/bap.wav [new file with mode: 0644]
sfx/gameover.mp3 [new file with mode: 0644]
sfx/lightning.wav [new file with mode: 0644]
sfx/ok.wav [new file with mode: 0644]
sfx/thud.wav [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..b79e268
--- /dev/null
@@ -0,0 +1,8 @@
+*.xcf
+*.swp
+*.swo
+sfx_raw/*
+ase/*
+packaging.zip
+packaging/*
+*-sheet.png
diff --git a/clicktostart.png b/clicktostart.png
new file mode 100644 (file)
index 0000000..eb5a61f
Binary files /dev/null and b/clicktostart.png differ
diff --git a/cover.png b/cover.png
new file mode 100644 (file)
index 0000000..5026f84
Binary files /dev/null and b/cover.png differ
diff --git a/game.js b/game.js
new file mode 100644 (file)
index 0000000..d22e86e
--- /dev/null
+++ b/game.js
@@ -0,0 +1,935 @@
+"use strict";
+
+let game;
+
+/* --------- definitions ---------- */
+
+/* state:
+ * TITLE: title screen
+ * STAND: waiting for input
+ * DRAG: connecting tiles together
+ * DESTROY: tiles exploding
+ * REPLACE: new tiles falling into place
+ * LIGHTNING: selecting something to lightning
+ * GAMEOVER: game over
+ */
+let State = { TITLE: -1, STAND: 0, DRAG: 1, DESTROY: 2, REPLACE: 3, LIGHTNING: 4, GAMEOVER: 5 };
+
+let can_continue = false;
+
+let save_data = 1;
+const SAVE_KEY = "casso.signalconnectinggame.save"
+
+zb.ready(function() {
+    game = zb.create_game({
+        canvas: 'canvas',
+        canvas_w: 486,
+        canvas_h: 756,
+        draw_scale: 3,
+        tile_size: 16,
+        level_w: 8,
+        level_h: 8,
+        background_color: '#f4ecce',
+        run_in_background: true,
+        load_with_progress_bar: true,
+        save_key: SAVE_KEY,
+        state: State.STAND,
+        events: {
+            gamestart: handle_gamestart,
+            mousemove: handle_mousemove,
+            mouseup: handle_mouseup,
+            mousedown: handle_mousedown,
+        },
+        modes: {
+            menu: {
+                draw: do_draw_menu,
+                update: do_update_menu,
+            },
+            youwon: {
+                draw: do_draw_youwon,
+            },
+            game: {
+                draw: do_draw,
+                update: do_update,
+            },
+        },
+        mode: 'game',
+        buttons: {},
+    });
+
+    game.register_images({
+        connector: 'img/connector.png',
+        gameover: 'img/gameover.png',
+        lightning: 'img/lightning.png',
+        lightningbutton: 'img/lightningbutton.png',
+        lightningnumbers: 'img/lightningnumbers.png',
+        playbutton: 'img/playbutton.png',
+        replaybutton: 'img/replaybutton.png',
+        scorenumbers: 'img/scorenumbers.png',
+        selector: 'img/selector.png',
+        text: 'img/text.png',
+        tiles: 'img/tiles.png',
+        timebar_inside: 'img/timebar_inside.png',
+        timebar_outside: 'img/timebar_outside.png',
+        title: 'img/title.png',
+    });
+
+    game.register_sfx({
+        ok: {
+            path: 'sfx/ok.wav',
+            volume: 0.7,
+        },
+        thud: {
+            path: 'sfx/thud.wav',
+            volume: 1.0,
+            copies: 20,
+        },
+        bap: {
+            path: 'sfx/bap.wav',
+            volume: 0.2,
+        },
+        lightning: {
+            path: 'sfx/lightning.wav',
+            volume: 0.4,
+        },
+        gameover: {
+            path: 'sfx/gameover.mp3',
+            volume: 0.3,
+        },
+    });
+
+    game.register_music({
+        bgm: {
+            path: 'music/suitgamesong',
+            volume: 0.5,
+        },
+    });
+
+    game.resources_ready();
+
+    board_offset_x = game.screen_w / 2 - TILE_W * BOARD_W / 2;
+    board_offset_y = game.screen_h / 2 - TILE_H * BOARD_H / 2;
+
+    high_score = game.load('high_score') || 0;
+
+    game.buttons.menu = zb.buttons.create({
+        img: game.img.menubuttons,
+        button_w: 64,
+        button_h: 16,
+        buttons: [ ],
+    });
+
+    game.buttons.lightning = zb.buttons.create({
+        img: game.img.lightningbutton,
+        button_w: 36,
+        button_h: 18,
+        buttons: [
+            {
+                /* Lightning button */
+                x: game.screen_w / 2 - 16,
+                y: board_offset_y + BOARD_H * TILE_H + 12,
+                id: 0,
+                disabled: true,
+                callback: function() {
+                    if (game.state === State.STAND) {
+                        game.state = State.LIGHTNING;
+                        game.buttons.lightning.buttons[0].disabled = true;
+                    }
+                },
+            },
+        ],
+    });
+
+    game.buttons.play = zb.buttons.create({
+        img: game.img.playbutton,
+        button_w: 36,
+        button_h: 18,
+        buttons: [
+            {
+                /* Play button */
+                x: game.screen_w / 2 - 16,
+                y: board_offset_y + BOARD_H * TILE_H + 12,
+                id: 0,
+                callback: function() {
+                    if (game.state === State.TITLE) {
+                        initialize();
+                        score = 0;
+                        displayed_score = 0;
+                        timer_running = false;
+                        lightnings = 0;
+                    }
+                },
+            },
+        ],
+    });
+
+
+    game.buttons.replay = zb.buttons.create({
+        img: game.img.replaybutton,
+        button_w: 36,
+        button_h: 18,
+        buttons: [
+            {
+                /* Replay button */
+                x: game.screen_w / 2 - 16,
+                y: board_offset_y + BOARD_H * TILE_H + 12,
+                id: 0,
+                callback: function() {
+                    if (game.state === State.GAMEOVER) {
+                        initialize();
+                        score = 0;
+                        displayed_score = 0;
+                        timer_running = false;
+                        lightnings = 0;
+                        n_colors = 2;
+                        game.music.bgm.currentTime = 0;
+                    }
+                },
+            },
+        ],
+    });
+
+    game.transition.color = game.background_color;
+
+    initialize();
+});
+
+/* ------ relevant constant types --------- */
+
+const suitID = {
+    spades: 0,
+    clubs: 1,
+    hearts: 2,
+    diamonds: 3,
+};
+
+const colorID = {
+    green: 0,
+    blue: 1,
+    orange: 2,
+    purple: 3,
+};
+
+const TILE_W = 16;
+const TILE_H = 16;
+
+const BOARD_W = 8;
+const BOARD_H = 8;
+
+const NSUITS = 6;
+let n_colors = 2;
+
+const MAX_N_COLORS = 6;
+
+const WILD_SUIT = 4;
+const EVIL_SUIT = 5;
+
+const MAX_LIGHTNINGS = 99;
+
+const GRAVITY = 400;
+
+const MAX_TIMER = 40000;
+const TIME_GAIN_PER_BLOCK = 150;
+const TIME_INCREASE_PER_BLOCK = 40;
+
+/* ------ timers & static timer values --------- */
+
+const TILE_DELAY_PER_ROW = 70;
+const TILE_DELAY_PER_COL = 60;
+
+const THUD_SPACING = 60;
+let thud_timeout = 0;
+
+/* ------- game global state -------- */
+
+let level = Array(BOARD_W * BOARD_H).fill(null);
+
+let selector = {
+    x: -1,
+    y: -1,
+};
+
+let board_offset_x;
+let board_offset_y;
+
+let path = [];
+
+let lightnings = 0;
+
+let lightningmode = false;
+
+let lightning_particles = [];
+
+let game_just_started = true;
+let show_title = true;
+
+let score = 0;
+let displayed_score = 0;
+
+let high_score = 0;
+
+let mouse_x = -1;
+let mouse_y = -1;
+
+let timer;
+let timer_running = false;
+
+/* ------- game behavior functions -------- */
+
+function tile_at(x, y) {
+    if (x < 0 || x >= game.level_w) return null;
+    if (y < 0 || y >= game.level_h) return null;
+
+    let result = level[y * game.level_w + x];
+    return result;
+}
+
+function set_tile_at(x, y, value) {
+    if (x < 0 || x >= game.level_w) return null;
+    if (y < 0 || y >= game.level_h) return null;
+
+    level[y * game.level_w + x] = value;
+    if (level[y * game.level_w + x]) {
+        level[y * game.level_w + x].tile_x = x;
+        level[y * game.level_w + x].tile_y = y;
+    }
+}
+
+function place_random_tile(x, y) {
+    let color = Math.floor(Math.random() * n_colors);
+    let suit = Math.floor(Math.random() * NSUITS);
+
+    if (suit === EVIL_SUIT && (Math.random() < 0.7 || game_just_started)
+            || suit === WILD_SUIT && Math.random() < 0.4) {
+        do {
+            suit = Math.floor(Math.random() * NSUITS);
+        } while (suit === EVIL_SUIT || suit === WILD_SUIT);
+    }
+
+    set_tile_at(x, y, {
+        color: color,
+        suit: suit,
+        delay: TILE_DELAY_PER_ROW * x + TILE_DELAY_PER_COL * (BOARD_H - y - 1),
+        tile_x: x,
+        tile_y: y,
+        x: x * TILE_W,
+        y: -game.screen_h * 0.75,
+        vy: 0,
+    });
+}
+
+/* -------- game phase functions ------------ */
+
+function initialize() {
+    game_just_started = true;
+    n_colors = 2;
+    timer = MAX_TIMER;
+    game.state = State.DESTROY;
+    for (let y = 0; y < BOARD_H; y++) {
+        for (let x = 0; x < BOARD_H; x++) {
+            place_random_tile(x, y);
+        }
+    }
+}
+
+function check_high_score() {
+    if (displayed_score > high_score) {
+        high_score = displayed_score;
+        game.save('high_score', high_score);
+    }
+}
+
+function reset_save() {
+    game.save('high_score', 0);
+}
+
+function win() {
+}
+
+function game_over() {
+    console.log("Game over... :(");
+    game.music.bgm.pause();
+    game.sfx.gameover.play();
+    game.state = State.GAMEOVER;
+}
+
+/* ---------- update functions ------------ */
+
+function get_score_threshold(n_colors) {
+    return Math.floor(Math.pow(n_colors - 1, 1.7) * 5) * 1000 - 2000;
+}
+
+for (let n_colors = 2; n_colors <= MAX_N_COLORS; n_colors ++) {
+    console.log("Score threshold: ", n_colors, get_score_threshold(n_colors));
+}
+
+/* MAIN UPDATE FUNCTION */
+function do_update(delta) {
+    if (game.state === State.LIGHTNING) {
+        game.canvas.style.cursor = 'none';
+    } else {
+        game.canvas.style.cursor = 'default';
+    }
+
+    if (displayed_score < score * 100) {
+        displayed_score += Math.max(700, Math.floor(Math.abs(displayed_score - score * 100) / 30));
+        if (displayed_score > score * 100) {
+            displayed_score = score * 100;
+        }
+
+        check_high_score();
+
+        high_score = Math.max(high_score, displayed_score);
+    }
+
+    if (score > get_score_threshold(n_colors)) {
+        n_colors ++;
+        console.log("advance to ", n_colors);
+        if (n_colors > MAX_N_COLORS) {
+            n_colors = MAX_N_COLORS;
+        }
+    }
+
+    for (let p of lightning_particles) {
+        p.x = p.x * 0.9 + p.target_x * 0.1;
+        p.y = p.y * 0.9 + p.target_y * 0.1;
+        if (Math.abs(p.x - p.target_x) < 1 && Math.abs(p.y - p.target_y) < 1) {
+            if (p.increases) {
+                lightnings ++;
+                game.buttons.lightning.buttons[0].disabled = false;
+                if (lightnings > MAX_LIGHTNINGS) {
+                    lightnings = MAX_LIGHTNINGS;
+                }
+            }
+            p.deleteme = true;
+        }
+    }
+
+    lightning_particles = lightning_particles.filter(p => !p.deleteme);
+
+    if (game.state === State.STAND) {
+        if (timer_running) {
+            timer -= delta;
+        }
+
+        if (timer <= 0) {
+            timer = 0;
+            game_over();
+        }
+
+        if (lightnings > 0) {
+            game.buttons.lightning.buttons[0].disabled = false;
+        } else {
+            game.buttons.lightning.buttons[0].disabled = true;
+        }
+    } else if (game.state === State.REPLACE) {
+        let complete_tile_count = 0;
+        thud_timeout -= delta;
+
+        for (let t of level) {
+            if (t == null) continue;
+
+            if (t.delay > 0) {
+                t.delay -= delta;
+                continue;
+            }
+
+            t.vy += GRAVITY * delta / 1000;
+            t.y += t.vy * delta / 1000;
+
+            if (t.y >= t.tile_y * TILE_H) {
+                if (!t.done && thud_timeout <= 0) {
+                    game.sfx.thud.play();
+                    thud_timeout = THUD_SPACING;
+                }
+                t.y = t.tile_y * TILE_H;
+                if (t.suit === EVIL_SUIT && t.tile_y === BOARD_H - 1) {
+                    game.state = State.DESTROY;
+                } else {
+                    t.vy = 0;
+                    t.done = true;
+                    complete_tile_count ++;
+                }
+            }
+        }
+
+        if (complete_tile_count === BOARD_W * BOARD_H && lightning_particles.length === 0) {
+            game.state = State.STAND;
+            if (!game.music.bgm.playing) game.music.bgm.play();
+            selector.x = -1;
+            selector.y = -1;
+            game_just_started = false;
+            show_title = false;
+        }
+    } else if (game.state === State.DESTROY) {
+        let score_increase_amount = 20;
+        let time_increase_amount = TIME_GAIN_PER_BLOCK;
+
+        if (!game_just_started) {
+            timer_running = true;
+        }
+
+        if (path.length > 0) {
+            game.sfx.bap.play();
+        }
+
+        for (let p of path) {
+            set_tile_at(p[0], p[1], null);
+            score += score_increase_amount;
+            timer += time_increase_amount;
+            if (timer > MAX_TIMER) timer = MAX_TIMER;
+            score_increase_amount += 6;
+            time_increase_amount += TIME_INCREASE_PER_BLOCK;
+        }
+
+        path = [];
+
+        for (let x = 0; x < BOARD_W; x++) {
+            if (tile_at(x, BOARD_H - 1) !== null && tile_at(x, BOARD_H - 1).suit == EVIL_SUIT) {
+                set_tile_at(x, BOARD_H - 1, null);
+                if (!game_just_started) {
+                    game.sfx.ok.play();
+                    lightning_particles.push({
+                        x: board_offset_x + x * TILE_W + TILE_W / 2,
+                        y: board_offset_y + (BOARD_H - 1) * TILE_H + TILE_H / 2,
+                        target_x: game.buttons.lightning.buttons[0].x + 10,
+                        target_y: game.buttons.lightning.buttons[0].y + 8,
+                        increases: true,
+                    });
+                    score += 1000;
+                }
+            }
+        }
+
+        let min_delay = 10000;
+        let created = [];
+
+        for (let y = BOARD_H - 1; y >= 0; y--) {
+            for (let x = 0; x < BOARD_W; x++) {
+                if (tile_at(x, y) === null) {
+                    /* find some thing above us to fall down */
+                    let found = false;
+                    for (let search_y = y - 1; search_y >= 0; search_y --) {
+                        let search_tile = tile_at(x, search_y);
+                        if (search_tile !== null) {
+                            search_tile.vy = 0;
+                            set_tile_at(x, y, search_tile);
+                            set_tile_at(x, search_y, null);
+                            found = true;
+                            break;
+                        }
+                    }
+
+                    /* if we found nothing, make something fall down from the sky */
+                    if (!found) {
+                        place_random_tile(x, y);
+                        created.push([x, y]);
+                        if (tile_at(x, y).delay < min_delay) {
+                            min_delay = tile_at(x, y).delay;
+                        }
+                    }
+                }
+            }
+        }
+
+        for (let p of created) {
+            tile_at(...p).delay -= min_delay;
+        }
+
+        game.state = State.REPLACE;
+        for (let t of level) {
+            if (t.tile_y * TILE_H > t.y) {
+                t.done = false;
+            }
+        }
+    }
+}
+
+function do_update_menu(delta) {
+    title_timer += delta;
+}
+
+/* ---------- draw functions ----------- */
+
+/* DRAW */
+function do_draw(ctx) {
+    ctx.save();
+
+    if (game.state === State.TITLE || show_title) {
+        zb.image_draw(ctx, game.img.title, board_offset_x + TILE_W * BOARD_W / 2 - game.img.title.width / 2, board_offset_y + TILE_H * BOARD_H / 2 - game.img.title.height / 2);
+
+        zb.buttons.draw(ctx, game.buttons.play);
+    }
+
+    draw_score(ctx);
+
+    draw_tiles(ctx);
+
+    draw_lightning(ctx);
+
+    draw_selector(ctx);
+
+    if (game.state === State.LIGHTNING) {
+        zb.sprite_draw(ctx, game.img.lightning, 9, 9, 0, 0, mouse_x - 4, mouse_y - 4);
+    }
+
+    if (game.state === State.GAMEOVER) {
+        zb.image_draw(ctx, game.img.gameover, board_offset_x + BOARD_W * TILE_W / 2 - game.img.gameover.width / 2, board_offset_y + BOARD_H * TILE_H / 2 - game.img.gameover.height / 2);
+
+        zb.buttons.draw(ctx, game.buttons.replay);
+    }
+
+    ctx.restore();
+}
+
+function draw_score_num(ctx, x, y, num) {
+    let render_count = num;
+    let col = 1;
+    let lb = game.buttons.lightning.buttons[0];
+    while (col <= 8 || render_count > 0) {
+        zb.sprite_draw(ctx, game.img.scorenumbers, 8, 8, render_count % 10, 0, x + (8 - col) * 8, y);
+        render_count = Math.floor(render_count / 10);
+        col ++;
+    }
+}
+
+function draw_score(ctx) {
+    let timebar_x = board_offset_x + BOARD_W * TILE_W / 2 - game.img.timebar_outside.width / 2 + 16;
+    let timebar_y = board_offset_y - 18 - game.img.timebar_outside.height / 2;
+
+    if (game.state === State.GAMEOVER || game.state === State.TITLE) {
+        draw_score_num(ctx, timebar_x, timebar_y + 2, high_score);
+
+        zb.sprite_draw(ctx, game.img.text, 32, 12, 0, 0, timebar_x - game.img.text.width, timebar_y + 2); // "best"
+    } else {
+        zb.image_draw(ctx, game.img.timebar_outside, timebar_x, timebar_y);
+        let timebar_width = Math.round((timer / MAX_TIMER) * game.img.timebar_inside.width);
+        zb.sprite_draw(ctx, game.img.timebar_inside, timebar_width, game.img.timebar_inside.height, 0, 0, timebar_x, timebar_y);
+
+        zb.sprite_draw(ctx, game.img.text, 32, 12, 0, 2, timebar_x - game.img.text.width, timebar_y + 2); // "time"
+    }
+
+    zb.sprite_draw(ctx, game.img.text, 32, 12, 0, 1, timebar_x - game.img.text.width, timebar_y - 10); // "score"
+
+    draw_score_num(ctx, timebar_x, timebar_y - 10, displayed_score);
+}
+
+function draw_tiles(ctx) {
+    ctx.save();
+
+    ctx.translate(board_offset_x, board_offset_y);
+
+    for (let t of level) {
+        if (t == null) continue;
+
+        zb.sprite_draw(ctx, game.img.tiles, TILE_W, TILE_H, t.suit, t.color, t.x, t.y);
+    }
+
+    ctx.restore();
+}
+
+function draw_selector(ctx) {
+    ctx.save();
+
+    ctx.translate(board_offset_x, board_offset_y);
+
+    if ((game.state === State.STAND || game.state === State.LIGHTNING) && selector.x !== -1 && selector.y !== -1) {
+        zb.sprite_draw(ctx, game.img.selector, 20, 20, 0, 0, selector.x * TILE_W - 2, selector.y * TILE_H - 2);
+    } else if (game.state === State.DRAG) {
+        let last_p = null;
+        for (let p of path) {
+            if (last_p === null) {
+                zb.sprite_draw(ctx, game.img.connector, 20, 20, 0, 0, p[0] * TILE_W - 2, p[1] * TILE_H - 2);
+            } else {
+                zb.sprite_draw(ctx, game.img.connector, 20, 20, 0, 0, p[0] * TILE_W - 2, p[1] * TILE_H - 2);
+            }
+            if (last_p) {
+                if (last_p[0] == p[0]) {
+                    for (let y = Math.min(last_p[1], p[1]) + 1; y <= Math.max(last_p[1], p[1]) - 1; y++) {
+                        zb.sprite_draw(ctx, game.img.connector, 20, 20, 2, 0, p[0] * TILE_W - 2, y * TILE_H - 2);
+                    }
+                } else if (last_p[1] == p[1]) {
+                    for (let x = Math.min(last_p[0], p[0]) + 1; x <= Math.max(last_p[0], p[0]) - 1; x++) {
+                        zb.sprite_draw(ctx, game.img.connector, 20, 20, 1, 0, x * TILE_W - 2, p[1] * TILE_H - 2);
+                    }
+                }
+            }
+            last_p = p;
+        }
+
+        last_p = null;
+        for (let p of path) {
+            if (last_p === null) {
+                zb.sprite_draw(ctx, game.img.connector, 20, 20, 0, 1, p[0] * TILE_W - 2, p[1] * TILE_H - 2);
+            } else {
+                zb.sprite_draw(ctx, game.img.connector, 20, 20, 0, 1, p[0] * TILE_W - 2, p[1] * TILE_H - 2);
+            }
+            if (last_p) {
+                if (last_p[0] == p[0]) {
+                    for (let y = Math.min(last_p[1], p[1]) + 1; y <= Math.max(last_p[1], p[1]) - 1; y++) {
+                        zb.sprite_draw(ctx, game.img.connector, 20, 20, 2, 1, p[0] * TILE_W - 2, y * TILE_H - 2);
+                    }
+                } else if (last_p[1] == p[1]) {
+                    for (let x = Math.min(last_p[0], p[0]) + 1; x <= Math.max(last_p[0], p[0]) - 1; x++) {
+                        zb.sprite_draw(ctx, game.img.connector, 20, 20, 1, 1, x * TILE_W - 2, p[1] * TILE_H - 2);
+                    }
+                }
+            }
+            last_p = p;
+        }
+    }
+
+    ctx.restore();
+}
+
+function draw_lightning(ctx) {
+    if (game.state !== State.GAMEOVER && game.state !== State.TITLE) {
+        zb.buttons.draw(ctx, game.buttons.lightning);
+
+        let render_count = lightnings;
+        let col = 1;
+        let lb = game.buttons.lightning.buttons[0];
+        while (render_count > 0 || col == 1) {
+            zb.sprite_draw(ctx, game.img.lightningnumbers, 5, 8, render_count % 10, 0, lb.x + game.buttons.lightning.button_w - 7 - 5 * col, lb.y + 4);
+            render_count = Math.floor(render_count / 10);
+            col ++;
+        }
+    }
+
+    for (let lp of lightning_particles) {
+        zb.sprite_draw(ctx, game.img.lightning, 9, 9, 0, 0, lp.x - 4, lp.y - 4);
+    }
+}
+
+function do_draw_menu(ctx) {
+    ctx.save();
+    ctx.translate(0, Math.sin(title_timer / TITLE_BOUNCE_LENGTH * 2 * Math.PI) * TITLE_BOUNCE_DISTANCE);
+    zb.screen_draw(ctx, game.img.title);
+    ctx.restore();
+
+    zb.screen_draw(ctx, game.img.titlegraphic);
+    zb.buttons.draw(ctx, game.buttons.menu);
+}
+
+function do_draw_youwon(ctx) {
+    zb.screen_draw(ctx, game.img.youwin);
+}
+
+/* ---------- event handlers ------------ */
+
+function handle_gamestart(game) {
+    console.log("Game start!");
+
+    game.state = State.TITLE;
+
+    //game.music.bgm_menu.play();
+}
+
+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_mousedown(game, e, x, y) {
+    if (game.state === State.TITLE) {
+        if (zb.buttons.click(game.buttons.play, x, y)) return;
+    } else if (game.state === State.GAMEOVER) {
+        if (zb.buttons.click(game.buttons.replay, x, y)) return;
+    } else {
+        if (zb.buttons.click(game.buttons.lightning, x, y)) return;
+
+        if (game.state === State.STAND) {
+            if (selector.x !== -1 && selector.y !== -1) {
+                path = [ [selector.x, selector.y] ];
+                game.state = State.DRAG;
+            }
+        } else if (game.state === State.LIGHTNING) {
+            if (selector.x !== -1 && selector.y !== -1) {
+                clicked_once_lightning = false;
+                let destroy_tile = tile_at(selector.x, selector.y);
+                let destroy_suit = destroy_tile.suit;
+                let destroy_color = destroy_tile.color;
+
+                if (destroy_suit === EVIL_SUIT) return;
+
+                lightnings --;
+
+                let cx = board_offset_x + selector.x * TILE_W + TILE_W / 2;
+                let cy = board_offset_y + selector.y * TILE_W + TILE_W / 2;
+                game.sfx.lightning.play();
+                for (let i = 0; i < 10; i++) {
+                    lightning_particles.push({
+                        x: cx,
+                        y: cy,
+                        target_x: cx + 50 * Math.cos(i * (2 * Math.PI / 10)),
+                        target_y: cy + 50 * Math.sin(i * (2 * Math.PI / 10)),
+                        increases: false,
+                    });
+                }
+
+                for (let x = 0; x < BOARD_W; x ++) {
+                    for (let y = 0; y < BOARD_H; y ++) {
+                        let tile = tile_at(x, y);
+                        if (tile.suit === destroy_suit && tile.color === destroy_color || tile.suit === WILD_SUIT && destroy_suit === WILD_SUIT) {
+                            set_tile_at(x, y, null);
+                            if (destroy_suit === WILD_SUIT) {
+                                score += 400;
+                            } else {
+                                score += 25;
+                            }
+                        }
+                    }
+                }
+                game.state = State.DESTROY;
+            }
+        }
+    }
+}
+
+function handle_mousemove(game, e, x, y) {
+    mouse_x = x;
+    mouse_y = y;
+
+    if (game.state === State.TITLE) {
+        zb.buttons.update(game.buttons.play, x, y);
+    } else if (game.state === State.GAMEOVER) {
+        zb.buttons.update(game.buttons.replay, x, y);
+    } else {
+        zb.buttons.update(game.buttons.lightning, x, y);
+
+        let tile_x = -1, tile_y = -1;
+        if (x >= board_offset_x && x < board_offset_x + TILE_W * BOARD_W
+                && y >= board_offset_y && y < board_offset_y + TILE_H * BOARD_H) {
+            tile_x = Math.floor((x - board_offset_x) / TILE_W);
+            tile_y = Math.floor((y - board_offset_y) / TILE_H);
+        }
+
+        if (game.state === State.STAND) {
+            if (tile_x !== -1 && tile_y !== -1 && tile_at(tile_x, tile_y).suit !== EVIL_SUIT) {
+                selector.x = tile_x;
+                selector.y = tile_y;
+            } else {
+                selector.x = -1;
+                selector.y = -1;
+            }
+        } else if (game.state === State.DRAG) {
+            if (tile_x === -1 || tile_y === -1) return;
+
+            let last_path_coords = path[path.length - 1];
+            let second_last_path_coords = null;
+            if (path.length >= 2) {
+                second_last_path_coords = path[path.length - 2];
+            }
+
+            let already_in_path = path.findIndex(p => p[0] === tile_x && p[1] === tile_y);
+
+            if (already_in_path === -1) {
+                if (tile_x === last_path_coords[0] && Math.abs(tile_y - last_path_coords[1]) === 1) {
+                    let ydir = 1;
+                    if (tile_y < last_path_coords[1]) ydir = -1;
+                    for (let y = last_path_coords[1] + ydir; y != tile_y + ydir; y += ydir) {
+                        last_path_coords = path[path.length - 1];
+                        let end_of_path_tile = tile_at(last_path_coords[0], last_path_coords[1]);
+                        let hover_tile = tile_at(tile_x, y);
+
+                        if (hover_tile.suit !== EVIL_SUIT &&
+                            (hover_tile.suit === end_of_path_tile.suit || hover_tile.color === end_of_path_tile.color
+                                || hover_tile.suit === WILD_SUIT || end_of_path_tile.suit === WILD_SUIT)) {
+                            if (Math.abs(tile_x - last_path_coords[0]) > 0 || Math.abs(y - last_path_coords[1]) > 0) {
+                                path.push([tile_x, y]);
+                            }
+                        }
+                    }
+                } else if (Math.abs(tile_x - last_path_coords[0]) === 1 && tile_y === last_path_coords[1]) {
+                    let xdir = 1;
+                    if (tile_x < last_path_coords[0]) xdir = -1;
+                    for (let x = last_path_coords[0] + xdir; x != tile_x + xdir; x += xdir) {
+                        last_path_coords = path[path.length - 1];
+                        let end_of_path_tile = tile_at(last_path_coords[0], last_path_coords[1]);
+                        let hover_tile = tile_at(x, tile_y);
+
+                        if (hover_tile.suit !== EVIL_SUIT &&
+                            (hover_tile.suit === end_of_path_tile.suit || hover_tile.color === end_of_path_tile.color
+                                || hover_tile.suit === WILD_SUIT || end_of_path_tile.suit === WILD_SUIT)) {
+                            if (Math.abs(x - last_path_coords[0]) > 0 || Math.abs(tile_y - last_path_coords[1]) > 0) {
+                                path.push([x, tile_y]);
+                            }
+                        }
+                    }
+                }
+            } else {
+                path.length = already_in_path + 1;
+            }
+        } else if (game.state === State.LIGHTNING) {
+            if (tile_x !== -1 && tile_y !== -1) {
+                selector.x = tile_x;
+                selector.y = tile_y;
+            } else {
+                selector.x = -1;
+                selector.y = -1;
+            }
+        }
+    }
+}
+
+let clicked_once_lightning = false;
+
+function handle_mouseup(game, e, x, y) {
+    if (game.state === State.TITLE) {
+        zb.buttons.update(game.buttons.play, x, y);
+    } else if (game.state === State.GAMEOVER) {
+        zb.buttons.update(game.buttons.replay, x, y);
+    } else if (game.mode === 'youwon') {
+        game.long_transition(zb.transition.type.fade, 1000, function() {
+            game.mode = 'menu';
+            continue_level = 1;
+        });
+    } else {
+        zb.buttons.update(game.buttons.lightning, x, y);
+
+        let tile_x = -1, tile_y = -1;
+        if (x >= board_offset_x && x < board_offset_x + TILE_W * BOARD_W
+                && y >= board_offset_y && y < board_offset_y + TILE_H * BOARD_H) {
+            tile_x = Math.floor((x - board_offset_x) / TILE_W);
+            tile_y = Math.floor((y - board_offset_y) / TILE_H);
+        }
+
+        if (game.state === State.DRAG) {
+            let start_of_path_tile = tile_at(path[0][0], path[0][1]);
+            let end_of_path_tile = tile_at(path[path.length - 1][0], path[path.length - 1][1]);
+            if (path.length > 1 && start_of_path_tile.suit === end_of_path_tile.suit && start_of_path_tile.color === end_of_path_tile.color && start_of_path_tile.suit !== WILD_SUIT) {
+                game.state = State.DESTROY;
+            } else {
+                game.state = State.STAND;
+                path = [];
+
+                if (tile_x !== -1 && tile_y !== -1) {
+                    selector.x = tile_x;
+                    selector.y = tile_y;
+                } else {
+                    selector.x = -1;
+                    selector.y = -1;
+                }
+            }
+        } else if (game.state === State.LIGHTNING) {
+            if (tile_x !== -1 && tile_y !== -1) {
+                clicked_once_lightning = false;
+            } else {
+                if (!clicked_once_lightning) {
+                    clicked_once_lightning = true;
+                    return;
+                } else {
+                    clicked_once_lightning = false;
+                    game.state = State.STAND;
+                    game.buttons.lightning.buttons[0].disabled = false;
+                    selector.x = -1;
+                    selector.y = -1;
+                }
+            }
+        }
+    }
+}
diff --git a/img/connector.png b/img/connector.png
new file mode 100644 (file)
index 0000000..42e2bb0
Binary files /dev/null and b/img/connector.png differ
diff --git a/img/gameover.png b/img/gameover.png
new file mode 100644 (file)
index 0000000..bbb2c2c
Binary files /dev/null and b/img/gameover.png differ
diff --git a/img/lightning.png b/img/lightning.png
new file mode 100644 (file)
index 0000000..2d46b0a
Binary files /dev/null and b/img/lightning.png differ
diff --git a/img/lightningbutton.png b/img/lightningbutton.png
new file mode 100644 (file)
index 0000000..69a61f8
Binary files /dev/null and b/img/lightningbutton.png differ
diff --git a/img/lightningnumbers.png b/img/lightningnumbers.png
new file mode 100644 (file)
index 0000000..b328611
Binary files /dev/null and b/img/lightningnumbers.png differ
diff --git a/img/playbutton.png b/img/playbutton.png
new file mode 100644 (file)
index 0000000..a5cbc3a
Binary files /dev/null and b/img/playbutton.png differ
diff --git a/img/replaybutton.png b/img/replaybutton.png
new file mode 100644 (file)
index 0000000..4c60293
Binary files /dev/null and b/img/replaybutton.png differ
diff --git a/img/scorenumbers.png b/img/scorenumbers.png
new file mode 100644 (file)
index 0000000..4054f59
Binary files /dev/null and b/img/scorenumbers.png differ
diff --git a/img/selector.png b/img/selector.png
new file mode 100644 (file)
index 0000000..1cfc563
Binary files /dev/null and b/img/selector.png differ
diff --git a/img/text.png b/img/text.png
new file mode 100644 (file)
index 0000000..ae65437
Binary files /dev/null and b/img/text.png differ
diff --git a/img/tiles.png b/img/tiles.png
new file mode 100644 (file)
index 0000000..9bf36b9
Binary files /dev/null and b/img/tiles.png differ
diff --git a/img/timebar_inside.png b/img/timebar_inside.png
new file mode 100644 (file)
index 0000000..ed31043
Binary files /dev/null and b/img/timebar_inside.png differ
diff --git a/img/timebar_outside.png b/img/timebar_outside.png
new file mode 100644 (file)
index 0000000..c93bdc3
Binary files /dev/null and b/img/timebar_outside.png differ
diff --git a/img/title.png b/img/title.png
new file mode 100644 (file)
index 0000000..50e4487
Binary files /dev/null and b/img/title.png differ
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..9d80167
--- /dev/null
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head><title>
+    suit yourself
+</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="486" height="756" 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/loading.png b/loading.png
new file mode 100644 (file)
index 0000000..871c7b4
Binary files /dev/null and b/loading.png differ
diff --git a/music/suitgamesong.mp3 b/music/suitgamesong.mp3
new file mode 100644 (file)
index 0000000..df90da8
Binary files /dev/null and b/music/suitgamesong.mp3 differ
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..97f8900
Binary files /dev/null and b/progressbar.png differ
diff --git a/saveerror.png b/saveerror.png
new file mode 100644 (file)
index 0000000..fd1f8e7
Binary files /dev/null and b/saveerror.png differ
diff --git a/sfx/bap.wav b/sfx/bap.wav
new file mode 100644 (file)
index 0000000..349c45f
Binary files /dev/null and b/sfx/bap.wav differ
diff --git a/sfx/gameover.mp3 b/sfx/gameover.mp3
new file mode 100644 (file)
index 0000000..047cf2b
Binary files /dev/null and b/sfx/gameover.mp3 differ
diff --git a/sfx/lightning.wav b/sfx/lightning.wav
new file mode 100644 (file)
index 0000000..3a3e3d3
Binary files /dev/null and b/sfx/lightning.wav differ
diff --git a/sfx/ok.wav b/sfx/ok.wav
new file mode 100644 (file)
index 0000000..44c0008
Binary files /dev/null and b/sfx/ok.wav differ
diff --git a/sfx/thud.wav b/sfx/thud.wav
new file mode 100644 (file)
index 0000000..b67e836
Binary files /dev/null and b/sfx/thud.wav differ
diff --git a/zucchinibread.js b/zucchinibread.js
new file mode 100644 (file)
index 0000000..f46ddc6
--- /dev/null
@@ -0,0 +1,1349 @@
+"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;
+    }
+
+    function rand_int(a, b) {
+    }
+
+    /* ---- 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;
+        } else if (_last_frame_time == 0) {
+            _last_frame_time = timestamp;
+        } else {
+            _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.fillRect(0, 0, game.screen_w, game.screen_h);
+
+        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.fillRect(0, 0, game.screen_w * game.draw_scale, game.screen_h * game.draw_scale);
+
+        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,
+            Math.round(section_w * section_x), Math.round(section_h * section_y), section_w, section_h,
+            Math.round(dest_x), Math.round(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 an image at a specified position */
+    function image_draw(ctx, img, dest_x, dest_y) {
+        ctx.drawImage(img, Math.round(dest_x), Math.round(dest_y));
+    }
+
+    /* 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;
+    })();
+
+    /* ---- direction objects ---- */
+    const _dir_up = { x: 0, y: -1, index: 0, rotate: 0, horizontal: false, vertical: true };
+    const _dir_right = { x: 1, y: 0, index: 1, rotate: Math.PI/2, horizontal: true, vertical: false };
+    const _dir_down = { x: 0, y: 1, index: 2, rotate: Math.PI, horizontal: false, vertical: true };
+    const _dir_left = { x: -1, y: 0, index: 3, rotate: 3*Math.PI/2, horizontal: true, vertical: false };
+    _dir_up.opposite = _dir_down;
+    _dir_down.opposite = _dir_up;
+    _dir_right.opposite = _dir_left;
+    _dir_left.opposite = _dir_right;
+
+    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,
+        image_draw: image_draw,
+
+        buttons: {
+            create: create_buttons,
+            update: update_buttons,
+            click: click_button,
+            draw: button_draw,
+        },
+
+        dir: {
+            up: _dir_up,
+            down: _dir_down,
+            left: _dir_left,
+            right: _dir_right,
+        },
+
+        phi: (1 + Math.sqrt(5)) / 2,
+
+        transition: {
+            type: {
+                fade: _draw_fade_transition,
+                slide_up: _draw_slide_up_transition,
+                slide_down: _draw_slide_down_transition,
+            },
+        },
+    }
+})();