zucchinibread.js 2.0... master
authorcassowarii <2374677+cassowarii@users.noreply.github.com>
Sat, 13 Jun 2026 22:13:57 +0000 (15:13 -0700)
committercassowarii <2374677+cassowarii@users.noreply.github.com>
Sat, 13 Jun 2026 22:13:57 +0000 (15:13 -0700)
cover.png [deleted file]
game.js
pause.png [deleted file]
zucchinibread.js

diff --git a/cover.png b/cover.png
deleted file mode 100644 (file)
index 5026f84..0000000
Binary files a/cover.png and /dev/null differ
diff --git a/game.js b/game.js
index d22e86e..e9e9957 100644 (file)
--- a/game.js
+++ b/game.js
@@ -1,8 +1,6 @@
 "use strict";
 
-let game;
-
-/* --------- definitions ---------- */
+/* --------- setup ---------- */
 
 /* state:
  * TITLE: title screen
@@ -15,23 +13,16 @@ let game;
  */
 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({
+zb.ready(() => {
+    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: {
@@ -41,13 +32,6 @@ zb.ready(function() {
             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,
@@ -55,9 +39,8 @@ zb.ready(function() {
         },
         mode: 'game',
         buttons: {},
-    });
-
-    game.register_images({
+    })
+    .register_images({
         connector: 'img/connector.png',
         gameover: 'img/gameover.png',
         lightning: 'img/lightning.png',
@@ -72,9 +55,8 @@ zb.ready(function() {
         timebar_inside: 'img/timebar_inside.png',
         timebar_outside: 'img/timebar_outside.png',
         title: 'img/title.png',
-    });
-
-    game.register_sfx({
+    })
+    .register_sfx({
         ok: {
             path: 'sfx/ok.wav',
             volume: 0.7,
@@ -96,102 +78,92 @@ zb.ready(function() {
             path: 'sfx/gameover.mp3',
             volume: 0.3,
         },
-    });
-
-    game.register_music({
+    })
+    .register_music({
         bgm: {
-            path: 'music/suitgamesong',
+            path: 'music/suitgamesong.mp3',
             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;
-                    }
+    })
+    .resources_ready()
+    .setup(() => {
+        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.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.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.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();
+        game.transition.color = game.background_color;
+    });
 });
 
 /* ------ relevant constant types --------- */
@@ -552,172 +524,156 @@ function do_update_menu(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);
+function do_draw() {
+    draw.scoped(() => {
+        if (game.state === State.TITLE || show_title) {
+            draw.image(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);
+
+            draw.buttons(game.buttons.play);
+        }
 
-    draw_tiles(ctx);
+        draw_score();
 
-    draw_lightning(ctx);
+        draw_tiles();
 
-    draw_selector(ctx);
+        draw_lightning();
 
-    if (game.state === State.LIGHTNING) {
-        zb.sprite_draw(ctx, game.img.lightning, 9, 9, 0, 0, mouse_x - 4, mouse_y - 4);
-    }
+        draw_selector();
 
-    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);
+        if (game.state === State.LIGHTNING) {
+            draw.sprite(game.img.lightning, 9, 9, 0, 0, mouse_x - 4, mouse_y - 4);
+        }
 
-        zb.buttons.draw(ctx, game.buttons.replay);
-    }
+        if (game.state === State.GAMEOVER) {
+            draw.image(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);
 
-    ctx.restore();
+            draw.buttons(game.buttons.replay);
+        }
+    });
 }
 
-function draw_score_num(ctx, x, y, num) {
+function draw_score_num(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);
+        draw.sprite(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) {
+function draw_score() {
     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);
+        draw_score_num(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"
+        draw.sprite(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);
+        draw.image(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);
+        draw.sprite(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"
+        draw.sprite(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.sprite(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);
+    draw_score_num(timebar_x, timebar_y - 10, displayed_score);
 }
 
-function draw_tiles(ctx) {
-    ctx.save();
+function draw_tiles() {
+    draw.scoped(() => {
+        draw.translate(board_offset_x, board_offset_y);
 
-    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);
-    }
+        for (let t of level) {
+            if (t == null) continue;
 
-    ctx.restore();
+            draw.sprite(game.img.tiles, TILE_W, TILE_H, t.suit, t.color, t.x, t.y);
+        }
+    });
 }
 
-function draw_selector(ctx) {
-    ctx.save();
-
-    ctx.translate(board_offset_x, board_offset_y);
+function draw_selector() {
+    draw.scoped(() => {
+        draw.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);
+        if ((game.state === State.STAND || game.state === State.LIGHTNING) && selector.x !== -1 && selector.y !== -1) {
+            draw.sprite(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) {
+                    draw.sprite(game.img.connector, 20, 20, 0, 0, p[0] * TILE_W - 2, p[1] * TILE_H - 2);
+                } else {
+                    draw.sprite(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++) {
+                            draw.sprite(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++) {
+                            draw.sprite(game.img.connector, 20, 20, 1, 0, x * TILE_W - 2, p[1] * TILE_H - 2);
+                        }
                     }
                 }
+                last_p = p;
             }
-            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 = null;
+            for (let p of path) {
+                if (last_p === null) {
+                    draw.sprite(game.img.connector, 20, 20, 0, 1, p[0] * TILE_W - 2, p[1] * TILE_H - 2);
+                } else {
+                    draw.sprite(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++) {
+                            draw.sprite(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++) {
+                            draw.sprite(game.img.connector, 20, 20, 1, 1, x * TILE_W - 2, p[1] * TILE_H - 2);
+                        }
                     }
                 }
+                last_p = p;
             }
-            last_p = p;
         }
-    }
-
-    ctx.restore();
+    });
 }
 
-function draw_lightning(ctx) {
+function draw_lightning() {
     if (game.state !== State.GAMEOVER && game.state !== State.TITLE) {
-        zb.buttons.draw(ctx, game.buttons.lightning);
+        draw.buttons(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);
+            draw.sprite(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);
+        draw.sprite(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) {
+function handle_gamestart() {
     console.log("Game start!");
 
     game.state = State.TITLE;
@@ -734,7 +690,7 @@ function toggle_mute_button() {
     game.toggle_mute();
 }
 
-function handle_mousedown(game, e, x, y) {
+function handle_mousedown(e, x, y) {
     if (game.state === State.TITLE) {
         if (zb.buttons.click(game.buttons.play, x, y)) return;
     } else if (game.state === State.GAMEOVER) {
@@ -790,7 +746,7 @@ function handle_mousedown(game, e, x, y) {
     }
 }
 
-function handle_mousemove(game, e, x, y) {
+function handle_mousemove(e, x, y) {
     mouse_x = x;
     mouse_y = y;
 
@@ -878,16 +834,11 @@ function handle_mousemove(game, e, x, y) {
 
 let clicked_once_lightning = false;
 
-function handle_mouseup(game, e, x, y) {
+function handle_mouseup(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);
 
diff --git a/pause.png b/pause.png
deleted file mode 100644 (file)
index 7479e89..0000000
Binary files a/pause.png and /dev/null differ
index f46ddc6..6c399cf 100644 (file)
@@ -1,7 +1,7 @@
 "use strict";
 
-/* Please forgive my strictly procedural/functional style here.
- * Thanks for reading though! */
+/* There are programming patterns in here that no one should ever have discovered.
+ * Proceed at your own risk. */
 
 let zb = (function() {
     /* ---- Util ---- */
@@ -47,6 +47,53 @@ let zb = (function() {
     }
 
     function rand_int(a, b) {
+        let min = 0;
+        let max = 0;
+        if (b === undefined) {
+            max = a;
+        } else {
+            min = a;
+            max = b;
+        }
+        return Math.floor(Math.random() * (max - min) + min);
+    }
+
+    /* ---- Interface to game code ---- */
+
+    function implicit_bind(context, func) {
+        /* cursed and evil javascript magic */
+        /* lets us temporarily bind globals like dynamic scope,
+         * so we can pass in properties like 'game' to our functions
+         * without needing to call them directly */
+        /* this lets us namespace stuff under 'game' or 'draw'
+         * but also not have those namespaces actually accessible
+         * from the global scope. but they will be accessible
+         * in all child function calls! aah! */
+        let original_props = {}
+        for (let key of Object.keys(context)) {
+            original_props[key] = globalThis[key];
+            globalThis[key] = context[key];
+        }
+
+        let result = func();
+
+        for (let key of Object.keys(context)) {
+            globalThis[key] = original_props[key];
+        }
+
+        return result;
+    }
+
+    function call_game_func(game, func, args) {
+        return implicit_bind({ game: game }, () => {
+            func.apply(game, args);
+        });
+    }
+
+    function call_draw_func(game, draw, func, args) {
+        return implicit_bind({ game: game, draw: draw }, () => {
+            func.apply(game, args);
+        });
     }
 
     /* ---- Resource loading / loading screen ---- */
@@ -55,6 +102,30 @@ let zb = (function() {
 
     let _SFX_ARRAY_SIZE = 10;
 
+    /* Update progress bar if the progress bar is activated. */
+    function _update_progress_bar(game) {
+        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);
+                if (!game.img._progressbar.failed_to_load) {
+                    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();
+            }
+        }
+    }
+
     /* Returns a callback that should be called when a resource finishes loading. */
     function _register_resource(name, game, callback) {
         game._total_things_to_load ++;
@@ -63,24 +134,7 @@ let zb = (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();
-                    }
-                }
+                _update_progress_bar(game);
                 _check_if_loaded(game);
                 if (callback) {
                     callback();
@@ -89,16 +143,29 @@ let zb = (function() {
         }
     }
 
-    /* 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);
-            }
+    function _set_resource_load_handlers(game, resource, success_event, name, callback) {
+        /* Check if resource successfully loaded and update loading screen. */
+        resource.addEventListener(success_event, _register_resource(name, game, callback), false);
+
+        /* Check if resource failed to load bc file doesn't exist and print error message. */
+        resource.addEventListener('error', function(e) {
+            resource.failed_to_load = true;
+            game._things_loaded ++;
+            console.error("Error loading resource, skipping:", name);
+            _update_progress_bar(game);
+            _check_if_loaded(game);
+        }, false);
+    }
+
+    /* Call any queued setup functions in game context (after waiting for things to load, maybe)
+     * and clear the setup function queue */
+    function _call_setup_funcs(game) {
+        for (let setupfunc of game._setup_funcs) {
+            call_game_func(game, setupfunc, []);
         }
+
+        /* clear queue */
+        game._setup_funcs = [];
     }
 
     /* Check if the game has loaded everything and if so show the 'click to start' image */
@@ -108,6 +175,8 @@ let zb = (function() {
         if (game._things_loaded >= game._total_things_to_load) {
             console.log("Ready");
             game.ready_to_go = true;
+            game._waiting_for_resources = false;
+            _call_setup_funcs(game);
             game._on_ready();
         }
     }
@@ -117,11 +186,9 @@ let zb = (function() {
             let sfx_size = sfxdata[key].copies || _SFX_ARRAY_SIZE;
             let sfx_array = new Array(sfx_size);
             for (let i = 0; i < sfx_size; i++) {
+                let resource_name = sfxdata[key].path + '#' + (i+1);
                 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]);
+                _set_resource_load_handlers(game, sfx_array[i], 'canplaythrough', resource_name);
                 if (sfxdata[key].hasOwnProperty('volume')) {
                     sfx_array[i].volume = sfxdata[key].volume;
                 }
@@ -130,6 +197,8 @@ let zb = (function() {
                 _array: sfx_array,
                 _index: 0,
                 play: function() {
+                    if (this.failed_to_load) return;
+
                     if (!game.muted) {
                         this._array[this._index].currentTime = 0;
                         this._array[this._index].play();
@@ -139,19 +208,21 @@ let zb = (function() {
                 }
             }
         }
+
+        return game;
     }
 
     function _register_music(musicdata, game) {
         for (let key in musicdata) {
             let musicpath;
-            if (musicdata[key].path.match(/\.[a-z]{3}$/)) {
+            if (musicdata[key].path.match(/\.[a-z0-9]+$/i)) {
                 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 :(");
+                console.error("Browser knows how to play neither mp3 nor ogg :(");
                 return;
             }
             let music = new Audio(musicpath);
@@ -163,13 +234,15 @@ let zb = (function() {
                 /* 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);
+
+            _set_resource_load_handlers(game, music, 'canplaythrough', musicpath);
 
             /* Handle music changes when muted, so when we unmute,
              * the correct music will be playing. */
             music._og_play = music.play;
             music.play = function() {
+                if (music.failed_to_load) return;
+
                 if (game.muted) {
                     music.was_playing = true;
                 } else {
@@ -179,6 +252,8 @@ let zb = (function() {
 
             music._og_pause = music.pause;
             music.pause = function() {
+                if (music.failed_to_load) return;
+
                 if (game.muted) {
                     music.was_playing = false;
                 } else {
@@ -188,6 +263,8 @@ let zb = (function() {
 
             game.music[key] = music;
         }
+
+        return game;
     }
 
     function _register_images(imgdata, game) {
@@ -198,7 +275,7 @@ let zb = (function() {
                     result[key] = _recursive_load_images(pathmap[key]);
                 } else {
                     result[key] = new Image();
-                    result[key].onload = _register_resource(pathmap[key], game);
+                    _set_resource_load_handlers(game, result[key], 'load', pathmap[key]);
                     result[key].src = pathmap[key];
                 }
             }
@@ -209,6 +286,8 @@ let zb = (function() {
         for (let k in loaded_imgs) {
             game.img[k] = loaded_imgs[k];
         }
+
+        return game;
     }
 
     function _create_canvas_context(canvas) {
@@ -224,26 +303,75 @@ let zb = (function() {
         return new_ctx;
     }
 
-    function create_game(params) {
-        let game_props = {...params};
+    function _generate_draw_context(canvas_ctx) {
+        /* this generates the magic 'draw' objects that get passed in
+         * implicitly to the draw event functions. we can still get
+         * at the original canvas context by `draw.context`, but this
+         * way, what is getting drawn onto is implicitly passed with
+         * `implicit_bind`, so we don't have to remember to pass `ctx`
+         * to every drawing function (which i always forget) but we
+         * can still draw to the correct canvas for stuff like
+         * custom transitions */
+        return {
+            context: canvas_ctx,
+
+            image: ((function(ctx) {
+                return function(img, x, y) {
+                    image_draw(ctx, img, x, y);
+                };
+            })(canvas_ctx)),
 
-        game_props.frame_rate = params.frame_rate || 60;
+            screen: ((function(ctx) {
+                return function(img) {
+                    screen_draw(ctx);
+                };
+            })(canvas_ctx)),
 
-        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;
+            sprite: ((function(ctx) {
+                return function(img, section_w, section_h, section_x, section_y, dest_x, dest_y) {
+                    sprite_draw(ctx, img, section_w, section_h, section_x, section_y, dest_x, dest_y);
+                };
+            })(canvas_ctx)),
 
-        game_props.tile_size = params.tile_size || 16;
-        game_props.level_w = params.level_w || 10;
-        game_props.level_h = params.level_h || 7;
+            buttons: ((function(ctx) {
+                return function(button_data) {
+                    button_draw(ctx, button_data);
+                };
+            })(canvas_ctx)),
 
-        game_props.screen_w = game_props.canvas_w / game_props.draw_scale;
-        game_props.screen_h = game_props.canvas_h / game_props.draw_scale;
+            scoped: ((function(ctx) {
+                return function(scopefunc) {
+                    ctx.save();
+                    scopefunc();
+                    ctx.restore();
+                };
+            })(canvas_ctx)),
 
+            translate: ((function(ctx) {
+                return function(x, y) {
+                    ctx.translate(Math.round(x), Math.round(y));
+                };
+            })(canvas_ctx)),
+
+            rotate: ((function(ctx) {
+                return function(radians) {
+                    ctx.rotate(radians);
+                };
+            })(canvas_ctx)),
+
+            rotate_deg: ((function(ctx) {
+                return function(degrees) {
+                    ctx.rotate(degrees * Math.PI / 180);
+                };
+            })(canvas_ctx)),
+        };
+    }
+
+    function create_game(params) {
+        let game_props = {...params};
+
+        game_props.frame_rate = params.frame_rate || 60;
+        game_props.draw_scale = params.draw_scale || 4;
         game_props.background_color = params.background_color || '#000000';
 
         let canvas = document.getElementById(game_props.canvas);
@@ -252,6 +380,7 @@ let zb = (function() {
         global_ctx.imageSmoothingEnabled = false;
         global_ctx.webkitImageSmoothingEnabled = false;
         global_ctx.mozImageSmoothingEnabled = false;
+        let global_draw = _generate_draw_context(global_ctx);
 
         let mask_canvas = document.createElement('canvas');
         mask_canvas.width = canvas.width;
@@ -260,6 +389,7 @@ let zb = (function() {
         mask_ctx.imageSmoothingEnabled = false;
         mask_ctx.webkitImageSmoothingEnabled = false;
         mask_ctx.mozImageSmoothingEnabled = false;
+        let mask_draw = _generate_draw_context(mask_ctx);
 
         let copy_canvas = document.createElement('canvas');
         copy_canvas.width = canvas.width;
@@ -268,6 +398,7 @@ let zb = (function() {
         copy_ctx.imageSmoothingEnabled = false;
         copy_ctx.webkitImageSmoothingEnabled = false;
         copy_ctx.mozImageSmoothingEnabled = false;
+        let copy_draw = _generate_draw_context(copy_ctx);
 
         let draw_canvas = document.createElement('canvas');
         draw_canvas.width = canvas.width;
@@ -276,8 +407,13 @@ let zb = (function() {
         draw_ctx.imageSmoothingEnabled = false;
         draw_ctx.webkitImageSmoothingEnabled = false;
         draw_ctx.mozImageSmoothingEnabled = false;
+        let draw_draw = _generate_draw_context(draw_ctx);
 
         let game = {
+            /* Defaults that can be overridden */
+            run_in_background: true,
+            load_with_progress_bar: true,
+
             /* General properties */
             ...game_props,
 
@@ -287,30 +423,61 @@ let zb = (function() {
             ready_to_go: false,
             _total_things_to_load: 1,
             _things_loaded: 0,
+            _waiting_for_resources: false,
+            _setup_funcs: [],
             resources_ready: function() {
                 this._things_loaded ++;
                 console.log("Finished enumerating resources to load. Things loaded:",
                     this._things_loaded, "/", this._total_things_to_load);
+                this._waiting_for_resources = true;
                 _check_if_loaded(this);
+                return 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();
+                if (this.img._clicktostart && this.img._clicktostart.complete) {
+                    this.ctx.global.save();
+                    this.ctx.global.scale(this.draw_scale, this.draw_scale);
+                    if (!this.img._clicktostart.failed_to_load) {
+                        this.ctx.global.drawImage(this.img._clicktostart, 0, 0);
+                    }
+                    this.ctx.global.restore();
                 }
             },
             playing: false,
             play: function() {
                 this.playing = true;
                 if (this.events.gamestart) {
-                    this.events.gamestart(this);
+                    call_game_func(this, this.events.gamestart, []);
                 }
                 _loop(this);
             },
             _norun: false,
+            setup: function(setupfunc) {
+                if (this._waiting_for_resources) {
+                    /* If we said resources_ready() before calling setup, then
+                     * the setup function will get queued until everything is
+                     * loaded. This allows us to let the setup function depend
+                     * on properties of the things that are loaded (eg image
+                     * sizes, etc) */
+                    /* TODO: It would be kind of neat to queue the other resource
+                     * loading functions as well if _waiting_for_resources is
+                     * set so that resources_ready() really always waits for
+                     * stuff to load before proceeding. But that might mess
+                     * with the loading screen, and I don't know why this would
+                     * be needed (maybe if somehow one image got loaded depending
+                     * on a property of another image?) so low priority.
+                     * Right now setup() is probably the only thing that depends
+                     * on waiting for loads to complete. */
+                    this._setup_funcs.push(setupfunc);
+                } else {
+                    /* If we aren't waiting for resources (ie either everything
+                     * already loaded, or we called setup() before calling
+                     * resources_ready()), just call the setupfunc directly */
+                    call_game_func(this, setupfunc, []);
+                }
+
+                return this;
+            },
 
             /* Audio */
             sfx: {},
@@ -330,10 +497,10 @@ let zb = (function() {
                 }
             },
             register_sfx: function(sfxdata) {
-                _register_sfx(sfxdata, this);
+                return _register_sfx(sfxdata, this);
             },
             register_music: function(musicdata) {
-                _register_music(musicdata, this);
+                return _register_music(musicdata, this);
             },
 
             /* Drawing */
@@ -343,13 +510,19 @@ let zb = (function() {
                 copy: copy_ctx,     /* context for copying the old screen on transition */
                 draw: draw_ctx,     /* context for drawing the real level */
             },
+            draw_context: {
+                global: global_draw,
+                mask: mask_draw,
+                copy: copy_draw,
+                draw: draw_draw,
+            },
             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);
+                return _register_images(imgdata, this);
             },
 
             /* Save */
@@ -389,12 +562,24 @@ let zb = (function() {
                 };
         })();
 
+        /* Calculate screen size */
+        if (!game.canvas_w) {
+            game.canvas_w = game.canvas.width;
+        }
+
+        if (!game.canvas_h) {
+            game.canvas_h = game.canvas.height;
+        }
+
+        game.screen_w = game.canvas_w / game.draw_scale;
+        game.screen_h = game.canvas_h / game.draw_scale;
+
         /* 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);
+                    call_game_func(game, params.events[ev], [e]);
                 }
             }
         }
@@ -444,14 +629,14 @@ let zb = (function() {
 
         canvas.onkeydown = function(e) {
             if (!game._norun && game.events.keydown) {
-                game.events.keydown(game, e);
+                call_game_func(game, game.events.keydown, [e]);
                 e.preventDefault();
             }
         }
 
         canvas.onkeyup = function(e) {
             if (!game._norun && game.events.keyup) {
-                game.events.keyup(game, e);
+                call_game_func(game, game.events.keyup, [e]);
                 e.preventDefault();
             }
         }
@@ -475,15 +660,18 @@ let zb = (function() {
 
         /* Set loading stuff */
         let loading_img = new Image();
-        loading_img.onload = _register_resource('loading.png', game, function() {
+        _set_resource_load_handlers(game, loading_img, 'load', 'loading.png', 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);
+                if (!loading_img.failed_to_load) {
+                    game.ctx.global.drawImage(loading_img, 0, 0);
+                }
                 game.ctx.global.restore();
             }
         });
         loading_img.src = 'loading.png';
+        game.img._loading = loading_img;
 
         if (game.load_with_progress_bar) {
             game._progressbar_img_loaded = false;
@@ -491,14 +679,16 @@ let zb = (function() {
             game._progressbar_height = 0;
 
             let progressbar_img = new Image();
-            progressbar_img.onload = _register_resource('progressbar.png', game, function() {
+            _set_resource_load_handlers(game, progressbar_img, 'load', 'progressbar.png', 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);
+                    if (!loading_img.failed_to_load) {
+                        game.ctx.global.drawImage(loading_img, 0, 0);
+                    }
                     game.ctx.global.restore();
                 }
             });
@@ -507,25 +697,21 @@ let zb = (function() {
         }
 
         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;
+            _register_images({ _pause: 'pause.png' }, game);
         }
 
         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;
+            _register_images({ _saveerror: 'saveerror.png' }, game);
         }
 
         let clicktostart_img = new Image();
-        clicktostart_img.onload = _register_resource('clicktostart.png', game, function() {
+        _set_resource_load_handlers(game, clicktostart_img, 'load', 'clicktostart.png', 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);
+                if (!game.img._clicktostart.failed_to_load) {
+                    game.ctx.global.drawImage(game.img._clicktostart, 0, 0);
+                }
                 game.ctx.global.restore();
             }
         });
@@ -578,15 +764,22 @@ let zb = (function() {
         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;
+        }
+        _timedelta += timestamp - _last_frame_time;
+        _last_frame_time = timestamp;
+
+        let times_updated = 0;
+        while (_timedelta >= 1000 / game.frame_rate) {
+            _update(game, 1000 / game.frame_rate);
+            _timedelta -= 1000 / game.frame_rate;
+
+            /* Draw at least once every 4 frames */
+            /* (also sometimes we get a really big delta e.g. if
+             * run_in_background: false, and we don't want to honor that) */
+            times_updated ++;
+            if (times_updated >= 4) {
+                _timedelta = 0;
+                break;
             }
         }
 
@@ -601,11 +794,11 @@ let zb = (function() {
 
     function _update(game, delta) {
         if (game.update_func) {
-            game.update_func(delta);
+            call_game_func(game, game.update_func, [delta]);
         }
 
         if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].update) {
-            game.modes[game.mode].update(delta);
+            call_game_func(game, game.modes[game.mode].update, [delta]);
         }
 
         if (game.transition.is_transitioning) {
@@ -670,7 +863,7 @@ let zb = (function() {
             game._clicked_button.state = 0;
             _draw(game);
             if (game._clicked_button.callback) {
-                game._clicked_button.callback();
+                call_game_func(game, game._clicked_button.callback, []);
             }
             game._clicked_button = null;
             return;
@@ -680,19 +873,21 @@ let zb = (function() {
     /* ---- Mouse ---- */
 
     function _handle_mousedown(game, e) {
-        if (!game.playing) return;
-
         game.touchmode = false;
 
+        if (!game.playing) return;
+
         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);
+            call_game_func(game, game.events.mousedown, [e, x, y]);
         }
     }
 
     function _handle_mouseup(game, e) {
+        game.touchmode = false;
+
         if (!game.playing && game.ready_to_go) {
             /* Click to start */
             if (game.hasOwnProperty('save_key') && !game._show_save_error) {
@@ -712,7 +907,9 @@ let zb = (function() {
                     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);
+                    if (!game.img._saveerror.failed_to_load) {
+                        game.ctx.global.drawImage(game.img._saveerror, 0, 0);
+                    }
                     game.ctx.global.restore();
                     return;
                 }
@@ -721,8 +918,6 @@ let zb = (function() {
             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;
@@ -730,18 +925,18 @@ let zb = (function() {
         _handle_clicked_button(game);
 
         if (e.button === 0 && game.events.mouseup) {
-            game.events.mouseup(game, e, x, y);
+            call_game_func(game, game.events.mouseup, [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);
+        if (game.playing && game.events.mousemove) {
+            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;
+            call_game_func(game, game.events.mousemove, [e, x, y]);
         }
     }
 
@@ -750,10 +945,10 @@ let zb = (function() {
     let _last_touch_event;
 
     function _handle_touchstart(game, e) {
-        if (!game.playing) return;
-
         game.touchmode = true;
 
+        if (!game.playing) return;
+
         if (e.touches) e = e.touches[0];
 
         _last_touch_event = e;
@@ -762,24 +957,24 @@ let zb = (function() {
         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);
+            call_game_func(game, game.events.touchstart, [e, x, y]);
         } else if (game.events.mousedown) {
             if (game.events.mousemove) {
-                game.events.mousemove(game, e, x, y);
+                call_game_func(game, game.events.mousemove, [e, x, y]);
             }
-            game.events.mousedown(game, e, x, y);
+            call_game_func(game, game.events.mousedown, [e, x, y]);
         }
     }
 
     function _handle_touchend(game, e) {
+        game.touchmode = true;
+
         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;
@@ -797,10 +992,10 @@ let zb = (function() {
     }
 
     function _handle_touchmove(game, e) {
-        if (!game.playing) return;
-
         game.touchmode = true;
 
+        if (!game.playing) return;
+
         if (e.touches) e = e.touches[0];
 
         _last_touch_event = e;
@@ -809,9 +1004,9 @@ let zb = (function() {
         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);
+            call_game_func(game, game.events.touchmove, [e, x, y]);
         } else if (game.events.mousemove) {
-            game.events.mousemove(game, e, x, y);
+            call_game_func(game, game.events.mousemove, [e, x, y]);
         }
     }
 
@@ -820,16 +1015,17 @@ let zb = (function() {
     function _draw(game) {
         game.ctx.draw.save();
 
+        game.ctx.draw.save();
         game.ctx.draw.fillStyle = game.background_color;
-
         game.ctx.draw.fillRect(0, 0, game.screen_w, game.screen_h);
+        game.ctx.draw.restore();
 
         if (game.draw_func) {
-            game.draw_func(game.ctx.draw);
+            call_draw_func(game, game.draw_context.draw, game.draw_func, []);
         }
 
         if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw) {
-            game.modes[game.mode].draw(game.ctx.draw);
+            call_draw_func(game, game.draw_context.draw, game.modes[game.mode].draw, []);
         }
 
         if (game.transition.mid_long) {
@@ -840,16 +1036,15 @@ let zb = (function() {
         game.ctx.draw.restore();
 
         if (game.transition.is_transitioning) {
-            _draw_transition(game, game.ctx.mask);
+            call_draw_func(game, game.draw_context.mask, _draw_transition, []);
         } 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);
+            call_draw_func(game, game.draw_context.mask, game.modes[game.mode].draw_after_transition, []);
         }
 
-
         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);
 
@@ -859,7 +1054,7 @@ let zb = (function() {
 
         game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0);
 
-        if (game._norun) {
+        if (game._norun && !game.img._pause.failed_to_load) {
             game.ctx.global.drawImage(game.img._pause, 0, 0);
         }
 
@@ -869,19 +1064,25 @@ let zb = (function() {
     /* 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)
+        if (!img.failed_to_load) {
+            ctx.drawImage(img,
+                section_w * section_x, 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);
+        if (!img.failed_to_load) {
+            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));
+        if (!img.failed_to_load) {
+            ctx.drawImage(img, Math.round(dest_x), Math.round(dest_y));
+        }
     }
 
     /* Draw a set of buttons. */
@@ -995,7 +1196,7 @@ let zb = (function() {
             let tdih = game.transition.dir_invert_h;
             _internal_start_transition(game, type, length, function() {
                 game.transition.mid_long = false;
-                callback();
+                call_game_func(game, callback, []);
                 game.transition.dir_invert_v = tdiv;
                 game.transition.dir_invert_h = tdih;
             });
@@ -1020,7 +1221,7 @@ let zb = (function() {
 
         game.ctx.copy.drawImage(game.ctx.draw.canvas, 0, 0);
 
-        callback();
+        call_game_func(game, callback, []);
 
         game.transition.is_transitioning = true;
     }
@@ -1031,283 +1232,67 @@ let zb = (function() {
 
         if (game.transition.done_func) {
             setTimeout(function() {
-                game.transition.done_func();
+                call_game_func(game, game.transition.done_func, []);
                 game.transition.done_func = null;
             }, 400);
         }
     }
 
-    function _draw_transition(game, ctx) {
+    function _draw_transition() {
         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();
+        draw.scoped(() => {
+            draw.context.clearRect(0, 0, game.screen_w, game.screen_h);
+            game.transition.type(game.ctx.copy.canvas, game.ctx.draw.canvas, frac, game.transition.invert_shape);
+        });
     }
 
-    function _draw_slide_down_transition(game, ctx, oldc, newc, frac, reverse) {
+    function _draw_slide_down_transition(from, to, frac, reverse) {
         let offset = frac * game.screen_h;
 
-        ctx.drawImage(oldc.canvas, 0, -offset);
-        ctx.drawImage(newc.canvas, 0, game.screen_h - offset);
+        draw.image(from, 0, -offset);
+        draw.image(to, 0, game.screen_h - offset);
     }
 
-    function _draw_slide_up_transition(game, ctx, oldc, newc, frac, reverse) {
+    function _draw_slide_up_transition(from, to, frac, reverse) {
         let offset = frac * game.screen_h;
 
-        ctx.drawImage(oldc.canvas, 0, offset);
-        ctx.drawImage(newc.canvas, 0, - game.screen_h + offset);
+        draw.image(from, 0, offset);
+        draw.image(to, 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);
+    function _draw_fade_transition(from, to, frac, reverse) {
+        draw.context.globalAlpha = 1;
+        draw.image(from, 0, 0);
+        draw.context.globalAlpha = frac;
+        draw.image(to, 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()
+    /* document.ready */
+    /* Apparently, we have had DOMContentLoaded widely available since 2015.
+     * I've been carrying around this ancient document.ready invocation in these games since 2017,
+     * when using scary new js features might break things, but now it's been almost ten years
+     * and it's not like the rest of the game code would work in IE8 or whatever. So here's the
+     * simple version of ready. */
+    function ready(ready_func) {
+        if (document.readyState === "loading") {
+            ready_func();
+        } else {
+            document.addEventListener("DOMContentLoaded", ready_func);
         }
-        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_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 };
+    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;
 
+    /* ---- export zb ---- */
     return {
         create_game: create_game,
 
@@ -1318,9 +1303,12 @@ let zb = (function() {
         copy_list: copy_list,
         copy_flat_objlist: copy_flat_objlist,
 
-        sprite_draw: sprite_draw,
-        screen_draw: screen_draw,
-        image_draw: image_draw,
+        draw: {
+            sprite: sprite_draw,
+            screen: screen_draw,
+            image: image_draw,
+            buttons: button_draw,
+        },
 
         buttons: {
             create: create_buttons,
@@ -1345,5 +1333,5 @@ let zb = (function() {
                 slide_down: _draw_slide_down_transition,
             },
         },
-    }
+    };
 })();