game
authorcassowarii <cassowarii@users.noreply.github.com>
Mon, 15 Apr 2024 01:11:23 +0000 (18:11 -0700)
committercassowarii <cassowarii@users.noreply.github.com>
Mon, 15 Apr 2024 01:11:23 +0000 (18:11 -0700)
140 files changed:
ase/beams.aseprite [new file with mode: 0644]
ase/lizwiz.aseprite [new file with mode: 0644]
ase/lizwiz.png [new file with mode: 0644]
ase/magic.aseprite [new file with mode: 0644]
ase/palette.aseprite [new file with mode: 0644]
ase/spin.gif [new file with mode: 0644]
ase/stones.aseprite [new file with mode: 0644]
ase/stones.gif [new file with mode: 0644]
ase/tiles.aseprite [new file with mode: 0644]
ase/tilesold.aseprite [new file with mode: 0644]
ase/titlescreen.aseprite [new file with mode: 0644]
ase/youdidit.aseprite [new file with mode: 0644]
clicktostart.png [new file with mode: 0644]
endscreen.aseprite [new file with mode: 0644]
game.js [new file with mode: 0644]
img/beams.png [new file with mode: 0644]
img/endscreen.png [new file with mode: 0644]
img/level/1.aseprite [new file with mode: 0644]
img/level/1.png [new file with mode: 0644]
img/level/10.aseprite [new file with mode: 0644]
img/level/10.png [new file with mode: 0644]
img/level/11.aseprite [new file with mode: 0644]
img/level/11.png [new file with mode: 0644]
img/level/12.aseprite [new file with mode: 0644]
img/level/12.png [new file with mode: 0644]
img/level/13.aseprite [new file with mode: 0644]
img/level/13.png [new file with mode: 0644]
img/level/14.aseprite [new file with mode: 0644]
img/level/14.png [new file with mode: 0644]
img/level/15.aseprite [new file with mode: 0644]
img/level/15.png [new file with mode: 0644]
img/level/2.aseprite [new file with mode: 0644]
img/level/2.png [new file with mode: 0644]
img/level/3.aseprite [new file with mode: 0644]
img/level/3.png [new file with mode: 0644]
img/level/4.aseprite [new file with mode: 0644]
img/level/4.png [new file with mode: 0644]
img/level/5.aseprite [new file with mode: 0644]
img/level/5.png [new file with mode: 0644]
img/level/6.aseprite [new file with mode: 0644]
img/level/6.png [new file with mode: 0644]
img/level/7.aseprite [new file with mode: 0644]
img/level/7.png [new file with mode: 0644]
img/level/8.aseprite [new file with mode: 0644]
img/level/8.png [new file with mode: 0644]
img/level/9.aseprite [new file with mode: 0644]
img/level/9.png [new file with mode: 0644]
img/lizwiz.png [new file with mode: 0644]
img/magic-sheet.png [new file with mode: 0644]
img/magic.png [new file with mode: 0644]
img/stones-sheet-sheet.png [new file with mode: 0644]
img/stones-sheet.png [new file with mode: 0644]
img/stones.png [new file with mode: 0644]
img/tiles-sheet-sheet.png [new file with mode: 0644]
img/tiles-sheet.png [new file with mode: 0644]
img/tiles.png [new file with mode: 0644]
img/titlescreen.png [new file with mode: 0644]
img/tmptile.png [new file with mode: 0644]
img/youdidit.png [new file with mode: 0644]
index.html [new file with mode: 0644]
levels.js [new file with mode: 0644]
levels/1.tmx [new file with mode: 0644]
levels/10.tmx [new file with mode: 0644]
levels/11.tmx [new file with mode: 0644]
levels/12.tmx [new file with mode: 0644]
levels/13.tmx [new file with mode: 0644]
levels/14.tmx [new file with mode: 0644]
levels/15.tmx [new file with mode: 0644]
levels/2.tmx [new file with mode: 0644]
levels/3.tmx [new file with mode: 0644]
levels/4.tmx [new file with mode: 0644]
levels/5.tmx [new file with mode: 0644]
levels/6.tmx [new file with mode: 0644]
levels/7.tmx [new file with mode: 0644]
levels/8.tmx [new file with mode: 0644]
levels/9.tmx [new file with mode: 0644]
levels/mapcompile.py [new file with mode: 0755]
levels/tmptile.png [new file with mode: 0644]
levels/tmptile.tsx [new file with mode: 0644]
lizardfont.ttf [new file with mode: 0644]
lizardfont.zip [new file with mode: 0644]
loading.png [new file with mode: 0644]
music/spacemagic.mp3 [new file with mode: 0644]
packaging.zip [new file with mode: 0644]
packaging/clicktostart.png [new file with mode: 0644]
packaging/game.js [new file with mode: 0644]
packaging/img/beams.png [new file with mode: 0644]
packaging/img/endscreen.png [new file with mode: 0644]
packaging/img/level/1.png [new file with mode: 0644]
packaging/img/level/10.png [new file with mode: 0644]
packaging/img/level/11.png [new file with mode: 0644]
packaging/img/level/12.png [new file with mode: 0644]
packaging/img/level/13.png [new file with mode: 0644]
packaging/img/level/14.png [new file with mode: 0644]
packaging/img/level/15.png [new file with mode: 0644]
packaging/img/level/2.png [new file with mode: 0644]
packaging/img/level/3.png [new file with mode: 0644]
packaging/img/level/4.png [new file with mode: 0644]
packaging/img/level/5.png [new file with mode: 0644]
packaging/img/level/6.png [new file with mode: 0644]
packaging/img/level/7.png [new file with mode: 0644]
packaging/img/level/8.png [new file with mode: 0644]
packaging/img/level/9.png [new file with mode: 0644]
packaging/img/lizwiz.png [new file with mode: 0644]
packaging/img/magic.png [new file with mode: 0644]
packaging/img/stones.png [new file with mode: 0644]
packaging/img/tiles.png [new file with mode: 0644]
packaging/img/titlescreen.png [new file with mode: 0644]
packaging/img/youdidit.png [new file with mode: 0644]
packaging/index.html [new file with mode: 0644]
packaging/levels.js [new file with mode: 0644]
packaging/license.txt [new file with mode: 0644]
packaging/loading.png [new file with mode: 0644]
packaging/music/spacemagic.mp3 [new file with mode: 0644]
packaging/pause.png [new file with mode: 0644]
packaging/readme.txt [new file with mode: 0644]
packaging/saveerror.png [new file with mode: 0644]
packaging/sfx/complete.wav [new file with mode: 0644]
packaging/sfx/connect.wav [new file with mode: 0644]
packaging/sfx/disconnect.wav [new file with mode: 0644]
packaging/sfx/go.wav [new file with mode: 0644]
packaging/sfx/magic.wav [new file with mode: 0644]
packaging/sfx/slide.wav [new file with mode: 0644]
packaging/sfx/step.wav [new file with mode: 0644]
packaging/sfx/step2.wav [new file with mode: 0644]
packaging/sfx/step3.wav [new file with mode: 0644]
packaging/zucchinibread.js [new file with mode: 0644]
palette-test.png [new file with mode: 0644]
pause.png [new file with mode: 0644]
saveerror.png [new file with mode: 0644]
sfx/complete.wav [new file with mode: 0644]
sfx/connect.wav [new file with mode: 0644]
sfx/disconnect.wav [new file with mode: 0644]
sfx/go.wav [new file with mode: 0644]
sfx/magic.wav [new file with mode: 0644]
sfx/slide.wav [new file with mode: 0644]
sfx/step.wav [new file with mode: 0644]
sfx/step2.wav [new file with mode: 0644]
sfx/step3.wav [new file with mode: 0644]
zucchinibread.js [new file with mode: 0644]

diff --git a/ase/beams.aseprite b/ase/beams.aseprite
new file mode 100644 (file)
index 0000000..0c9b53d
Binary files /dev/null and b/ase/beams.aseprite differ
diff --git a/ase/lizwiz.aseprite b/ase/lizwiz.aseprite
new file mode 100644 (file)
index 0000000..86189ba
Binary files /dev/null and b/ase/lizwiz.aseprite differ
diff --git a/ase/lizwiz.png b/ase/lizwiz.png
new file mode 100644 (file)
index 0000000..67aebbe
Binary files /dev/null and b/ase/lizwiz.png differ
diff --git a/ase/magic.aseprite b/ase/magic.aseprite
new file mode 100644 (file)
index 0000000..801dc62
Binary files /dev/null and b/ase/magic.aseprite differ
diff --git a/ase/palette.aseprite b/ase/palette.aseprite
new file mode 100644 (file)
index 0000000..6bcaf3b
Binary files /dev/null and b/ase/palette.aseprite differ
diff --git a/ase/spin.gif b/ase/spin.gif
new file mode 100644 (file)
index 0000000..a2cc113
Binary files /dev/null and b/ase/spin.gif differ
diff --git a/ase/stones.aseprite b/ase/stones.aseprite
new file mode 100644 (file)
index 0000000..b67a79c
Binary files /dev/null and b/ase/stones.aseprite differ
diff --git a/ase/stones.gif b/ase/stones.gif
new file mode 100644 (file)
index 0000000..15e0730
Binary files /dev/null and b/ase/stones.gif differ
diff --git a/ase/tiles.aseprite b/ase/tiles.aseprite
new file mode 100644 (file)
index 0000000..718ac58
Binary files /dev/null and b/ase/tiles.aseprite differ
diff --git a/ase/tilesold.aseprite b/ase/tilesold.aseprite
new file mode 100644 (file)
index 0000000..d9f8065
Binary files /dev/null and b/ase/tilesold.aseprite differ
diff --git a/ase/titlescreen.aseprite b/ase/titlescreen.aseprite
new file mode 100644 (file)
index 0000000..17c37e1
Binary files /dev/null and b/ase/titlescreen.aseprite differ
diff --git a/ase/youdidit.aseprite b/ase/youdidit.aseprite
new file mode 100644 (file)
index 0000000..620fc73
Binary files /dev/null and b/ase/youdidit.aseprite differ
diff --git a/clicktostart.png b/clicktostart.png
new file mode 100644 (file)
index 0000000..9fd9728
Binary files /dev/null and b/clicktostart.png differ
diff --git a/endscreen.aseprite b/endscreen.aseprite
new file mode 100644 (file)
index 0000000..5290952
Binary files /dev/null and b/endscreen.aseprite differ
diff --git a/game.js b/game.js
new file mode 100644 (file)
index 0000000..0894ba1
--- /dev/null
+++ b/game.js
@@ -0,0 +1,1143 @@
+"use strict";
+
+let game;
+
+let game_started = false;
+
+let level_number = 1;
+
+let map;
+
+let intitle;
+let wonitall;
+
+/* --------- definitions ---------- */
+
+/* state:
+ * STAND: waiting for input
+ * MOVE: walking to another square
+ * CAST: casting a spell (successfully, i.e. pulling an object)
+ * CASTEND: done casting a spell but waiting for the magic to go away
+ * WIN: level complete
+ */
+let State = { STAND: 0, MOVE: 1, CAST: 2, CASTEND: 3, WIN: 4 };
+
+let can_continue = false;
+
+let save_data = 1;
+const SAVE_KEY = "casso.lizardwizard.save"
+
+zb.ready(function() {
+    game = zb.create_game({
+        canvas: 'canvas',
+        canvas_w: 800,
+        canvas_h: 800,
+        draw_scale: 3,
+        tile_size: 32,
+        level_w: 8,
+        level_h: 8,
+        background_color: '#36374b',
+        draw_func: do_draw,
+        update_func: do_update,
+        run_in_background: true,
+        save_key: SAVE_KEY,
+        state: State.STAND,
+        events: {
+            keydown: handle_keydown,
+            keyup: handle_keyup,
+            mouseup: handle_mouseup,
+            mousedown: handle_mousedown,
+            gamestart: handle_gamestart,
+        },
+    });
+
+    game.register_sfx({
+        slide: {
+            path: 'sfx/slide.wav',
+            volume: 0.75,
+        },
+        connect: {
+            path: 'sfx/connect.wav',
+            volume: 0.8,
+        },
+        disconnect: {
+            path: 'sfx/disconnect.wav',
+            volume: 0.5,
+        },
+        win: {
+            path: 'sfx/complete.wav',
+            volume: 1,
+        },
+        go: {
+            path: 'sfx/go.wav',
+            volume: 1,
+        },
+        step: {
+            path: 'sfx/step.wav',
+            volume: 0.3,
+        },
+        step2: {
+            path: 'sfx/step2.wav',
+            volume: 0.3,
+        },
+        step3: {
+            path: 'sfx/step3.wav',
+            volume: 0.3,
+        },
+    });
+
+    game.register_images({
+        character: 'img/lizwiz.png',
+        tiles: 'img/tiles.png',
+        magic: 'img/magic.png',
+        stones: 'img/stones.png',
+        beams: 'img/beams.png',
+        youdidit: 'img/youdidit.png',
+        endscreen: 'img/endscreen.png',
+        titlescreen: 'img/titlescreen.png',
+        levelimgs: {
+             1: 'img/level/1.png',
+             2: 'img/level/2.png',
+             3: 'img/level/3.png',
+             4: 'img/level/4.png',
+             5: 'img/level/5.png',
+             6: 'img/level/6.png',
+             7: 'img/level/7.png',
+             8: 'img/level/8.png',
+             9: 'img/level/9.png',
+            10: 'img/level/10.png',
+            11: 'img/level/11.png',
+            12: 'img/level/12.png',
+            13: 'img/level/13.png',
+            14: 'img/level/14.png',
+            15: 'img/level/15.png',
+        }
+    });
+
+    game.register_music({
+        magic: {
+            path: 'sfx/magic.wav',
+            volume: 0.7,
+        },
+        bgm: {
+            path: 'music/spacemagic',
+            volume: 0.95,
+        },
+    });
+
+    game.resources_ready();
+});
+
+let ID = {
+    lwall: 0,
+    floor: 1,
+    rwall: 2,
+    bothwall: 3,
+    gaptop: 4,
+    gap: 5,
+}
+
+let walkable = {
+    [ID.floor]: true,
+}
+
+let stoneID = {
+    blank: 0,
+    leftup: 1,
+    rightup: 2,
+    leftdown: 3,
+    rightdown: 4,
+    up: 5,
+    right: 6,
+    down: 7,
+    left: 8,
+    blocker: 9,
+};
+
+let beamID = {
+    leftright: 0,
+    updown: 1,
+    left: 2,
+    down: 3,
+    right: 4,
+    up: 5,
+};
+
+let connect_right_stones = {
+    [stoneID.rightup]: true,
+    [stoneID.rightdown]: true,
+    [stoneID.right]: true,
+};
+
+let connect_left_stones = {
+    [stoneID.leftup]: true,
+    [stoneID.leftdown]: true,
+    [stoneID.left]: true,
+};
+
+let connect_up_stones = {
+    [stoneID.leftup]: true,
+    [stoneID.rightup]: true,
+    [stoneID.up]: true,
+};
+
+let connect_down_stones = {
+    [stoneID.leftdown]: true,
+    [stoneID.rightdown]: true,
+    [stoneID.down]: true,
+};
+
+let dirs = {
+    down: 0,
+    right: 1,
+    up: 2,
+    left: 3,
+};
+
+let MAGIC_DIMENSION = 8;
+let MAGIC_TYPES = 24;
+
+/* ------ timers & static timer values --------- */
+
+let CHAR_MOVE_SPEED = 4;
+let OBJ_PULL_SPEED = 3.5;
+
+let magic_particle_spawn_timer = 0;
+let MAGIC_PARTICLE_SPAWN_GAP = 50;
+let MAGIC_PARTICLE_LIFESPAN = 800;
+let MAGIC_PARTICLE_LIFESPAN_VARIANCE = 150;
+
+let turn_timer = 0;
+let CHANGE_DIRECTION_GAP = 70;
+
+let OBJ_FADE_TIME = 800;
+
+/* ------- game global state -------- */
+
+let character = {};
+
+let objects = [];
+
+let magic_particles = [];
+
+let beams = [];
+
+let arrows = [];
+let space_pressed = false;
+
+let cast_target = null;
+
+/* ------- game behavior functions -------- */
+
+function change_anim(object, anim_name) {
+    if (object.current_anim !== anim_name) {
+        object.current_anim = anim_name;
+        object.current_frame = 0;
+        object.anim_timer = 0;
+    }
+}
+
+function complete_char_move(character) {
+    if (arrows.length === 0) {
+        change_anim(character, 'stand');
+        game.state = State.STAND;
+        play_step_sound();
+    } else {
+        check_arrow_inputs();
+    }
+}
+
+function check_all_stone_connections(playsfx) {
+    beams = [];
+
+    for (let o of objects) {
+        if (o.is_stone) {
+            o.was_already_on = false;
+            if (o.frame === 1) {
+                o.was_already_on = true;
+            }
+            o.frame = 0;
+        }
+    }
+
+    let level_complete = true;
+    for (let o of objects) {
+        if (o.is_stone) {
+            if (o.target_x === o.x && o.target_y === o.y) {
+                if (!check_connections(o)) {
+                    level_complete = false;
+                }
+            }
+        }
+    }
+
+    let connected = false, disconnected = false;
+    for (let o of objects) {
+        if (!o.was_already_on && o.frame === 1) {
+            connected = true;
+        }
+        if (o.was_already_on && o.frame === 0) {
+            disconnected = true;
+        }
+    }
+
+    if (connected && playsfx) {
+        game.sfx.connect.play();
+    }
+    if (disconnected && playsfx) {
+        game.sfx.disconnect.play();
+    }
+
+    if (level_complete) {
+        win();
+    }
+}
+
+function complete_stone_pull(stone) {
+    if (game.state === State.CAST || space_pressed) {
+        attempt_cast();
+    } else {
+        check_all_stone_connections(true);
+    }
+}
+
+function check_connections(stone) {
+    let friend;
+    let found_any = false;
+    let missed_any = false;
+    let missed = { up: false, down: false, left: false, right: false };
+    if (connect_up_stones[stone.variant]) {
+        friend = find_stone_friend(stone.x, stone.y, 0, -1);
+        if (friend && connect_down_stones[friend.variant]) {
+            connect_stones_vert(friend, stone);
+            found_any = true;
+        } else {
+            missed_any = true;
+            missed.up = true;
+        }
+    }
+    if (connect_down_stones[stone.variant]) {
+        friend = find_stone_friend(stone.x, stone.y, 0, 1);
+        if (friend && connect_up_stones[friend.variant]) {
+            connect_stones_vert(stone, friend);
+            found_any = true;
+        } else {
+            missed_any = true;
+            missed.down = true;
+        }
+    }
+    if (connect_left_stones[stone.variant]) {
+        friend = find_stone_friend(stone.x, stone.y, -1, 0);
+        if (friend && connect_right_stones[friend.variant]) {
+            connect_stones_horiz(friend, stone);
+            found_any = true;
+        } else {
+            missed_any = true;
+            missed.left = true;
+        }
+    }
+    if (connect_right_stones[stone.variant]) {
+        friend = find_stone_friend(stone.x, stone.y, 1, 0);
+        if (friend && connect_left_stones[friend.variant]) {
+            connect_stones_horiz(stone, friend);
+            found_any = true;
+        } else {
+            missed_any = true;
+            missed.right = true;
+        }
+    }
+
+    if (found_any && missed_any) {
+        if (missed.left) {
+            beams.push({
+                x: stone.x - 1,
+                y: stone.y,
+                sprite: {
+                    img: game.img.beams,
+                    y_offset: 4,
+                },
+                variant: beamID.right,
+            });
+        }
+        if (missed.right) {
+            beams.push({
+                x: stone.x + 1,
+                y: stone.y,
+                sprite: {
+                    img: game.img.beams,
+                    y_offset: 4,
+                },
+                variant: beamID.left,
+            });
+        }
+        if (missed.up) {
+            beams.push({
+                x: stone.x,
+                y: stone.y - 1,
+                sprite: {
+                    img: game.img.beams,
+                    y_offset: 4,
+                },
+                variant: beamID.down,
+            });
+        }
+        if (missed.down) {
+            beams.push({
+                x: stone.x,
+                y: stone.y + 1,
+                sprite: {
+                    img: game.img.beams,
+                    y_offset: 4,
+                },
+                variant: beamID.up,
+            });
+        }
+    }
+
+    return !missed_any;
+}
+
+function find_stone_friend(x, y, dx, dy) {
+    let sx = x + dx, sy = y + dy;
+    let friend = null;
+
+    do {
+        let objs = objs_at(sx, sy);
+        for (let o of objs) {
+            if (o.is_stone) {
+                friend = o;
+            }
+        }
+        sx += dx;
+        sy += dy;
+    } while (sx >= 0 && sy >= 0 && sx < game.level_w && sy < game.level_h && !friend);
+
+    return friend;
+}
+
+function connect_stones_horiz(left_stone, right_stone) {
+    if (left_stone.target_x !== left_stone.x || left_stone.target_y !== left_stone.y) return;
+    if (right_stone.target_x !== right_stone.x || right_stone.target_y !== right_stone.y) return;
+    left_stone.frame = 1;
+    right_stone.frame = 1;
+    for (let x = left_stone.x + 1; x < right_stone.x; x++) {
+        beams.push({
+            variant: beamID.leftright,
+            sprite: {
+                img: game.img.beams,
+                y_offset: 4,
+            },
+            x: x,
+            y: left_stone.y,
+        });
+    }
+}
+
+function connect_stones_vert(top_stone, bottom_stone) {
+    /*
+    let fade_beam = false;
+    if (!top_stone.was_already_on) {
+        fade_beam = true;
+        top_stone.fade = true;
+        top_stone.old_frame = 0;
+        top_stone.fade_fraction = 0;
+    }
+    if (!bottom_stone.was_already_on) {
+        fade_beam = true;
+        bottom_stone.fade = true;
+        bottom_stone.old_frame = 0;
+        bottom_stone.fade_fraction = 0;
+    }
+*/
+    if (top_stone.target_x !== top_stone.x || top_stone.target_y !== top_stone.y) return;
+    if (bottom_stone.target_x !== bottom_stone.x || bottom_stone.target_y !== bottom_stone.y) return;
+    top_stone.frame = 1;
+    bottom_stone.frame = 1;
+    for (let y = top_stone.y + 1; y < bottom_stone.y; y++) {
+        beams.push({
+            variant: beamID.updown,
+            sprite: {
+                img: game.img.beams,
+                y_offset: 4,
+            },
+            x: top_stone.x,
+            y: y,
+        });
+    }
+}
+
+function start_cast() {
+    if (game.state === State.STAND) {
+        attempt_cast();
+    }
+}
+
+function attempt_cast() {
+    change_anim(character, 'cast');
+
+    game.state = State.CAST;
+    let sdx = 0, sdy = 0;
+    switch (character.dir) {
+        case dirs.right:
+            sdx = 1;
+            break;
+        case dirs.down:
+            sdy = 1;
+            break;
+        case dirs.left:
+            sdx = -1;
+            break;
+        case dirs.up:
+            sdy = -1;
+            break;
+    }
+    let target_x = character.x + sdx;
+    let target_y = character.y + sdy;
+    let found_obj = null;
+    do {
+        let objs = objs_at(target_x, target_y);
+        for (let o of objs) {
+            if (o.pullable) {
+                found_obj = objs[0];
+            } else if (o.blocks) {
+                target_x = 10000;
+            }
+        }
+        target_x += sdx;
+        target_y += sdy;
+    } while (!found_obj && target_x >= 0 && target_y >= 0 && target_x < game.level_w && target_y < game.level_h);
+
+    if (found_obj && found_obj.pullable) {
+        cast_target = found_obj;
+        game.music.magic.play();
+        let can_move = check_move(cast_target.x, cast_target.y, -sdx, -sdy);
+        if (can_move) {
+            cast_target.target_x = cast_target.x - sdx;
+            cast_target.target_y = cast_target.y - sdy;
+            cast_target.move_speed = OBJ_PULL_SPEED;
+            cast_target.move_fraction = 0;
+            cast_target.completed_move = complete_stone_pull;
+            if (cast_target.x - sdx === character.x && cast_target.y - sdy === character.y) {
+                cast_target.target_x = cast_target.x;
+                cast_target.target_y = cast_target.y;
+            } else {
+                game.sfx.slide.play();
+                create_undo_point();
+            }
+        }
+        check_all_stone_connections(true);
+    }
+}
+
+function cancel_cast() {
+    if (game.state === State.STAND) {
+        change_anim(character, 'stand');
+    }
+
+    if (game.state === State.CAST) {
+        game.state = State.CASTEND;
+    }
+}
+
+function delete_save() {
+    try {
+        game.save('level_num', 1);
+    } catch (e) {
+        console.error("oops, can't save! though that uh... doesn't matter here");
+    }
+}
+
+function save() {
+    try {
+        console.log("Saving");
+        let save_data = level_number;
+        if (game.state === State.WIN) {
+            console.log("on next level");
+            save_data = level_number + 1;
+        }
+        game.save('level_num', save_data);
+    } catch (e) {
+        console.error("oops, can't save!", e);
+    }
+}
+
+function play_step_sound() {
+    // attempted to add this last minute, doesn't work well
+    /*let step = Math.floor(Math.random() * 3);
+    switch (step) {
+        case 0:
+            game.sfx.step.play();
+            break;
+        case 1:
+            game.sfx.step2.play();
+            break;
+        case 2:
+            game.sfx.step3.play();
+            break;
+    }*/
+}
+
+function handle_gamestart(game) {
+    console.log("Game start!");
+
+    intitle = true;
+    wonitall = false;
+
+    character = {
+        x: 6,
+        y: 6,
+        sprite: {
+            img: game.img.character,
+            w: 40,
+            h: 40,
+            x_offset: 4,
+            y_offset: 8,
+        },
+        move_speed: CHAR_MOVE_SPEED,
+        completed_move: complete_char_move,
+        dir: dirs.right,
+        current_anim: "stand",
+        current_frame: 0,
+        anim_timer: 0,
+        anims: {
+            stand: [ [0, 1000] ],
+            walk: [ [1, 150], [0, 150], [2, 150], [0, 150] ],
+            cast: [ [3, 1000] ],
+        },
+    };
+
+    let save_data = parseInt(game.load('level_num') || "1");
+    console.log("save data: ", save_data);
+    level_number = save_data;
+    load_level();
+}
+
+function objs_at(x, y) {
+    return objects.filter(o => o.x === x && o.y === y);
+}
+
+function tile_at(x, y) {
+    return map[y * game.level_w + x];
+}
+
+let undo_stack = [];
+
+function create_undo_point() {
+    let copied_objs = zb.copy_flat_objlist(objects.filter(o => o !== character));
+
+    let undo_point = {
+        char_x: character.x,
+        char_y: character.y,
+        char_dir: character.dir,
+        objs: copied_objs,
+        beams: zb.copy_flat_objlist(beams),
+    }
+
+    undo_stack.push(undo_point);
+}
+
+function undo() {
+    let undo_point = undo_stack.pop();
+    if (!undo_point) return;
+
+    objects = undo_point.objs;
+    objects.push(character);
+
+    beams = undo_point.beams;
+    character.x = undo_point.char_x;
+    character.y = undo_point.char_y;
+    character.dir = undo_point.char_dir;
+    change_anim(character, "stand");
+    can_continue = false;
+
+    for (let o of objects) {
+        o.target_x = o.x;
+        o.target_y = o.y;
+    }
+
+    game.state = State.STAND;
+    check_all_stone_connections(false);
+
+    console.log(game.state);
+}
+
+function reset() {
+    game.start_transition(zb.transition.FADE, 500, function() {
+        load_level();
+        game.state = State.STAND;
+    });
+}
+
+function advance_level() {
+    if (!can_continue) return;
+    console.log("advlevel");
+    game.start_transition(zb.transition.SLIDE_DOWN, 750, function() {
+        level_number ++;
+        load_level();
+        can_continue = false;
+        character.dir = dirs.right;
+        game.state = State.STAND;
+    }, function() {
+        game.sfx.go.play();
+    });
+}
+
+function win_everything() {
+    game.long_transition(zb.transition.FADE, 1000, function() {
+        wonitall = true;
+        delete_save();
+    });
+}
+
+function load_level() {
+    if (level_number > Object.keys(levels).length) {
+        win_everything();
+    } else {
+        load_level_data(levels[level_number]);
+    }
+}
+
+function load_level_data(lvl) {
+    undo_stack = [];
+
+    beams = [];
+
+    character.target_x = character.x = lvl.start_x;
+    character.target_y = character.y = lvl.start_y;
+    character.dir = dirs.right;
+    change_anim(character, 'stand');
+
+    game.state = State.STAND;
+
+    map = lvl.map;
+
+    objects = [ character ];
+
+    for (let s of lvl.stones) {
+        let stone = {
+            x: s.x,
+            y: s.y,
+            target_x: s.x,
+            target_y: s.y,
+            move_fraction: 0,
+            sprite: {
+                img: game.img.stones,
+                w: 40,
+                h: 40,
+                x_offset: 4,
+                y_offset: 8,
+            },
+            variant: s.type,
+            pullable: s.type !== stoneID.blocker,
+            blocks: s.type === stoneID.blocker,
+            is_stone: true,
+        };
+        objects.push(stone);
+    }
+
+    check_all_stone_connections(false);
+}
+
+function check_victory() {
+    win();
+}
+
+function win() {
+    console.log("You won!");
+    game.state = State.WIN;
+    game.music.magic.pause();
+    save();
+    window.setTimeout(function() {
+        game.sfx.win.play();
+        can_continue = true;
+    }, 350);
+}
+
+function check_move(x, y, dx, dy) {
+    if (x + dx < 0 || y + dy < 0 || x + dx >= game.level_w || y + dy >= game.level_h) {
+        return false;
+    }
+
+    if (!walkable[tile_at(x + dx, y + dy)]) {
+        return false;
+    }
+
+    let blocking_objs = objs_at(x + dx, y + dy);
+    if (blocking_objs.length > 0) {
+        return false;
+    }
+
+    return true;
+}
+
+function do_move(dir, dx, dy) {
+    if (game.state === State.WIN) return;
+
+    if (character.current_anim === 'cast') return;
+
+    if (dir !== character.dir) {
+        turn_timer = 0;
+    }
+
+    character.dir = dir;
+
+    if (turn_timer < CHANGE_DIRECTION_GAP) {
+        game.state = State.STAND;
+        return;
+    }
+
+    let can_move = check_move(character.x, character.y, dx, dy);
+
+    if (!can_move) {
+        game.state = State.STAND;
+        if (character.current_anim === 'walk') {
+            play_step_sound();
+        }
+        change_anim(character, 'stand');
+        return;
+    }
+
+    create_undo_point();
+
+    character.target_x = character.x + dx;
+    character.target_y = character.y + dy;
+    character.move_fraction = 0;
+    change_anim(character, 'walk');
+    game.state = State.MOVE;
+}
+
+/* ---------- update functions ------------ */
+
+function check_arrow_inputs() {
+    if (arrows.length > 0) {
+        switch (arrows[arrows.length - 1]) {
+            case dirs.right:
+                do_move(dirs.right, 1, 0);
+                break;
+            case dirs.down:
+                do_move(dirs.down, 0, 1);
+                break;
+            case dirs.left:
+                do_move(dirs.left, -1, 0);
+                break;
+            case dirs.up:
+                do_move(dirs.up, 0, -1);
+                break;
+        }
+    } else {
+        game.state = State.STAND;
+    }
+}
+
+/* MAIN UPDATE FUNCTION */
+function do_update(delta) {
+    turn_timer += delta;
+
+    if (game.state === State.STAND) {
+        check_arrow_inputs();
+    }
+
+    update_magic_particles(delta);
+
+    for (let o of objects) {
+        if (o.current_anim) {
+            o.anim_timer += delta;
+            while (o.anim_timer > o.anims[o.current_anim][o.current_frame][1]) {
+                o.anim_timer -= o.anims[o.current_anim][o.current_frame][1];
+                o.current_frame ++;
+                o.current_frame = zb.mod(o.current_frame, o.anims[o.current_anim].length);
+                if (o === character && o.current_anim === 'walk' && o.anims[o.current_anim][o.current_frame][0] !== 0) {
+                    play_step_sound();
+                }
+            }
+        }
+
+        if (o.target_x !== o.x || o.target_y !== o.y) {
+            o.move_fraction += o.move_speed * delta / 1000;
+            if (o.move_fraction > 1) {
+                o.x = o.target_x;
+                o.y = o.target_y;
+                o.move_fraction = 0;
+                o.completed_move(o);
+            }
+        }
+    }
+
+    if (arrows.length === 0 && character.x === character.target_x && character.y === character.target_y && game.state === State.STAND) {
+        change_anim(character, 'stand');
+    }
+}
+
+function update_magic_particles(delta) {
+    if (cast_target && (game.state === State.CAST || cast_target.target_x !== cast_target.x || cast_target.target_y !== cast_target.y)) {
+        magic_particle_spawn_timer += delta;
+        while (magic_particle_spawn_timer > MAGIC_PARTICLE_SPAWN_GAP) {
+            magic_particle_spawn_timer -= MAGIC_PARTICLE_SPAWN_GAP;
+            let ctx = cast_target.x * game.tile_size * (1 - cast_target.move_fraction) + cast_target.target_x * game.tile_size * cast_target.move_fraction;
+            let cty = cast_target.y * game.tile_size * (1 - cast_target.move_fraction) + cast_target.target_y * game.tile_size * cast_target.move_fraction - 3;
+            let target_x_offset = 12;
+            if (character.dir === dirs.up) {
+                target_x_offset = 28;
+            }
+            magic_particles.push({
+                x: ctx + Math.random() * game.tile_size,
+                y: cty + Math.random() * game.tile_size,
+                target_x: character.x * game.tile_size + target_x_offset - character.sprite.x_offset,
+                target_y: character.y * game.tile_size + 19 - character.sprite.y_offset,
+                progress: 0,
+                lifespan: MAGIC_PARTICLE_LIFESPAN + (Math.random() * 2 - 1) * MAGIC_PARTICLE_LIFESPAN_VARIANCE,
+                type: Math.floor(Math.random() * MAGIC_TYPES),
+            });
+        }
+    }
+
+    if (game.state === State.CASTEND) {
+        if (!cast_target || cast_target.target_x === cast_target.x && cast_target.target_y === cast_target.y) {
+            for (let p of magic_particles) {
+                p.lifespan *= 0.92;
+            }
+        }
+    }
+
+    for (let p of magic_particles) {
+        p.progress += delta / p.lifespan;
+        if (p.progress >= 1) {
+            p.deleteme = true;
+        }
+    }
+
+    magic_particles = magic_particles.filter(p => !p.deleteme);
+    if (game.state === State.CASTEND && magic_particles.length === 0) {
+        change_anim(character, 'stand');
+        cast_target = null;
+        game.state = State.STAND;
+        game.music.magic.pause();
+    }
+}
+
+/* ---------- draw functions ----------- */
+
+/* DRAW */
+function do_draw(ctx) {
+    if (intitle) {
+        zb.screen_draw(ctx, game.img.titlescreen);
+        return;
+    }
+
+    if (wonitall) {
+        zb.screen_draw(ctx, game.img.endscreen);
+        return;
+    }
+
+    ctx.save();
+    ctx.translate(5, 5);
+
+    draw_map(ctx);
+
+    if (game.img.levelimgs.hasOwnProperty(level_number)) {
+        ctx.save();
+        ctx.translate(-5, -5);
+        zb.screen_draw(ctx, game.img.levelimgs[level_number]);
+        ctx.restore();
+    }
+
+    let drawables = zb.copy_list(objects).concat(zb.copy_list(beams));
+
+    drawables.sort((a, b) => {
+        let amf = a.move_fraction || 0;
+        let bmf = b.move_fraction || 0;
+        let aty = a.target_y || 0;
+        let bty = b.target_y || 0;
+        let ay = Math.max(a.y, aty); /*a.y * (1 - amf) + aty * amf;*/
+        let by = b.y * (1 - bmf) + bty * bmf;
+
+        if (ay < by) {
+            return -1;
+        }
+        if (ay > by) {
+            return 1;
+        }
+        if (a === character) {
+            return 1;
+        }
+        if (b === character) {
+            return -1;
+        }
+        return 0;
+    });
+
+    for (let d of drawables) {
+        if (!d.sprite && !d.img) continue;
+        let sprite = d.sprite ? d.sprite.img : d.img;
+        let sw = d.sprite ? d.sprite.w || game.tile_size : game.tile_size;
+        let sh = d.sprite ? d.sprite.h || game.tile_size : game.tile_size;
+        let xo = d.sprite ? d.sprite.x_offset || 0 : 0;
+        let yo = d.sprite ? d.sprite.y_offset || 0 : 0;
+        let tx = d.target_x !== undefined ? d.target_x : d.x;
+        let ty = d.target_y !== undefined ? d.target_y : d.y;
+        let mf = d.move_fraction || 0;
+        let dir = d.dir || d.variant || 0;
+        let frame = d.frame || 0;
+        if (d.anims && d.current_anim && d.current_frame !== undefined) {
+            frame = d.anims[d.current_anim][d.current_frame][0];
+        }
+
+        if (d === character && d.dir === dirs.left) {
+            /* we draw magic particles at same time as lizard, before/after lizard depending on whether you're facing left or not.
+             * when you're facing left, the wand is behind the lizard's face, so the particles should also go behind. */
+            draw_magic_particles(ctx);
+        }
+
+        zb.sprite_draw(ctx, sprite, sw, sh, dir, frame,
+                    (d.x * (1 - mf) + tx * mf) * game.tile_size - xo, (d.y * (1 - mf) + ty * mf) * game.tile_size - yo);
+
+        if (d === character && d.dir !== dirs.left && d.dir !== dirs.down) {
+            draw_magic_particles(ctx);
+        }
+    }
+
+    if (character.dir === dirs.down) {
+        /* If character is facing down, the thing we are pulling gets drawn after us, so draw magic particles at the very end. */
+        draw_magic_particles(ctx);
+    }
+
+    if (game.state === State.WIN && can_continue) {
+        zb.screen_draw(ctx, game.img.youdidit);
+    }
+
+    ctx.restore();
+}
+
+function draw_magic_particles(ctx) {
+    for (let p of magic_particles) {
+        let interp = Math.pow(p.progress, 1.5);
+        zb.sprite_draw(ctx, game.img.magic, MAGIC_DIMENSION, MAGIC_DIMENSION, p.type, 0,
+                    p.x * (1 - interp) + p.target_x * interp - MAGIC_DIMENSION / 2, p.y * (1 - interp) + p.target_y * interp - MAGIC_DIMENSION / 2);
+    }
+}
+
+function draw_beams(ctx) {
+    for (let b of beams) {
+        zb.sprite_draw(ctx, game.img.beams, game.tile_size, game.tile_size, b.type, 0, b.x * game.tile_size, b.y * game.tile_size - 4);
+    }
+}
+
+function draw_map(ctx) {
+    for (let y = 0; y < game.level_h; y++) {
+        for (let x = 0; x < game.level_w; x++) {
+            let tile = map[y * game.level_w + x];
+            let variant = (x + y) % 2;
+            zb.sprite_draw(ctx, game.img.tiles, game.tile_size, game.tile_size, tile, variant, x * game.tile_size, y * game.tile_size);
+        }
+    }
+}
+
+/* ---------- event handlers ------------ */
+
+let z_presses = 0;
+function handle_keydown(game, e) {
+    if (wonitall || intitle) return;
+    if (e.repeat && e.key !== 'z') return;
+    if (game.transition.is_transitioning) return;
+
+    if (game.state === State.WIN && e.key !== 'r' && e.key !== 'z') {
+        advance_level();
+        return;
+    }
+
+    switch (e.key) {
+        case 'ArrowRight':
+            arrows.push(dirs.right);
+            break;
+        case 'ArrowDown':
+            arrows.push(dirs.down);
+            break;
+        case 'ArrowLeft':
+            arrows.push(dirs.left);
+            break;
+        case 'ArrowUp':
+            arrows.push(dirs.up);
+            break;
+        case ' ':
+            space_pressed = true;
+            start_cast();
+            break;
+        case 'z':
+            if (!e.repeat || z_presses % 3 === 0) {
+                undo();
+            }
+            z_presses ++;
+            e.preventDefault();
+            break;
+    }
+}
+
+let x_pressed = false;
+function handle_keyup(game, e) {
+    if (wonitall) return;
+    if (game.transition.is_transitioning) return;
+
+    // key up
+    switch (e.key) {
+        case 'ArrowRight':
+            arrows = arrows.filter(a => a !== dirs.right);
+            break;
+        case 'ArrowDown':
+            arrows = arrows.filter(a => a !== dirs.down);
+            break;
+        case 'ArrowLeft':
+            arrows = arrows.filter(a => a !== dirs.left);
+            break;
+        case 'ArrowUp':
+            arrows = arrows.filter(a => a !== dirs.up);
+            break;
+        case ' ':
+            space_pressed = false;
+            cancel_cast();
+            break;
+        case 'm':
+            game.toggle_mute();
+            e.preventDefault();
+            break;
+        case 'r':
+            reset();
+            e.preventDefault();
+            break;
+        case 'x':
+            x_pressed = true;
+            break;
+        case 'w':
+            if (x_pressed) {
+                delete_save();
+                e.preventDefault();
+            }
+            break;
+    }
+
+    if (e.keyCode !== 88) {
+        /* non-X key */
+        x_pressed = false;
+    }
+}
+
+function handle_mousedown(game) {
+    // click down
+}
+
+function handle_mouseup(game) {
+    if (intitle) {
+        game.long_transition(zb.transition.FADE, 1000, function() {
+            load_level();
+            game.music.bgm.play();
+            intitle = false;
+        });
+    }
+
+    if (wonitall) {
+        game.long_transition(zb.transition.FADE, 1000, function() {
+            wonitall = false;
+            level_number = 1;
+            intitle = true;
+        });
+    }
+}
diff --git a/img/beams.png b/img/beams.png
new file mode 100644 (file)
index 0000000..00b5c56
Binary files /dev/null and b/img/beams.png differ
diff --git a/img/endscreen.png b/img/endscreen.png
new file mode 100644 (file)
index 0000000..935af74
Binary files /dev/null and b/img/endscreen.png differ
diff --git a/img/level/1.aseprite b/img/level/1.aseprite
new file mode 100644 (file)
index 0000000..b0b3ba9
Binary files /dev/null and b/img/level/1.aseprite differ
diff --git a/img/level/1.png b/img/level/1.png
new file mode 100644 (file)
index 0000000..d9553d1
Binary files /dev/null and b/img/level/1.png differ
diff --git a/img/level/10.aseprite b/img/level/10.aseprite
new file mode 100644 (file)
index 0000000..8e0c7f5
Binary files /dev/null and b/img/level/10.aseprite differ
diff --git a/img/level/10.png b/img/level/10.png
new file mode 100644 (file)
index 0000000..472c94f
Binary files /dev/null and b/img/level/10.png differ
diff --git a/img/level/11.aseprite b/img/level/11.aseprite
new file mode 100644 (file)
index 0000000..26eb4b0
Binary files /dev/null and b/img/level/11.aseprite differ
diff --git a/img/level/11.png b/img/level/11.png
new file mode 100644 (file)
index 0000000..415800c
Binary files /dev/null and b/img/level/11.png differ
diff --git a/img/level/12.aseprite b/img/level/12.aseprite
new file mode 100644 (file)
index 0000000..e1839e4
Binary files /dev/null and b/img/level/12.aseprite differ
diff --git a/img/level/12.png b/img/level/12.png
new file mode 100644 (file)
index 0000000..79fea49
Binary files /dev/null and b/img/level/12.png differ
diff --git a/img/level/13.aseprite b/img/level/13.aseprite
new file mode 100644 (file)
index 0000000..069fe84
Binary files /dev/null and b/img/level/13.aseprite differ
diff --git a/img/level/13.png b/img/level/13.png
new file mode 100644 (file)
index 0000000..212fa64
Binary files /dev/null and b/img/level/13.png differ
diff --git a/img/level/14.aseprite b/img/level/14.aseprite
new file mode 100644 (file)
index 0000000..da73db6
Binary files /dev/null and b/img/level/14.aseprite differ
diff --git a/img/level/14.png b/img/level/14.png
new file mode 100644 (file)
index 0000000..992c12d
Binary files /dev/null and b/img/level/14.png differ
diff --git a/img/level/15.aseprite b/img/level/15.aseprite
new file mode 100644 (file)
index 0000000..261db62
Binary files /dev/null and b/img/level/15.aseprite differ
diff --git a/img/level/15.png b/img/level/15.png
new file mode 100644 (file)
index 0000000..7d4bbf0
Binary files /dev/null and b/img/level/15.png differ
diff --git a/img/level/2.aseprite b/img/level/2.aseprite
new file mode 100644 (file)
index 0000000..5265ddb
Binary files /dev/null and b/img/level/2.aseprite differ
diff --git a/img/level/2.png b/img/level/2.png
new file mode 100644 (file)
index 0000000..b472439
Binary files /dev/null and b/img/level/2.png differ
diff --git a/img/level/3.aseprite b/img/level/3.aseprite
new file mode 100644 (file)
index 0000000..2457071
Binary files /dev/null and b/img/level/3.aseprite differ
diff --git a/img/level/3.png b/img/level/3.png
new file mode 100644 (file)
index 0000000..d15656c
Binary files /dev/null and b/img/level/3.png differ
diff --git a/img/level/4.aseprite b/img/level/4.aseprite
new file mode 100644 (file)
index 0000000..57c8717
Binary files /dev/null and b/img/level/4.aseprite differ
diff --git a/img/level/4.png b/img/level/4.png
new file mode 100644 (file)
index 0000000..3fa1154
Binary files /dev/null and b/img/level/4.png differ
diff --git a/img/level/5.aseprite b/img/level/5.aseprite
new file mode 100644 (file)
index 0000000..9b633f5
Binary files /dev/null and b/img/level/5.aseprite differ
diff --git a/img/level/5.png b/img/level/5.png
new file mode 100644 (file)
index 0000000..e37806f
Binary files /dev/null and b/img/level/5.png differ
diff --git a/img/level/6.aseprite b/img/level/6.aseprite
new file mode 100644 (file)
index 0000000..02fb6db
Binary files /dev/null and b/img/level/6.aseprite differ
diff --git a/img/level/6.png b/img/level/6.png
new file mode 100644 (file)
index 0000000..18b3e06
Binary files /dev/null and b/img/level/6.png differ
diff --git a/img/level/7.aseprite b/img/level/7.aseprite
new file mode 100644 (file)
index 0000000..3a09af4
Binary files /dev/null and b/img/level/7.aseprite differ
diff --git a/img/level/7.png b/img/level/7.png
new file mode 100644 (file)
index 0000000..76cec03
Binary files /dev/null and b/img/level/7.png differ
diff --git a/img/level/8.aseprite b/img/level/8.aseprite
new file mode 100644 (file)
index 0000000..653ece8
Binary files /dev/null and b/img/level/8.aseprite differ
diff --git a/img/level/8.png b/img/level/8.png
new file mode 100644 (file)
index 0000000..f9a3c74
Binary files /dev/null and b/img/level/8.png differ
diff --git a/img/level/9.aseprite b/img/level/9.aseprite
new file mode 100644 (file)
index 0000000..1f5e2e7
Binary files /dev/null and b/img/level/9.aseprite differ
diff --git a/img/level/9.png b/img/level/9.png
new file mode 100644 (file)
index 0000000..8db3aca
Binary files /dev/null and b/img/level/9.png differ
diff --git a/img/lizwiz.png b/img/lizwiz.png
new file mode 100644 (file)
index 0000000..8cfab39
Binary files /dev/null and b/img/lizwiz.png differ
diff --git a/img/magic-sheet.png b/img/magic-sheet.png
new file mode 100644 (file)
index 0000000..ced4100
Binary files /dev/null and b/img/magic-sheet.png differ
diff --git a/img/magic.png b/img/magic.png
new file mode 100644 (file)
index 0000000..0c7fb60
Binary files /dev/null and b/img/magic.png differ
diff --git a/img/stones-sheet-sheet.png b/img/stones-sheet-sheet.png
new file mode 100644 (file)
index 0000000..74e5f3d
Binary files /dev/null and b/img/stones-sheet-sheet.png differ
diff --git a/img/stones-sheet.png b/img/stones-sheet.png
new file mode 100644 (file)
index 0000000..24eb09e
Binary files /dev/null and b/img/stones-sheet.png differ
diff --git a/img/stones.png b/img/stones.png
new file mode 100644 (file)
index 0000000..185fae5
Binary files /dev/null and b/img/stones.png differ
diff --git a/img/tiles-sheet-sheet.png b/img/tiles-sheet-sheet.png
new file mode 100644 (file)
index 0000000..e1c5b94
Binary files /dev/null and b/img/tiles-sheet-sheet.png differ
diff --git a/img/tiles-sheet.png b/img/tiles-sheet.png
new file mode 100644 (file)
index 0000000..e1c5b94
Binary files /dev/null and b/img/tiles-sheet.png differ
diff --git a/img/tiles.png b/img/tiles.png
new file mode 100644 (file)
index 0000000..2d01e0d
Binary files /dev/null and b/img/tiles.png differ
diff --git a/img/titlescreen.png b/img/titlescreen.png
new file mode 100644 (file)
index 0000000..2d44468
Binary files /dev/null and b/img/titlescreen.png differ
diff --git a/img/tmptile.png b/img/tmptile.png
new file mode 100644 (file)
index 0000000..38b1293
Binary files /dev/null and b/img/tmptile.png differ
diff --git a/img/youdidit.png b/img/youdidit.png
new file mode 100644 (file)
index 0000000..b17e59b
Binary files /dev/null and b/img/youdidit.png differ
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..5486da0
--- /dev/null
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+<head><title>
+    lizard wizard
+</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 }</style>
+</head>
+<body style="background: black">
+    <canvas id="canvas" width="798" height="798" style="max-width:100%" tabindex=1> </canvas>
+</body>
+<script src="levels.js"></script>
+<script src="zucchinibread.js"></script>
+<script src="game.js"></script>
+</html>
diff --git a/levels.js b/levels.js
new file mode 100644 (file)
index 0000000..53528b7
--- /dev/null
+++ b/levels.js
@@ -0,0 +1,17 @@
+var levels={
+        10: { map: [5,5,1,1,1,1,1,1,1,1,1,5,1,1,1,1,1,1,1,5,1,1,1,1,1,5,5,5,5,5,5,1,1,1,1,5,1,1,1,1,5,1,1,5,1,1,5,5,5,1,1,5,1,1,5,5,5,5,1,1,1,5,5,5,],stones: [ { x:6, y:1, type:3 }, { x:1, y:2, type:4 }, { x:5, y:2, type:7 }, { x:6, y:2, type:9 }, { x:6, y:4, type:1 }, { x:2, y:5, type:5 }, { x:4, y:5, type:2 }, ], start_x:0, start_y:3 },
+        11: { map: [5,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,5,1,1,1,1,1,1,5,],stones: [ { x:2, y:2, type:0 }, { x:3, y:2, type:0 }, { x:4, y:2, type:0 }, { x:5, y:2, type:0 }, { x:2, y:3, type:0 }, { x:3, y:3, type:1 }, { x:4, y:3, type:2 }, { x:5, y:3, type:0 }, { x:2, y:4, type:0 }, { x:3, y:4, type:3 }, { x:4, y:4, type:4 }, { x:5, y:4, type:0 }, { x:2, y:5, type:0 }, { x:3, y:5, type:0 }, { x:4, y:5, type:0 }, { x:5, y:5, type:0 }, ], start_x:1, start_y:1 },
+        12: { map: [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,],stones: [ { x:1, y:1, type:3 }, { x:3, y:1, type:9 }, { x:4, y:1, type:9 }, { x:6, y:1, type:1 }, { x:2, y:3, type:9 }, { x:3, y:3, type:9 }, { x:4, y:3, type:9 }, { x:5, y:3, type:9 }, { x:2, y:4, type:9 }, { x:3, y:4, type:9 }, { x:4, y:4, type:9 }, { x:5, y:4, type:9 }, { x:1, y:6, type:4 }, { x:3, y:6, type:9 }, { x:4, y:6, type:9 }, { x:6, y:6, type:2 }, ], start_x:2, start_y:2 },
+        13: { map: [5,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,5,1,1,1,1,1,1,1,],stones: [ { x:1, y:0, type:9 }, { x:6, y:0, type:9 }, { x:0, y:1, type:9 }, { x:1, y:1, type:4 }, { x:0, y:2, type:0 }, { x:3, y:2, type:9 }, { x:5, y:2, type:9 }, { x:0, y:3, type:0 }, { x:2, y:3, type:9 }, { x:6, y:3, type:9 }, { x:0, y:4, type:0 }, { x:5, y:4, type:2 }, { x:7, y:4, type:8 }, { x:0, y:5, type:0 }, { x:2, y:5, type:9 }, { x:4, y:5, type:3 }, { x:6, y:5, type:9 }, { x:0, y:6, type:9 }, { x:3, y:6, type:9 }, { x:5, y:6, type:9 }, { x:4, y:7, type:5 }, ], start_x:2, start_y:2 },
+        14: { map: [5,5,5,1,1,1,5,5,5,5,1,1,5,1,5,5,5,1,1,1,5,1,5,5,5,1,1,1,5,1,1,5,5,1,1,5,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,],stones: [ { x:2, y:4, type:9 }, { x:4, y:4, type:9 }, { x:5, y:4, type:9 }, { x:1, y:5, type:6 }, { x:6, y:5, type:8 }, { x:6, y:6, type:8 }, { x:1, y:7, type:6 }, ], start_x:3, start_y:2 },
+        15: { map: [5,5,1,1,1,1,5,5,5,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,1,1,1,1,1,5,1,1,1,1,1,1,5,5,5,1,1,1,1,5,5,],stones: [ { x:1, y:1, type:6 }, { x:4, y:1, type:9 }, { x:2, y:2, type:4 }, { x:5, y:2, type:3 }, { x:1, y:3, type:9 }, { x:6, y:4, type:9 }, { x:2, y:5, type:2 }, { x:5, y:5, type:1 }, { x:3, y:6, type:9 }, { x:6, y:6, type:8 }, ], start_x:4, start_y:0 },
+        1: { map: [5,5,5,5,5,5,5,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,5,5,5,5,5,5,5,],stones: [ { x:4, y:2, type:3 }, { x:3, y:3, type:2 }, { x:2, y:4, type:4 }, { x:5, y:5, type:1 }, ], start_x:3, start_y:5 },
+        2: { map: [5,5,5,5,5,5,5,5,5,5,1,1,1,1,5,5,5,5,5,5,5,1,5,5,1,1,1,1,1,1,1,1,5,5,1,1,1,5,5,1,5,5,1,1,5,5,5,1,5,1,1,1,1,1,1,1,5,5,5,5,5,5,5,5,],stones: [ { x:5, y:1, type:8 }, { x:2, y:5, type:4 }, { x:1, y:6, type:5 }, { x:2, y:6, type:9 }, ], start_x:2, start_y:3 },
+        3: { map: [5,5,5,5,5,1,1,5,5,1,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,1,5,5,5,1,1,1,1,1,5,1,5,1,1,1,1,1,5,5,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,],stones: [ { x:6, y:1, type:4 }, { x:3, y:2, type:5 }, { x:2, y:3, type:7 }, { x:6, y:4, type:1 }, { x:0, y:5, type:9 }, { x:2, y:5, type:9 }, { x:3, y:5, type:9 }, { x:4, y:5, type:9 }, ], start_x:1, start_y:1 },
+        4: { map: [5,5,5,5,5,5,5,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,5,5,5,5,5,5,5,],stones: [ { x:3, y:2, type:0 }, { x:4, y:2, type:1 }, { x:2, y:3, type:3 }, { x:5, y:3, type:0 }, { x:2, y:4, type:0 }, { x:5, y:4, type:2 }, { x:3, y:5, type:4 }, { x:4, y:5, type:0 }, ], start_x:3, start_y:1 },
+        5: { map: [1,1,1,1,1,5,5,5,1,1,5,1,1,1,5,5,1,1,5,1,1,1,1,5,1,1,1,5,1,1,1,1,1,1,1,5,1,1,1,1,5,1,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,5,1,1,1,1,1,],stones: [ { x:4, y:0, type:9 }, { x:3, y:1, type:7 }, { x:5, y:1, type:9 }, { x:6, y:2, type:9 }, { x:2, y:3, type:2 }, { x:5, y:3, type:3 }, { x:7, y:3, type:9 }, { x:7, y:4, type:9 }, { x:2, y:5, type:6 }, { x:5, y:5, type:1 }, { x:7, y:5, type:9 }, { x:7, y:6, type:9 }, { x:3, y:7, type:9 }, { x:4, y:7, type:9 }, { x:5, y:7, type:9 }, { x:6, y:7, type:9 }, { x:7, y:7, type:9 }, ], start_x:1, start_y:1 },
+        6: { map: [5,5,5,5,5,5,5,5,5,1,1,5,5,5,5,5,5,1,1,1,1,1,5,5,5,1,1,1,1,1,5,5,5,5,1,1,1,1,1,5,5,5,1,1,1,1,1,5,5,5,5,5,1,1,1,5,5,5,5,5,5,5,5,5,],stones: [ { x:3, y:2, type:0 }, { x:4, y:2, type:7 }, { x:2, y:4, type:6 }, { x:4, y:4, type:0 }, { x:6, y:4, type:8 }, { x:4, y:6, type:5 }, ], start_x:1, start_y:1 },
+        7: { map: [5,5,5,5,5,5,5,5,5,1,1,1,1,1,1,5,5,1,1,5,1,1,1,5,5,1,1,5,1,1,1,5,5,1,1,1,5,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,5,5,5,5,5,5,5,],stones: [ { x:2, y:2, type:6 }, { x:5, y:2, type:1 }, { x:3, y:4, type:9 }, { x:2, y:5, type:6 }, { x:3, y:5, type:0 }, { x:5, y:5, type:3 }, ], start_x:1, start_y:3 },
+        8: { map: [5,5,5,5,5,5,5,5,5,1,1,1,1,1,1,5,5,1,1,1,5,1,1,5,5,1,1,5,5,1,1,5,5,1,1,5,5,5,1,5,5,5,1,1,5,1,1,5,5,5,1,1,1,1,1,5,5,5,5,5,5,5,5,5,],stones: [ { x:5, y:1, type:7 }, { x:3, y:2, type:7 }, { x:3, y:5, type:2 }, { x:5, y:6, type:1 }, ], start_x:1, start_y:1 },
+        9: { map: [1,1,1,1,1,1,1,1,1,5,5,5,5,5,5,1,1,5,1,1,1,1,5,1,1,5,1,1,1,1,5,1,1,5,1,1,1,1,5,1,1,5,1,1,1,1,5,1,1,5,5,5,5,5,5,1,1,1,1,1,1,1,1,1,],stones: [ { x:3, y:2, type:7 }, { x:5, y:2, type:0 }, { x:3, y:3, type:0 }, { x:4, y:3, type:4 }, { x:3, y:4, type:1 }, { x:4, y:4, type:0 }, { x:2, y:5, type:0 }, { x:4, y:5, type:5 }, ], start_x:3, start_y:0 },
+}
diff --git a/levels/1.tmx b/levels/1.tmx
new file mode 100644 (file)
index 0000000..2cfeb80
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,7,7,7,7,7,7,7,
+7,2,2,2,2,2,2,7,
+7,2,2,2,11,2,2,7,
+7,2,2,10,2,2,2,7,
+7,2,12,2,2,2,2,7,
+7,2,2,5,2,9,2,7,
+7,2,2,2,2,2,2,7,
+7,7,7,7,7,7,7,7
+</data>
+ </layer>
+</map>
diff --git a/levels/10.tmx b/levels/10.tmx
new file mode 100644 (file)
index 0000000..963b60e
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,7,2,2,2,2,2,2,
+2,2,2,7,2,2,11,2,
+2,12,2,7,2,15,1,2,
+5,7,7,7,7,7,7,2,
+2,2,2,7,2,2,9,2,
+7,2,13,7,10,2,7,7,
+7,2,2,7,2,2,7,7,
+7,7,2,2,2,7,7,7
+</data>
+ </layer>
+</map>
diff --git a/levels/11.tmx b/levels/11.tmx
new file mode 100644 (file)
index 0000000..ae221a1
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,2,2,2,2,2,2,7,
+2,5,2,2,2,2,2,2,
+2,2,8,8,8,8,2,2,
+2,2,8,9,10,8,2,2,
+2,2,8,11,12,8,2,2,
+2,2,8,8,8,8,2,2,
+2,2,2,2,2,2,2,2,
+7,2,2,2,2,2,2,7
+</data>
+ </layer>
+</map>
diff --git a/levels/12.tmx b/levels/12.tmx
new file mode 100644 (file)
index 0000000..900246e
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+2,2,2,2,2,2,2,2,
+2,11,2,1,1,2,9,2,
+2,2,5,2,2,2,2,2,
+7,2,1,1,1,1,2,7,
+7,2,1,1,1,1,2,7,
+2,2,2,2,2,2,2,2,
+2,12,2,1,1,2,10,2,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
diff --git a/levels/13.tmx b/levels/13.tmx
new file mode 100644 (file)
index 0000000..ecdd229
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,1,2,2,2,2,1,7,
+1,12,2,2,2,2,2,2,
+8,2,5,1,2,1,2,2,
+8,2,1,2,2,2,1,2,
+8,2,2,2,2,10,2,16,
+8,2,1,2,11,2,1,2,
+1,2,2,1,2,1,2,2,
+7,2,2,2,13,2,2,2
+</data>
+ </layer>
+</map>
diff --git a/levels/14.tmx b/levels/14.tmx
new file mode 100644 (file)
index 0000000..adfdac6
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,7,7,2,2,2,7,7,
+7,7,2,2,7,2,7,7,
+7,2,2,5,7,2,7,7,
+7,2,2,2,7,2,2,7,
+7,2,1,7,1,1,2,7,
+2,14,2,2,2,2,16,2,
+2,2,2,2,2,2,16,2,
+2,14,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
diff --git a/levels/15.tmx b/levels/15.tmx
new file mode 100644 (file)
index 0000000..ff34f45
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,7,2,2,5,2,7,7,
+7,14,2,2,1,2,2,7,
+2,2,12,2,2,11,2,2,
+2,1,2,7,7,2,2,2,
+2,2,2,7,7,2,1,2,
+2,2,10,2,2,9,2,2,
+7,2,2,1,2,2,16,7,
+7,7,2,2,2,2,7,7
+</data>
+ </layer>
+</map>
diff --git a/levels/2.tmx b/levels/2.tmx
new file mode 100644 (file)
index 0000000..1825d80
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,7,7,7,7,7,7,7,
+7,7,2,2,2,16,7,7,
+7,7,7,7,7,2,7,7,
+2,2,5,2,2,2,2,2,
+7,7,2,2,2,7,7,2,
+7,7,12,2,7,7,7,2,
+7,13,1,2,2,2,2,2,
+7,7,7,7,7,7,7,7
+</data>
+ </layer>
+</map>
diff --git a/levels/3.tmx b/levels/3.tmx
new file mode 100644 (file)
index 0000000..a52e208
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,7,7,7,7,2,2,7,
+7,5,2,2,2,2,12,2,
+7,2,2,13,2,2,2,2,
+2,2,15,2,2,7,7,7,
+2,2,2,2,2,7,9,7,
+1,2,1,1,1,7,7,7,
+2,2,2,2,2,2,2,2,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
diff --git a/levels/4.tmx b/levels/4.tmx
new file mode 100644 (file)
index 0000000..93699ff
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,7,7,7,7,7,7,7,
+7,2,2,5,2,2,2,7,
+7,2,2,8,9,2,2,7,
+7,2,11,2,2,8,2,7,
+7,2,8,2,2,10,2,7,
+7,2,2,12,8,2,2,7,
+7,2,2,2,2,2,2,7,
+7,7,7,7,7,7,7,7
+</data>
+ </layer>
+</map>
diff --git a/levels/5.tmx b/levels/5.tmx
new file mode 100644 (file)
index 0000000..b071812
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+2,2,2,2,1,7,7,7,
+2,5,7,15,2,1,7,7,
+2,2,7,2,2,2,1,7,
+2,2,10,7,2,11,2,1,
+2,2,2,7,2,2,2,1,
+7,2,14,2,2,9,2,1,
+7,7,2,2,2,2,2,1,
+7,7,7,1,1,1,1,1
+</data>
+ </layer>
+</map>
diff --git a/levels/6.tmx b/levels/6.tmx
new file mode 100644 (file)
index 0000000..cd8ebeb
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,7,7,7,7,7,7,7,
+7,5,2,7,7,7,7,7,
+7,2,2,8,15,2,7,7,
+7,2,2,2,2,2,7,7,
+7,7,14,2,8,2,16,7,
+7,7,2,2,2,2,2,7,
+7,7,7,7,13,2,2,7,
+7,7,7,7,7,7,7,7
+</data>
+ </layer>
+</map>
diff --git a/levels/7.tmx b/levels/7.tmx
new file mode 100644 (file)
index 0000000..7775ad9
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,7,7,7,7,7,7,7,
+7,2,2,2,2,2,2,7,
+7,2,14,7,2,9,2,7,
+7,5,2,7,2,2,2,7,
+7,2,2,1,7,2,2,7,
+7,2,14,8,2,11,2,7,
+7,2,2,2,2,2,2,7,
+7,7,7,7,7,7,7,7
+</data>
+ </layer>
+</map>
diff --git a/levels/8.tmx b/levels/8.tmx
new file mode 100644 (file)
index 0000000..962c4d6
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+7,7,7,7,7,7,7,7,
+7,5,2,2,2,15,2,7,
+7,2,2,15,7,2,2,7,
+7,2,2,7,7,2,2,7,
+7,2,2,7,7,7,2,7,
+7,7,2,10,7,2,2,7,
+7,7,2,2,2,9,2,7,
+7,7,7,7,7,7,7,7
+</data>
+ </layer>
+</map>
diff --git a/levels/9.tmx b/levels/9.tmx
new file mode 100644 (file)
index 0000000..5e7aa95
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="-1" width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="8" height="8">
+  <data encoding="csv">
+2,2,2,5,2,2,2,2,
+2,7,7,7,7,7,7,2,
+2,7,2,15,2,8,7,2,
+2,7,2,8,12,2,7,2,
+2,7,2,9,8,2,7,2,
+2,7,8,2,13,2,7,2,
+2,7,7,7,7,7,7,2,
+2,2,2,2,2,2,2,2
+</data>
+ </layer>
+</map>
diff --git a/levels/mapcompile.py b/levels/mapcompile.py
new file mode 100755 (executable)
index 0000000..c5670df
--- /dev/null
@@ -0,0 +1,53 @@
+#!/usr/bin/python3
+
+import ulvl
+import sys
+
+if len(sys.argv) < 2:
+    print("usage:", sys.argv[0], "<infiles>")
+    sys.exit(1)
+
+screenwidth, screenheight = 8, 8
+
+tilemapping = { 0: 1, 4: 1, 5: 4, 6: 5, 7: 1, 8: 1, 9: 1, 10: 1, 11: 1, 12: 1, 13: 1, 14: 1, 15: 1 }
+
+stonemapping = { 0: 9, 7: 0, 8: 1, 9: 2, 10: 3, 11: 4, 12: 5, 13: 6, 14: 7, 15: 8 }
+
+char_index = 4
+
+print("var levels={")
+for filename in sys.argv[1:]:
+    m = ulvl.TMX.load(filename)
+
+    w = m.meta['width']
+    h = m.meta['height']
+
+    print('\t', filename.replace('.tmx', '').replace('levels/', ''), end=': { ')
+
+    stones = [ ]
+
+    print('map: [', end='')
+    start_x = 6
+    start_y = 4
+    for y in range(h):
+        for x in range(w):
+            thing = m.layers[0].tiles[y * w + x] - 1
+            if thing in stonemapping:
+                stones.append({ 'type': stonemapping[thing], 'x': x, 'y': y })
+            if thing == char_index:
+                start_x = x
+                start_y = y
+
+            print("" + str(tilemapping.get(thing, thing)) + ",", end='')
+    print('],', end='');
+
+    print('stones: [ ', end='')
+    for s in stones:
+        print('{ x:' + str(s['x']) + ', y:' + str(s['y']) + ', type:' + str(s['type']) + ' }, ', end='')
+    print('],', end='')
+
+    print(' start_x:' + str(start_x) + ', start_y:' + str(start_y), end='');
+
+    print(' },')
+
+print("}")
diff --git a/levels/tmptile.png b/levels/tmptile.png
new file mode 100644 (file)
index 0000000..baf8f89
Binary files /dev/null and b/levels/tmptile.png differ
diff --git a/levels/tmptile.tsx b/levels/tmptile.tsx
new file mode 100644 (file)
index 0000000..dff5641
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<tileset version="1.2" tiledversion="1.3.2" name="tmptile" tilewidth="16" tileheight="16" tilecount="10" columns="10">
+ <image source="tmptile.png" width="160" height="16"/>
+</tileset>
diff --git a/lizardfont.ttf b/lizardfont.ttf
new file mode 100644 (file)
index 0000000..754d0db
Binary files /dev/null and b/lizardfont.ttf differ
diff --git a/lizardfont.zip b/lizardfont.zip
new file mode 100644 (file)
index 0000000..fa7c858
Binary files /dev/null and b/lizardfont.zip differ
diff --git a/loading.png b/loading.png
new file mode 100644 (file)
index 0000000..7b41a63
Binary files /dev/null and b/loading.png differ
diff --git a/music/spacemagic.mp3 b/music/spacemagic.mp3
new file mode 100644 (file)
index 0000000..d59fc17
Binary files /dev/null and b/music/spacemagic.mp3 differ
diff --git a/packaging.zip b/packaging.zip
new file mode 100644 (file)
index 0000000..aae6fb6
Binary files /dev/null and b/packaging.zip differ
diff --git a/packaging/clicktostart.png b/packaging/clicktostart.png
new file mode 100644 (file)
index 0000000..9fd9728
Binary files /dev/null and b/packaging/clicktostart.png differ
diff --git a/packaging/game.js b/packaging/game.js
new file mode 100644 (file)
index 0000000..0894ba1
--- /dev/null
@@ -0,0 +1,1143 @@
+"use strict";
+
+let game;
+
+let game_started = false;
+
+let level_number = 1;
+
+let map;
+
+let intitle;
+let wonitall;
+
+/* --------- definitions ---------- */
+
+/* state:
+ * STAND: waiting for input
+ * MOVE: walking to another square
+ * CAST: casting a spell (successfully, i.e. pulling an object)
+ * CASTEND: done casting a spell but waiting for the magic to go away
+ * WIN: level complete
+ */
+let State = { STAND: 0, MOVE: 1, CAST: 2, CASTEND: 3, WIN: 4 };
+
+let can_continue = false;
+
+let save_data = 1;
+const SAVE_KEY = "casso.lizardwizard.save"
+
+zb.ready(function() {
+    game = zb.create_game({
+        canvas: 'canvas',
+        canvas_w: 800,
+        canvas_h: 800,
+        draw_scale: 3,
+        tile_size: 32,
+        level_w: 8,
+        level_h: 8,
+        background_color: '#36374b',
+        draw_func: do_draw,
+        update_func: do_update,
+        run_in_background: true,
+        save_key: SAVE_KEY,
+        state: State.STAND,
+        events: {
+            keydown: handle_keydown,
+            keyup: handle_keyup,
+            mouseup: handle_mouseup,
+            mousedown: handle_mousedown,
+            gamestart: handle_gamestart,
+        },
+    });
+
+    game.register_sfx({
+        slide: {
+            path: 'sfx/slide.wav',
+            volume: 0.75,
+        },
+        connect: {
+            path: 'sfx/connect.wav',
+            volume: 0.8,
+        },
+        disconnect: {
+            path: 'sfx/disconnect.wav',
+            volume: 0.5,
+        },
+        win: {
+            path: 'sfx/complete.wav',
+            volume: 1,
+        },
+        go: {
+            path: 'sfx/go.wav',
+            volume: 1,
+        },
+        step: {
+            path: 'sfx/step.wav',
+            volume: 0.3,
+        },
+        step2: {
+            path: 'sfx/step2.wav',
+            volume: 0.3,
+        },
+        step3: {
+            path: 'sfx/step3.wav',
+            volume: 0.3,
+        },
+    });
+
+    game.register_images({
+        character: 'img/lizwiz.png',
+        tiles: 'img/tiles.png',
+        magic: 'img/magic.png',
+        stones: 'img/stones.png',
+        beams: 'img/beams.png',
+        youdidit: 'img/youdidit.png',
+        endscreen: 'img/endscreen.png',
+        titlescreen: 'img/titlescreen.png',
+        levelimgs: {
+             1: 'img/level/1.png',
+             2: 'img/level/2.png',
+             3: 'img/level/3.png',
+             4: 'img/level/4.png',
+             5: 'img/level/5.png',
+             6: 'img/level/6.png',
+             7: 'img/level/7.png',
+             8: 'img/level/8.png',
+             9: 'img/level/9.png',
+            10: 'img/level/10.png',
+            11: 'img/level/11.png',
+            12: 'img/level/12.png',
+            13: 'img/level/13.png',
+            14: 'img/level/14.png',
+            15: 'img/level/15.png',
+        }
+    });
+
+    game.register_music({
+        magic: {
+            path: 'sfx/magic.wav',
+            volume: 0.7,
+        },
+        bgm: {
+            path: 'music/spacemagic',
+            volume: 0.95,
+        },
+    });
+
+    game.resources_ready();
+});
+
+let ID = {
+    lwall: 0,
+    floor: 1,
+    rwall: 2,
+    bothwall: 3,
+    gaptop: 4,
+    gap: 5,
+}
+
+let walkable = {
+    [ID.floor]: true,
+}
+
+let stoneID = {
+    blank: 0,
+    leftup: 1,
+    rightup: 2,
+    leftdown: 3,
+    rightdown: 4,
+    up: 5,
+    right: 6,
+    down: 7,
+    left: 8,
+    blocker: 9,
+};
+
+let beamID = {
+    leftright: 0,
+    updown: 1,
+    left: 2,
+    down: 3,
+    right: 4,
+    up: 5,
+};
+
+let connect_right_stones = {
+    [stoneID.rightup]: true,
+    [stoneID.rightdown]: true,
+    [stoneID.right]: true,
+};
+
+let connect_left_stones = {
+    [stoneID.leftup]: true,
+    [stoneID.leftdown]: true,
+    [stoneID.left]: true,
+};
+
+let connect_up_stones = {
+    [stoneID.leftup]: true,
+    [stoneID.rightup]: true,
+    [stoneID.up]: true,
+};
+
+let connect_down_stones = {
+    [stoneID.leftdown]: true,
+    [stoneID.rightdown]: true,
+    [stoneID.down]: true,
+};
+
+let dirs = {
+    down: 0,
+    right: 1,
+    up: 2,
+    left: 3,
+};
+
+let MAGIC_DIMENSION = 8;
+let MAGIC_TYPES = 24;
+
+/* ------ timers & static timer values --------- */
+
+let CHAR_MOVE_SPEED = 4;
+let OBJ_PULL_SPEED = 3.5;
+
+let magic_particle_spawn_timer = 0;
+let MAGIC_PARTICLE_SPAWN_GAP = 50;
+let MAGIC_PARTICLE_LIFESPAN = 800;
+let MAGIC_PARTICLE_LIFESPAN_VARIANCE = 150;
+
+let turn_timer = 0;
+let CHANGE_DIRECTION_GAP = 70;
+
+let OBJ_FADE_TIME = 800;
+
+/* ------- game global state -------- */
+
+let character = {};
+
+let objects = [];
+
+let magic_particles = [];
+
+let beams = [];
+
+let arrows = [];
+let space_pressed = false;
+
+let cast_target = null;
+
+/* ------- game behavior functions -------- */
+
+function change_anim(object, anim_name) {
+    if (object.current_anim !== anim_name) {
+        object.current_anim = anim_name;
+        object.current_frame = 0;
+        object.anim_timer = 0;
+    }
+}
+
+function complete_char_move(character) {
+    if (arrows.length === 0) {
+        change_anim(character, 'stand');
+        game.state = State.STAND;
+        play_step_sound();
+    } else {
+        check_arrow_inputs();
+    }
+}
+
+function check_all_stone_connections(playsfx) {
+    beams = [];
+
+    for (let o of objects) {
+        if (o.is_stone) {
+            o.was_already_on = false;
+            if (o.frame === 1) {
+                o.was_already_on = true;
+            }
+            o.frame = 0;
+        }
+    }
+
+    let level_complete = true;
+    for (let o of objects) {
+        if (o.is_stone) {
+            if (o.target_x === o.x && o.target_y === o.y) {
+                if (!check_connections(o)) {
+                    level_complete = false;
+                }
+            }
+        }
+    }
+
+    let connected = false, disconnected = false;
+    for (let o of objects) {
+        if (!o.was_already_on && o.frame === 1) {
+            connected = true;
+        }
+        if (o.was_already_on && o.frame === 0) {
+            disconnected = true;
+        }
+    }
+
+    if (connected && playsfx) {
+        game.sfx.connect.play();
+    }
+    if (disconnected && playsfx) {
+        game.sfx.disconnect.play();
+    }
+
+    if (level_complete) {
+        win();
+    }
+}
+
+function complete_stone_pull(stone) {
+    if (game.state === State.CAST || space_pressed) {
+        attempt_cast();
+    } else {
+        check_all_stone_connections(true);
+    }
+}
+
+function check_connections(stone) {
+    let friend;
+    let found_any = false;
+    let missed_any = false;
+    let missed = { up: false, down: false, left: false, right: false };
+    if (connect_up_stones[stone.variant]) {
+        friend = find_stone_friend(stone.x, stone.y, 0, -1);
+        if (friend && connect_down_stones[friend.variant]) {
+            connect_stones_vert(friend, stone);
+            found_any = true;
+        } else {
+            missed_any = true;
+            missed.up = true;
+        }
+    }
+    if (connect_down_stones[stone.variant]) {
+        friend = find_stone_friend(stone.x, stone.y, 0, 1);
+        if (friend && connect_up_stones[friend.variant]) {
+            connect_stones_vert(stone, friend);
+            found_any = true;
+        } else {
+            missed_any = true;
+            missed.down = true;
+        }
+    }
+    if (connect_left_stones[stone.variant]) {
+        friend = find_stone_friend(stone.x, stone.y, -1, 0);
+        if (friend && connect_right_stones[friend.variant]) {
+            connect_stones_horiz(friend, stone);
+            found_any = true;
+        } else {
+            missed_any = true;
+            missed.left = true;
+        }
+    }
+    if (connect_right_stones[stone.variant]) {
+        friend = find_stone_friend(stone.x, stone.y, 1, 0);
+        if (friend && connect_left_stones[friend.variant]) {
+            connect_stones_horiz(stone, friend);
+            found_any = true;
+        } else {
+            missed_any = true;
+            missed.right = true;
+        }
+    }
+
+    if (found_any && missed_any) {
+        if (missed.left) {
+            beams.push({
+                x: stone.x - 1,
+                y: stone.y,
+                sprite: {
+                    img: game.img.beams,
+                    y_offset: 4,
+                },
+                variant: beamID.right,
+            });
+        }
+        if (missed.right) {
+            beams.push({
+                x: stone.x + 1,
+                y: stone.y,
+                sprite: {
+                    img: game.img.beams,
+                    y_offset: 4,
+                },
+                variant: beamID.left,
+            });
+        }
+        if (missed.up) {
+            beams.push({
+                x: stone.x,
+                y: stone.y - 1,
+                sprite: {
+                    img: game.img.beams,
+                    y_offset: 4,
+                },
+                variant: beamID.down,
+            });
+        }
+        if (missed.down) {
+            beams.push({
+                x: stone.x,
+                y: stone.y + 1,
+                sprite: {
+                    img: game.img.beams,
+                    y_offset: 4,
+                },
+                variant: beamID.up,
+            });
+        }
+    }
+
+    return !missed_any;
+}
+
+function find_stone_friend(x, y, dx, dy) {
+    let sx = x + dx, sy = y + dy;
+    let friend = null;
+
+    do {
+        let objs = objs_at(sx, sy);
+        for (let o of objs) {
+            if (o.is_stone) {
+                friend = o;
+            }
+        }
+        sx += dx;
+        sy += dy;
+    } while (sx >= 0 && sy >= 0 && sx < game.level_w && sy < game.level_h && !friend);
+
+    return friend;
+}
+
+function connect_stones_horiz(left_stone, right_stone) {
+    if (left_stone.target_x !== left_stone.x || left_stone.target_y !== left_stone.y) return;
+    if (right_stone.target_x !== right_stone.x || right_stone.target_y !== right_stone.y) return;
+    left_stone.frame = 1;
+    right_stone.frame = 1;
+    for (let x = left_stone.x + 1; x < right_stone.x; x++) {
+        beams.push({
+            variant: beamID.leftright,
+            sprite: {
+                img: game.img.beams,
+                y_offset: 4,
+            },
+            x: x,
+            y: left_stone.y,
+        });
+    }
+}
+
+function connect_stones_vert(top_stone, bottom_stone) {
+    /*
+    let fade_beam = false;
+    if (!top_stone.was_already_on) {
+        fade_beam = true;
+        top_stone.fade = true;
+        top_stone.old_frame = 0;
+        top_stone.fade_fraction = 0;
+    }
+    if (!bottom_stone.was_already_on) {
+        fade_beam = true;
+        bottom_stone.fade = true;
+        bottom_stone.old_frame = 0;
+        bottom_stone.fade_fraction = 0;
+    }
+*/
+    if (top_stone.target_x !== top_stone.x || top_stone.target_y !== top_stone.y) return;
+    if (bottom_stone.target_x !== bottom_stone.x || bottom_stone.target_y !== bottom_stone.y) return;
+    top_stone.frame = 1;
+    bottom_stone.frame = 1;
+    for (let y = top_stone.y + 1; y < bottom_stone.y; y++) {
+        beams.push({
+            variant: beamID.updown,
+            sprite: {
+                img: game.img.beams,
+                y_offset: 4,
+            },
+            x: top_stone.x,
+            y: y,
+        });
+    }
+}
+
+function start_cast() {
+    if (game.state === State.STAND) {
+        attempt_cast();
+    }
+}
+
+function attempt_cast() {
+    change_anim(character, 'cast');
+
+    game.state = State.CAST;
+    let sdx = 0, sdy = 0;
+    switch (character.dir) {
+        case dirs.right:
+            sdx = 1;
+            break;
+        case dirs.down:
+            sdy = 1;
+            break;
+        case dirs.left:
+            sdx = -1;
+            break;
+        case dirs.up:
+            sdy = -1;
+            break;
+    }
+    let target_x = character.x + sdx;
+    let target_y = character.y + sdy;
+    let found_obj = null;
+    do {
+        let objs = objs_at(target_x, target_y);
+        for (let o of objs) {
+            if (o.pullable) {
+                found_obj = objs[0];
+            } else if (o.blocks) {
+                target_x = 10000;
+            }
+        }
+        target_x += sdx;
+        target_y += sdy;
+    } while (!found_obj && target_x >= 0 && target_y >= 0 && target_x < game.level_w && target_y < game.level_h);
+
+    if (found_obj && found_obj.pullable) {
+        cast_target = found_obj;
+        game.music.magic.play();
+        let can_move = check_move(cast_target.x, cast_target.y, -sdx, -sdy);
+        if (can_move) {
+            cast_target.target_x = cast_target.x - sdx;
+            cast_target.target_y = cast_target.y - sdy;
+            cast_target.move_speed = OBJ_PULL_SPEED;
+            cast_target.move_fraction = 0;
+            cast_target.completed_move = complete_stone_pull;
+            if (cast_target.x - sdx === character.x && cast_target.y - sdy === character.y) {
+                cast_target.target_x = cast_target.x;
+                cast_target.target_y = cast_target.y;
+            } else {
+                game.sfx.slide.play();
+                create_undo_point();
+            }
+        }
+        check_all_stone_connections(true);
+    }
+}
+
+function cancel_cast() {
+    if (game.state === State.STAND) {
+        change_anim(character, 'stand');
+    }
+
+    if (game.state === State.CAST) {
+        game.state = State.CASTEND;
+    }
+}
+
+function delete_save() {
+    try {
+        game.save('level_num', 1);
+    } catch (e) {
+        console.error("oops, can't save! though that uh... doesn't matter here");
+    }
+}
+
+function save() {
+    try {
+        console.log("Saving");
+        let save_data = level_number;
+        if (game.state === State.WIN) {
+            console.log("on next level");
+            save_data = level_number + 1;
+        }
+        game.save('level_num', save_data);
+    } catch (e) {
+        console.error("oops, can't save!", e);
+    }
+}
+
+function play_step_sound() {
+    // attempted to add this last minute, doesn't work well
+    /*let step = Math.floor(Math.random() * 3);
+    switch (step) {
+        case 0:
+            game.sfx.step.play();
+            break;
+        case 1:
+            game.sfx.step2.play();
+            break;
+        case 2:
+            game.sfx.step3.play();
+            break;
+    }*/
+}
+
+function handle_gamestart(game) {
+    console.log("Game start!");
+
+    intitle = true;
+    wonitall = false;
+
+    character = {
+        x: 6,
+        y: 6,
+        sprite: {
+            img: game.img.character,
+            w: 40,
+            h: 40,
+            x_offset: 4,
+            y_offset: 8,
+        },
+        move_speed: CHAR_MOVE_SPEED,
+        completed_move: complete_char_move,
+        dir: dirs.right,
+        current_anim: "stand",
+        current_frame: 0,
+        anim_timer: 0,
+        anims: {
+            stand: [ [0, 1000] ],
+            walk: [ [1, 150], [0, 150], [2, 150], [0, 150] ],
+            cast: [ [3, 1000] ],
+        },
+    };
+
+    let save_data = parseInt(game.load('level_num') || "1");
+    console.log("save data: ", save_data);
+    level_number = save_data;
+    load_level();
+}
+
+function objs_at(x, y) {
+    return objects.filter(o => o.x === x && o.y === y);
+}
+
+function tile_at(x, y) {
+    return map[y * game.level_w + x];
+}
+
+let undo_stack = [];
+
+function create_undo_point() {
+    let copied_objs = zb.copy_flat_objlist(objects.filter(o => o !== character));
+
+    let undo_point = {
+        char_x: character.x,
+        char_y: character.y,
+        char_dir: character.dir,
+        objs: copied_objs,
+        beams: zb.copy_flat_objlist(beams),
+    }
+
+    undo_stack.push(undo_point);
+}
+
+function undo() {
+    let undo_point = undo_stack.pop();
+    if (!undo_point) return;
+
+    objects = undo_point.objs;
+    objects.push(character);
+
+    beams = undo_point.beams;
+    character.x = undo_point.char_x;
+    character.y = undo_point.char_y;
+    character.dir = undo_point.char_dir;
+    change_anim(character, "stand");
+    can_continue = false;
+
+    for (let o of objects) {
+        o.target_x = o.x;
+        o.target_y = o.y;
+    }
+
+    game.state = State.STAND;
+    check_all_stone_connections(false);
+
+    console.log(game.state);
+}
+
+function reset() {
+    game.start_transition(zb.transition.FADE, 500, function() {
+        load_level();
+        game.state = State.STAND;
+    });
+}
+
+function advance_level() {
+    if (!can_continue) return;
+    console.log("advlevel");
+    game.start_transition(zb.transition.SLIDE_DOWN, 750, function() {
+        level_number ++;
+        load_level();
+        can_continue = false;
+        character.dir = dirs.right;
+        game.state = State.STAND;
+    }, function() {
+        game.sfx.go.play();
+    });
+}
+
+function win_everything() {
+    game.long_transition(zb.transition.FADE, 1000, function() {
+        wonitall = true;
+        delete_save();
+    });
+}
+
+function load_level() {
+    if (level_number > Object.keys(levels).length) {
+        win_everything();
+    } else {
+        load_level_data(levels[level_number]);
+    }
+}
+
+function load_level_data(lvl) {
+    undo_stack = [];
+
+    beams = [];
+
+    character.target_x = character.x = lvl.start_x;
+    character.target_y = character.y = lvl.start_y;
+    character.dir = dirs.right;
+    change_anim(character, 'stand');
+
+    game.state = State.STAND;
+
+    map = lvl.map;
+
+    objects = [ character ];
+
+    for (let s of lvl.stones) {
+        let stone = {
+            x: s.x,
+            y: s.y,
+            target_x: s.x,
+            target_y: s.y,
+            move_fraction: 0,
+            sprite: {
+                img: game.img.stones,
+                w: 40,
+                h: 40,
+                x_offset: 4,
+                y_offset: 8,
+            },
+            variant: s.type,
+            pullable: s.type !== stoneID.blocker,
+            blocks: s.type === stoneID.blocker,
+            is_stone: true,
+        };
+        objects.push(stone);
+    }
+
+    check_all_stone_connections(false);
+}
+
+function check_victory() {
+    win();
+}
+
+function win() {
+    console.log("You won!");
+    game.state = State.WIN;
+    game.music.magic.pause();
+    save();
+    window.setTimeout(function() {
+        game.sfx.win.play();
+        can_continue = true;
+    }, 350);
+}
+
+function check_move(x, y, dx, dy) {
+    if (x + dx < 0 || y + dy < 0 || x + dx >= game.level_w || y + dy >= game.level_h) {
+        return false;
+    }
+
+    if (!walkable[tile_at(x + dx, y + dy)]) {
+        return false;
+    }
+
+    let blocking_objs = objs_at(x + dx, y + dy);
+    if (blocking_objs.length > 0) {
+        return false;
+    }
+
+    return true;
+}
+
+function do_move(dir, dx, dy) {
+    if (game.state === State.WIN) return;
+
+    if (character.current_anim === 'cast') return;
+
+    if (dir !== character.dir) {
+        turn_timer = 0;
+    }
+
+    character.dir = dir;
+
+    if (turn_timer < CHANGE_DIRECTION_GAP) {
+        game.state = State.STAND;
+        return;
+    }
+
+    let can_move = check_move(character.x, character.y, dx, dy);
+
+    if (!can_move) {
+        game.state = State.STAND;
+        if (character.current_anim === 'walk') {
+            play_step_sound();
+        }
+        change_anim(character, 'stand');
+        return;
+    }
+
+    create_undo_point();
+
+    character.target_x = character.x + dx;
+    character.target_y = character.y + dy;
+    character.move_fraction = 0;
+    change_anim(character, 'walk');
+    game.state = State.MOVE;
+}
+
+/* ---------- update functions ------------ */
+
+function check_arrow_inputs() {
+    if (arrows.length > 0) {
+        switch (arrows[arrows.length - 1]) {
+            case dirs.right:
+                do_move(dirs.right, 1, 0);
+                break;
+            case dirs.down:
+                do_move(dirs.down, 0, 1);
+                break;
+            case dirs.left:
+                do_move(dirs.left, -1, 0);
+                break;
+            case dirs.up:
+                do_move(dirs.up, 0, -1);
+                break;
+        }
+    } else {
+        game.state = State.STAND;
+    }
+}
+
+/* MAIN UPDATE FUNCTION */
+function do_update(delta) {
+    turn_timer += delta;
+
+    if (game.state === State.STAND) {
+        check_arrow_inputs();
+    }
+
+    update_magic_particles(delta);
+
+    for (let o of objects) {
+        if (o.current_anim) {
+            o.anim_timer += delta;
+            while (o.anim_timer > o.anims[o.current_anim][o.current_frame][1]) {
+                o.anim_timer -= o.anims[o.current_anim][o.current_frame][1];
+                o.current_frame ++;
+                o.current_frame = zb.mod(o.current_frame, o.anims[o.current_anim].length);
+                if (o === character && o.current_anim === 'walk' && o.anims[o.current_anim][o.current_frame][0] !== 0) {
+                    play_step_sound();
+                }
+            }
+        }
+
+        if (o.target_x !== o.x || o.target_y !== o.y) {
+            o.move_fraction += o.move_speed * delta / 1000;
+            if (o.move_fraction > 1) {
+                o.x = o.target_x;
+                o.y = o.target_y;
+                o.move_fraction = 0;
+                o.completed_move(o);
+            }
+        }
+    }
+
+    if (arrows.length === 0 && character.x === character.target_x && character.y === character.target_y && game.state === State.STAND) {
+        change_anim(character, 'stand');
+    }
+}
+
+function update_magic_particles(delta) {
+    if (cast_target && (game.state === State.CAST || cast_target.target_x !== cast_target.x || cast_target.target_y !== cast_target.y)) {
+        magic_particle_spawn_timer += delta;
+        while (magic_particle_spawn_timer > MAGIC_PARTICLE_SPAWN_GAP) {
+            magic_particle_spawn_timer -= MAGIC_PARTICLE_SPAWN_GAP;
+            let ctx = cast_target.x * game.tile_size * (1 - cast_target.move_fraction) + cast_target.target_x * game.tile_size * cast_target.move_fraction;
+            let cty = cast_target.y * game.tile_size * (1 - cast_target.move_fraction) + cast_target.target_y * game.tile_size * cast_target.move_fraction - 3;
+            let target_x_offset = 12;
+            if (character.dir === dirs.up) {
+                target_x_offset = 28;
+            }
+            magic_particles.push({
+                x: ctx + Math.random() * game.tile_size,
+                y: cty + Math.random() * game.tile_size,
+                target_x: character.x * game.tile_size + target_x_offset - character.sprite.x_offset,
+                target_y: character.y * game.tile_size + 19 - character.sprite.y_offset,
+                progress: 0,
+                lifespan: MAGIC_PARTICLE_LIFESPAN + (Math.random() * 2 - 1) * MAGIC_PARTICLE_LIFESPAN_VARIANCE,
+                type: Math.floor(Math.random() * MAGIC_TYPES),
+            });
+        }
+    }
+
+    if (game.state === State.CASTEND) {
+        if (!cast_target || cast_target.target_x === cast_target.x && cast_target.target_y === cast_target.y) {
+            for (let p of magic_particles) {
+                p.lifespan *= 0.92;
+            }
+        }
+    }
+
+    for (let p of magic_particles) {
+        p.progress += delta / p.lifespan;
+        if (p.progress >= 1) {
+            p.deleteme = true;
+        }
+    }
+
+    magic_particles = magic_particles.filter(p => !p.deleteme);
+    if (game.state === State.CASTEND && magic_particles.length === 0) {
+        change_anim(character, 'stand');
+        cast_target = null;
+        game.state = State.STAND;
+        game.music.magic.pause();
+    }
+}
+
+/* ---------- draw functions ----------- */
+
+/* DRAW */
+function do_draw(ctx) {
+    if (intitle) {
+        zb.screen_draw(ctx, game.img.titlescreen);
+        return;
+    }
+
+    if (wonitall) {
+        zb.screen_draw(ctx, game.img.endscreen);
+        return;
+    }
+
+    ctx.save();
+    ctx.translate(5, 5);
+
+    draw_map(ctx);
+
+    if (game.img.levelimgs.hasOwnProperty(level_number)) {
+        ctx.save();
+        ctx.translate(-5, -5);
+        zb.screen_draw(ctx, game.img.levelimgs[level_number]);
+        ctx.restore();
+    }
+
+    let drawables = zb.copy_list(objects).concat(zb.copy_list(beams));
+
+    drawables.sort((a, b) => {
+        let amf = a.move_fraction || 0;
+        let bmf = b.move_fraction || 0;
+        let aty = a.target_y || 0;
+        let bty = b.target_y || 0;
+        let ay = Math.max(a.y, aty); /*a.y * (1 - amf) + aty * amf;*/
+        let by = b.y * (1 - bmf) + bty * bmf;
+
+        if (ay < by) {
+            return -1;
+        }
+        if (ay > by) {
+            return 1;
+        }
+        if (a === character) {
+            return 1;
+        }
+        if (b === character) {
+            return -1;
+        }
+        return 0;
+    });
+
+    for (let d of drawables) {
+        if (!d.sprite && !d.img) continue;
+        let sprite = d.sprite ? d.sprite.img : d.img;
+        let sw = d.sprite ? d.sprite.w || game.tile_size : game.tile_size;
+        let sh = d.sprite ? d.sprite.h || game.tile_size : game.tile_size;
+        let xo = d.sprite ? d.sprite.x_offset || 0 : 0;
+        let yo = d.sprite ? d.sprite.y_offset || 0 : 0;
+        let tx = d.target_x !== undefined ? d.target_x : d.x;
+        let ty = d.target_y !== undefined ? d.target_y : d.y;
+        let mf = d.move_fraction || 0;
+        let dir = d.dir || d.variant || 0;
+        let frame = d.frame || 0;
+        if (d.anims && d.current_anim && d.current_frame !== undefined) {
+            frame = d.anims[d.current_anim][d.current_frame][0];
+        }
+
+        if (d === character && d.dir === dirs.left) {
+            /* we draw magic particles at same time as lizard, before/after lizard depending on whether you're facing left or not.
+             * when you're facing left, the wand is behind the lizard's face, so the particles should also go behind. */
+            draw_magic_particles(ctx);
+        }
+
+        zb.sprite_draw(ctx, sprite, sw, sh, dir, frame,
+                    (d.x * (1 - mf) + tx * mf) * game.tile_size - xo, (d.y * (1 - mf) + ty * mf) * game.tile_size - yo);
+
+        if (d === character && d.dir !== dirs.left && d.dir !== dirs.down) {
+            draw_magic_particles(ctx);
+        }
+    }
+
+    if (character.dir === dirs.down) {
+        /* If character is facing down, the thing we are pulling gets drawn after us, so draw magic particles at the very end. */
+        draw_magic_particles(ctx);
+    }
+
+    if (game.state === State.WIN && can_continue) {
+        zb.screen_draw(ctx, game.img.youdidit);
+    }
+
+    ctx.restore();
+}
+
+function draw_magic_particles(ctx) {
+    for (let p of magic_particles) {
+        let interp = Math.pow(p.progress, 1.5);
+        zb.sprite_draw(ctx, game.img.magic, MAGIC_DIMENSION, MAGIC_DIMENSION, p.type, 0,
+                    p.x * (1 - interp) + p.target_x * interp - MAGIC_DIMENSION / 2, p.y * (1 - interp) + p.target_y * interp - MAGIC_DIMENSION / 2);
+    }
+}
+
+function draw_beams(ctx) {
+    for (let b of beams) {
+        zb.sprite_draw(ctx, game.img.beams, game.tile_size, game.tile_size, b.type, 0, b.x * game.tile_size, b.y * game.tile_size - 4);
+    }
+}
+
+function draw_map(ctx) {
+    for (let y = 0; y < game.level_h; y++) {
+        for (let x = 0; x < game.level_w; x++) {
+            let tile = map[y * game.level_w + x];
+            let variant = (x + y) % 2;
+            zb.sprite_draw(ctx, game.img.tiles, game.tile_size, game.tile_size, tile, variant, x * game.tile_size, y * game.tile_size);
+        }
+    }
+}
+
+/* ---------- event handlers ------------ */
+
+let z_presses = 0;
+function handle_keydown(game, e) {
+    if (wonitall || intitle) return;
+    if (e.repeat && e.key !== 'z') return;
+    if (game.transition.is_transitioning) return;
+
+    if (game.state === State.WIN && e.key !== 'r' && e.key !== 'z') {
+        advance_level();
+        return;
+    }
+
+    switch (e.key) {
+        case 'ArrowRight':
+            arrows.push(dirs.right);
+            break;
+        case 'ArrowDown':
+            arrows.push(dirs.down);
+            break;
+        case 'ArrowLeft':
+            arrows.push(dirs.left);
+            break;
+        case 'ArrowUp':
+            arrows.push(dirs.up);
+            break;
+        case ' ':
+            space_pressed = true;
+            start_cast();
+            break;
+        case 'z':
+            if (!e.repeat || z_presses % 3 === 0) {
+                undo();
+            }
+            z_presses ++;
+            e.preventDefault();
+            break;
+    }
+}
+
+let x_pressed = false;
+function handle_keyup(game, e) {
+    if (wonitall) return;
+    if (game.transition.is_transitioning) return;
+
+    // key up
+    switch (e.key) {
+        case 'ArrowRight':
+            arrows = arrows.filter(a => a !== dirs.right);
+            break;
+        case 'ArrowDown':
+            arrows = arrows.filter(a => a !== dirs.down);
+            break;
+        case 'ArrowLeft':
+            arrows = arrows.filter(a => a !== dirs.left);
+            break;
+        case 'ArrowUp':
+            arrows = arrows.filter(a => a !== dirs.up);
+            break;
+        case ' ':
+            space_pressed = false;
+            cancel_cast();
+            break;
+        case 'm':
+            game.toggle_mute();
+            e.preventDefault();
+            break;
+        case 'r':
+            reset();
+            e.preventDefault();
+            break;
+        case 'x':
+            x_pressed = true;
+            break;
+        case 'w':
+            if (x_pressed) {
+                delete_save();
+                e.preventDefault();
+            }
+            break;
+    }
+
+    if (e.keyCode !== 88) {
+        /* non-X key */
+        x_pressed = false;
+    }
+}
+
+function handle_mousedown(game) {
+    // click down
+}
+
+function handle_mouseup(game) {
+    if (intitle) {
+        game.long_transition(zb.transition.FADE, 1000, function() {
+            load_level();
+            game.music.bgm.play();
+            intitle = false;
+        });
+    }
+
+    if (wonitall) {
+        game.long_transition(zb.transition.FADE, 1000, function() {
+            wonitall = false;
+            level_number = 1;
+            intitle = true;
+        });
+    }
+}
diff --git a/packaging/img/beams.png b/packaging/img/beams.png
new file mode 100644 (file)
index 0000000..00b5c56
Binary files /dev/null and b/packaging/img/beams.png differ
diff --git a/packaging/img/endscreen.png b/packaging/img/endscreen.png
new file mode 100644 (file)
index 0000000..935af74
Binary files /dev/null and b/packaging/img/endscreen.png differ
diff --git a/packaging/img/level/1.png b/packaging/img/level/1.png
new file mode 100644 (file)
index 0000000..d9553d1
Binary files /dev/null and b/packaging/img/level/1.png differ
diff --git a/packaging/img/level/10.png b/packaging/img/level/10.png
new file mode 100644 (file)
index 0000000..472c94f
Binary files /dev/null and b/packaging/img/level/10.png differ
diff --git a/packaging/img/level/11.png b/packaging/img/level/11.png
new file mode 100644 (file)
index 0000000..415800c
Binary files /dev/null and b/packaging/img/level/11.png differ
diff --git a/packaging/img/level/12.png b/packaging/img/level/12.png
new file mode 100644 (file)
index 0000000..79fea49
Binary files /dev/null and b/packaging/img/level/12.png differ
diff --git a/packaging/img/level/13.png b/packaging/img/level/13.png
new file mode 100644 (file)
index 0000000..212fa64
Binary files /dev/null and b/packaging/img/level/13.png differ
diff --git a/packaging/img/level/14.png b/packaging/img/level/14.png
new file mode 100644 (file)
index 0000000..992c12d
Binary files /dev/null and b/packaging/img/level/14.png differ
diff --git a/packaging/img/level/15.png b/packaging/img/level/15.png
new file mode 100644 (file)
index 0000000..7d4bbf0
Binary files /dev/null and b/packaging/img/level/15.png differ
diff --git a/packaging/img/level/2.png b/packaging/img/level/2.png
new file mode 100644 (file)
index 0000000..b472439
Binary files /dev/null and b/packaging/img/level/2.png differ
diff --git a/packaging/img/level/3.png b/packaging/img/level/3.png
new file mode 100644 (file)
index 0000000..d15656c
Binary files /dev/null and b/packaging/img/level/3.png differ
diff --git a/packaging/img/level/4.png b/packaging/img/level/4.png
new file mode 100644 (file)
index 0000000..3fa1154
Binary files /dev/null and b/packaging/img/level/4.png differ
diff --git a/packaging/img/level/5.png b/packaging/img/level/5.png
new file mode 100644 (file)
index 0000000..e37806f
Binary files /dev/null and b/packaging/img/level/5.png differ
diff --git a/packaging/img/level/6.png b/packaging/img/level/6.png
new file mode 100644 (file)
index 0000000..18b3e06
Binary files /dev/null and b/packaging/img/level/6.png differ
diff --git a/packaging/img/level/7.png b/packaging/img/level/7.png
new file mode 100644 (file)
index 0000000..76cec03
Binary files /dev/null and b/packaging/img/level/7.png differ
diff --git a/packaging/img/level/8.png b/packaging/img/level/8.png
new file mode 100644 (file)
index 0000000..f9a3c74
Binary files /dev/null and b/packaging/img/level/8.png differ
diff --git a/packaging/img/level/9.png b/packaging/img/level/9.png
new file mode 100644 (file)
index 0000000..8db3aca
Binary files /dev/null and b/packaging/img/level/9.png differ
diff --git a/packaging/img/lizwiz.png b/packaging/img/lizwiz.png
new file mode 100644 (file)
index 0000000..8cfab39
Binary files /dev/null and b/packaging/img/lizwiz.png differ
diff --git a/packaging/img/magic.png b/packaging/img/magic.png
new file mode 100644 (file)
index 0000000..0c7fb60
Binary files /dev/null and b/packaging/img/magic.png differ
diff --git a/packaging/img/stones.png b/packaging/img/stones.png
new file mode 100644 (file)
index 0000000..185fae5
Binary files /dev/null and b/packaging/img/stones.png differ
diff --git a/packaging/img/tiles.png b/packaging/img/tiles.png
new file mode 100644 (file)
index 0000000..2d01e0d
Binary files /dev/null and b/packaging/img/tiles.png differ
diff --git a/packaging/img/titlescreen.png b/packaging/img/titlescreen.png
new file mode 100644 (file)
index 0000000..2d44468
Binary files /dev/null and b/packaging/img/titlescreen.png differ
diff --git a/packaging/img/youdidit.png b/packaging/img/youdidit.png
new file mode 100644 (file)
index 0000000..b17e59b
Binary files /dev/null and b/packaging/img/youdidit.png differ
diff --git a/packaging/index.html b/packaging/index.html
new file mode 100644 (file)
index 0000000..5486da0
--- /dev/null
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+<head><title>
+    lizard wizard
+</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 }</style>
+</head>
+<body style="background: black">
+    <canvas id="canvas" width="798" height="798" style="max-width:100%" tabindex=1> </canvas>
+</body>
+<script src="levels.js"></script>
+<script src="zucchinibread.js"></script>
+<script src="game.js"></script>
+</html>
diff --git a/packaging/levels.js b/packaging/levels.js
new file mode 100644 (file)
index 0000000..53528b7
--- /dev/null
@@ -0,0 +1,17 @@
+var levels={
+        10: { map: [5,5,1,1,1,1,1,1,1,1,1,5,1,1,1,1,1,1,1,5,1,1,1,1,1,5,5,5,5,5,5,1,1,1,1,5,1,1,1,1,5,1,1,5,1,1,5,5,5,1,1,5,1,1,5,5,5,5,1,1,1,5,5,5,],stones: [ { x:6, y:1, type:3 }, { x:1, y:2, type:4 }, { x:5, y:2, type:7 }, { x:6, y:2, type:9 }, { x:6, y:4, type:1 }, { x:2, y:5, type:5 }, { x:4, y:5, type:2 }, ], start_x:0, start_y:3 },
+        11: { map: [5,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,5,1,1,1,1,1,1,5,],stones: [ { x:2, y:2, type:0 }, { x:3, y:2, type:0 }, { x:4, y:2, type:0 }, { x:5, y:2, type:0 }, { x:2, y:3, type:0 }, { x:3, y:3, type:1 }, { x:4, y:3, type:2 }, { x:5, y:3, type:0 }, { x:2, y:4, type:0 }, { x:3, y:4, type:3 }, { x:4, y:4, type:4 }, { x:5, y:4, type:0 }, { x:2, y:5, type:0 }, { x:3, y:5, type:0 }, { x:4, y:5, type:0 }, { x:5, y:5, type:0 }, ], start_x:1, start_y:1 },
+        12: { map: [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,],stones: [ { x:1, y:1, type:3 }, { x:3, y:1, type:9 }, { x:4, y:1, type:9 }, { x:6, y:1, type:1 }, { x:2, y:3, type:9 }, { x:3, y:3, type:9 }, { x:4, y:3, type:9 }, { x:5, y:3, type:9 }, { x:2, y:4, type:9 }, { x:3, y:4, type:9 }, { x:4, y:4, type:9 }, { x:5, y:4, type:9 }, { x:1, y:6, type:4 }, { x:3, y:6, type:9 }, { x:4, y:6, type:9 }, { x:6, y:6, type:2 }, ], start_x:2, start_y:2 },
+        13: { map: [5,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,5,1,1,1,1,1,1,1,],stones: [ { x:1, y:0, type:9 }, { x:6, y:0, type:9 }, { x:0, y:1, type:9 }, { x:1, y:1, type:4 }, { x:0, y:2, type:0 }, { x:3, y:2, type:9 }, { x:5, y:2, type:9 }, { x:0, y:3, type:0 }, { x:2, y:3, type:9 }, { x:6, y:3, type:9 }, { x:0, y:4, type:0 }, { x:5, y:4, type:2 }, { x:7, y:4, type:8 }, { x:0, y:5, type:0 }, { x:2, y:5, type:9 }, { x:4, y:5, type:3 }, { x:6, y:5, type:9 }, { x:0, y:6, type:9 }, { x:3, y:6, type:9 }, { x:5, y:6, type:9 }, { x:4, y:7, type:5 }, ], start_x:2, start_y:2 },
+        14: { map: [5,5,5,1,1,1,5,5,5,5,1,1,5,1,5,5,5,1,1,1,5,1,5,5,5,1,1,1,5,1,1,5,5,1,1,5,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,],stones: [ { x:2, y:4, type:9 }, { x:4, y:4, type:9 }, { x:5, y:4, type:9 }, { x:1, y:5, type:6 }, { x:6, y:5, type:8 }, { x:6, y:6, type:8 }, { x:1, y:7, type:6 }, ], start_x:3, start_y:2 },
+        15: { map: [5,5,1,1,1,1,5,5,5,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,1,1,1,1,1,5,1,1,1,1,1,1,5,5,5,1,1,1,1,5,5,],stones: [ { x:1, y:1, type:6 }, { x:4, y:1, type:9 }, { x:2, y:2, type:4 }, { x:5, y:2, type:3 }, { x:1, y:3, type:9 }, { x:6, y:4, type:9 }, { x:2, y:5, type:2 }, { x:5, y:5, type:1 }, { x:3, y:6, type:9 }, { x:6, y:6, type:8 }, ], start_x:4, start_y:0 },
+        1: { map: [5,5,5,5,5,5,5,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,5,5,5,5,5,5,5,],stones: [ { x:4, y:2, type:3 }, { x:3, y:3, type:2 }, { x:2, y:4, type:4 }, { x:5, y:5, type:1 }, ], start_x:3, start_y:5 },
+        2: { map: [5,5,5,5,5,5,5,5,5,5,1,1,1,1,5,5,5,5,5,5,5,1,5,5,1,1,1,1,1,1,1,1,5,5,1,1,1,5,5,1,5,5,1,1,5,5,5,1,5,1,1,1,1,1,1,1,5,5,5,5,5,5,5,5,],stones: [ { x:5, y:1, type:8 }, { x:2, y:5, type:4 }, { x:1, y:6, type:5 }, { x:2, y:6, type:9 }, ], start_x:2, start_y:3 },
+        3: { map: [5,5,5,5,5,1,1,5,5,1,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,1,1,1,5,5,5,1,1,1,1,1,5,1,5,1,1,1,1,1,5,5,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,],stones: [ { x:6, y:1, type:4 }, { x:3, y:2, type:5 }, { x:2, y:3, type:7 }, { x:6, y:4, type:1 }, { x:0, y:5, type:9 }, { x:2, y:5, type:9 }, { x:3, y:5, type:9 }, { x:4, y:5, type:9 }, ], start_x:1, start_y:1 },
+        4: { map: [5,5,5,5,5,5,5,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,5,5,5,5,5,5,5,],stones: [ { x:3, y:2, type:0 }, { x:4, y:2, type:1 }, { x:2, y:3, type:3 }, { x:5, y:3, type:0 }, { x:2, y:4, type:0 }, { x:5, y:4, type:2 }, { x:3, y:5, type:4 }, { x:4, y:5, type:0 }, ], start_x:3, start_y:1 },
+        5: { map: [1,1,1,1,1,5,5,5,1,1,5,1,1,1,5,5,1,1,5,1,1,1,1,5,1,1,1,5,1,1,1,1,1,1,1,5,1,1,1,1,5,1,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,5,1,1,1,1,1,],stones: [ { x:4, y:0, type:9 }, { x:3, y:1, type:7 }, { x:5, y:1, type:9 }, { x:6, y:2, type:9 }, { x:2, y:3, type:2 }, { x:5, y:3, type:3 }, { x:7, y:3, type:9 }, { x:7, y:4, type:9 }, { x:2, y:5, type:6 }, { x:5, y:5, type:1 }, { x:7, y:5, type:9 }, { x:7, y:6, type:9 }, { x:3, y:7, type:9 }, { x:4, y:7, type:9 }, { x:5, y:7, type:9 }, { x:6, y:7, type:9 }, { x:7, y:7, type:9 }, ], start_x:1, start_y:1 },
+        6: { map: [5,5,5,5,5,5,5,5,5,1,1,5,5,5,5,5,5,1,1,1,1,1,5,5,5,1,1,1,1,1,5,5,5,5,1,1,1,1,1,5,5,5,1,1,1,1,1,5,5,5,5,5,1,1,1,5,5,5,5,5,5,5,5,5,],stones: [ { x:3, y:2, type:0 }, { x:4, y:2, type:7 }, { x:2, y:4, type:6 }, { x:4, y:4, type:0 }, { x:6, y:4, type:8 }, { x:4, y:6, type:5 }, ], start_x:1, start_y:1 },
+        7: { map: [5,5,5,5,5,5,5,5,5,1,1,1,1,1,1,5,5,1,1,5,1,1,1,5,5,1,1,5,1,1,1,5,5,1,1,1,5,1,1,5,5,1,1,1,1,1,1,5,5,1,1,1,1,1,1,5,5,5,5,5,5,5,5,5,],stones: [ { x:2, y:2, type:6 }, { x:5, y:2, type:1 }, { x:3, y:4, type:9 }, { x:2, y:5, type:6 }, { x:3, y:5, type:0 }, { x:5, y:5, type:3 }, ], start_x:1, start_y:3 },
+        8: { map: [5,5,5,5,5,5,5,5,5,1,1,1,1,1,1,5,5,1,1,1,5,1,1,5,5,1,1,5,5,1,1,5,5,1,1,5,5,5,1,5,5,5,1,1,5,1,1,5,5,5,1,1,1,1,1,5,5,5,5,5,5,5,5,5,],stones: [ { x:5, y:1, type:7 }, { x:3, y:2, type:7 }, { x:3, y:5, type:2 }, { x:5, y:6, type:1 }, ], start_x:1, start_y:1 },
+        9: { map: [1,1,1,1,1,1,1,1,1,5,5,5,5,5,5,1,1,5,1,1,1,1,5,1,1,5,1,1,1,1,5,1,1,5,1,1,1,1,5,1,1,5,1,1,1,1,5,1,1,5,5,5,5,5,5,1,1,1,1,1,1,1,1,1,],stones: [ { x:3, y:2, type:7 }, { x:5, y:2, type:0 }, { x:3, y:3, type:0 }, { x:4, y:3, type:4 }, { x:3, y:4, type:1 }, { x:4, y:4, type:0 }, { x:2, y:5, type:0 }, { x:4, y:5, type:5 }, ], start_x:3, start_y:0 },
+}
diff --git a/packaging/license.txt b/packaging/license.txt
new file mode 100644 (file)
index 0000000..b95dde2
--- /dev/null
@@ -0,0 +1,163 @@
+FontStruct Non-Commercial Software End User License Agreement 1.0
+
+This software end user license agreement (hereinafter “Agreement”) is made
+between you or, if you represent a legal entity, that legal entity (hereinafter
+“You”) and “broomweed”, the copyright holder and designer (hereinafter
+“Font Designer”) of the font software named “lizardfont” (hereinafter
+“Font Software”).
+
+1. BINDING AGREEMENT
+This Agreement is a binding agreement between You and the Font Designer and
+shall set forth the terms and conditions under which You can use the Font
+Software. For the purposes of this Agreement, Font Software shall mean the Font
+Software, and any parts or copies of it.
+
+2. GRANT OF NON-EXCLUSIVE AND LICENCE
+The Font Designer hereby grants You a non-exclusive, non-transferable licence to
+use the Font Software provided that You comply with all terms and conditions of
+this Agreement.
+
+2.1 PERSONAL USE
+You may use the Fonts Software only for Your own Personal Purposes and for Your
+Work. For the purposes of this Agreement, “Personal Purposes” shall mean any
+private use of the Font Software by you. “Work” shall mean any work or
+result created by You or on Your behalf with the Font Software. For the
+avoidance of doubt, the use for Personal Purposes does not allow any use of the
+Font Software and/or any Work for commercial purposes (see Section 2.2) and/or
+as a web-font (see Section 2.6).
+
+2.2 NO COMMERCIAL USE
+Neither the Font Software, nor any of its individual components or any Work, may
+be used by You for any form of profit-making or otherwise commercial projects,
+without express prior written permission from the Font Designer.
+
+2.3 NO SALE OR DISTRIBUTION
+You may not sell, rent, license, sublicense, distribute, redistribute, give-away
+or make available (in any other way) the Font Software alone or as part of any
+collection, product or service to any third party. 
+
+2.4 NO MODIFICATION
+You may not modify, adapt, rename, translate, reverse engineer, decompile,
+disassemble, alter, or attempt to discover the source code of the Font Software.
+
+2.5 EMBEDDING
+You may embed the Font Software in documents, applications or devices either as
+a rasterized representation of the Font Software (e.g., a GIF or JPEG) or as a
+subset of the Font Software as long as the document, application or device is
+distributed in a secure format that permits only the viewing and printing but
+not the editing of the text.
+
+2.6 NO USE AS A WEBFONT
+You may not make the Font Software accessible on a web server in order to enable
+a web browser to render the content of Websites using the respective Font
+Software.
+
+2.7 BACKUP
+You may make backup copies of the Font Software for archival purposes only,
+provided that You retain exclusive custody and control over such copies. Any
+backup copy of the Font Software must contain the same copyright, trademark, and
+other proprietary information as the original.
+
+2.8 PRINT SHOPS AND SERVICE BUREAUS
+You may provide a copy of the Font Software used in Your Work to a print shop,
+service bureau, etc. for printing or otherwise outputting Your Work. Afterwards,
+the print shop, service bureau, etc. must destroy and/or delete all copies of
+the Font Software.
+
+3. ATTRIBUTION
+If You make Your Work publicly accessible in any form, You must keep intact all
+copyright notices for the Font Software and display, reasonable to the medium or
+means You are utilizing for Your Work, an attribution notice (hereinafter “The
+Attribution Notice”) comprising of:
+
+the name of the Font Designer or pseudonym, if applicable (“broomweed”);
+the title of the Font Software (“lizardfont”); and
+to the extent reasonably practicable, the URI
+(https://fontstruct.com/fontstructions/show/2462545) from where the Font was
+originally downloaded.
+
+The Attribution Notice may be implemented in any reasonable manner. For the
+avoidance of doubt, You may only use the credit required by this Section for the
+purpose of attribution in the manner set out above and, by exercising Your
+rights under this License, You may not implicitly or explicitly assert or imply
+any connection with, sponsorship or endorsement by the Font Designer of You or
+Your Work.
+
+4. YOUR REPRESENTATIONS AND INDEMNIFICATIONS
+You represent, warrant, and covenant that You shall use the Font Software only
+in accordance with the terms of this Agreement and keep the Font Designer fully
+indemnified against all actions, claims, costs, proceedings and damages
+whatsoever incurred by any breach of any of the aforesaid representations,
+warranties of this Agreement.
+
+5. OWNERSHIP AND RIGHTS TO THE FONT SOFTWARE
+The Font Designer reserves all rights not expressly granted to You in this
+Agreement (hereinafter “Reserved Rights”). The Font Software is protected by
+copyright and other intellectual property laws and treaties. You acknowledge
+that the Font Designer shall be the sole owner of the Font Software, of the
+copyright and all Reserved Rights of whatever kind and nature in the Font
+Software.
+
+6. WARRANTY
+The Font Software is made available “as is” WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT.THE FONT DESIGNER DOES NOT AND
+CANNOT GUARANTEE THE PERFORMANCE OR RESULTS YOU MAY OBTAIN BY USING THE FONT
+SOFTWARE.
+THE FONT DESIGNER SHALL IN NO EVENT BE LIABLE TO YOU FOR ANY CONSEQUENTIAL,
+INCIDENTAL OR SPECIAL DAMAGES, INCLUDING ANY LOST PROFITS OR LOST SAVINGS, OR
+FOR ANY CLAIM BY ANY THIRD PARTY, ARISING OUT OF THE USE OR INABILITY TO USE THE
+FONT SOFTWARE.
+
+7. TERMINATION / CONDITION PRECEDENT
+You may use the Font Software only as set forth in this Agreement. Any use of
+the Font Software that is not expressly allowed in this Agreement will
+automatically terminate Your rights to use the Font Software under this
+Agreement.  If Your right to use the Font Software is terminated You shall
+immediately stop using the Font Software, call back or change any Work that is
+used in the public, and delete/destroy any copies of the Font Software. The
+termination of the Agreement shall not affect the right of the Font Designer to
+claim damages or other rights.
+
+8. WAIVER
+A waiver by either Party of any term or condition of this Agreement shall not be
+deemed or construed to be a waiver of such term or condition for the future or
+any subsequent breach thereof.  All remedies, rights, undertakings, obligations
+and agreements contained in this Agreement shall be cumulative and none of them
+shall be in limitation of any other remedy, right, undertaking, obligation or
+agreement of either Party.
+
+9. GOVERNING LAW, PLACE OF JURISDICTION
+This Agreement shall be construed and shall take effect in accordance with the
+laws of the Federal Republic of Germany excluding the United Nations Convention
+on Contracts for the International Sales of Goods (CISG).  To the extent
+permitted by law, the Courts of Berlin, Germany shall have exclusive
+jurisdiction to resolve any dispute which may arise.
+
+10. ENTIRE AGREEMENT
+This Agreement contains the entire agreement between the Parties. No variation
+of any of the terms or conditions of this Agreement may be made unless such
+variation is agreed in writing and signed by You and the Font Designer.
+
+11. SEVERABILITY
+If any provision of this Agreement is adjudged by a court to be invalid or
+unenforceable, such provision shall in no way affect any other provision of this
+Agreement, the application of such provision in any other circumstance or the
+validity or enforceability of this Agreement.  The Parties will negotiate in
+good faith about a provision that will replace the invalid or unenforceable
+provision.
+
+FontStruct Notice: FontStruct no Party to this Agreement
+
+Please note that FontStruct (www.fontstruct.com) is not a party to this
+Agreement between You and the Font Designer.
+
+FontStruct makes no warranty of any kind in connection with the Font Software,
+its use or the results that You may obtain by using the Font Software.
+
+FontStruct shall in no event be liable to You or any third party for any damages
+whatsoever arising out of or relating to this Agreement or the use of the Font
+Software, including but not limited to incidental, special damages, any lost
+profits or lost savings or any claim made by a third party.
+
diff --git a/packaging/loading.png b/packaging/loading.png
new file mode 100644 (file)
index 0000000..7b41a63
Binary files /dev/null and b/packaging/loading.png differ
diff --git a/packaging/music/spacemagic.mp3 b/packaging/music/spacemagic.mp3
new file mode 100644 (file)
index 0000000..d59fc17
Binary files /dev/null and b/packaging/music/spacemagic.mp3 differ
diff --git a/packaging/pause.png b/packaging/pause.png
new file mode 100644 (file)
index 0000000..2530de4
Binary files /dev/null and b/packaging/pause.png differ
diff --git a/packaging/readme.txt b/packaging/readme.txt
new file mode 100644 (file)
index 0000000..de35809
--- /dev/null
@@ -0,0 +1,16 @@
+The font file in this archive was created using Fontstruct the free, online
+font-building tool.\r
+This font was created by “broomweed”.\r
+This font has a homepage where this archive and other versions may be found:
+https://fontstruct.com/fontstructions/show/2462545\r
+\r
+Try Fontstruct at https://fontstruct.com\r
+It’s easy and it’s fun.\r
+\r
+Fontstruct is copyright ©2024 Rob Meek\r
+\r
+LEGAL NOTICE:\r
+In using this font you must comply with the licensing terms described in the
+file “license.txt” included with this archive.\r
+If you redistribute the font file in this archive, it must be accompanied by all
+the other files from this archive, including this one.\r
diff --git a/packaging/saveerror.png b/packaging/saveerror.png
new file mode 100644 (file)
index 0000000..faa7606
Binary files /dev/null and b/packaging/saveerror.png differ
diff --git a/packaging/sfx/complete.wav b/packaging/sfx/complete.wav
new file mode 100644 (file)
index 0000000..0d9a0d3
Binary files /dev/null and b/packaging/sfx/complete.wav differ
diff --git a/packaging/sfx/connect.wav b/packaging/sfx/connect.wav
new file mode 100644 (file)
index 0000000..4aaa73e
Binary files /dev/null and b/packaging/sfx/connect.wav differ
diff --git a/packaging/sfx/disconnect.wav b/packaging/sfx/disconnect.wav
new file mode 100644 (file)
index 0000000..91545cc
Binary files /dev/null and b/packaging/sfx/disconnect.wav differ
diff --git a/packaging/sfx/go.wav b/packaging/sfx/go.wav
new file mode 100644 (file)
index 0000000..bd46924
Binary files /dev/null and b/packaging/sfx/go.wav differ
diff --git a/packaging/sfx/magic.wav b/packaging/sfx/magic.wav
new file mode 100644 (file)
index 0000000..0d347cd
Binary files /dev/null and b/packaging/sfx/magic.wav differ
diff --git a/packaging/sfx/slide.wav b/packaging/sfx/slide.wav
new file mode 100644 (file)
index 0000000..f9c9a81
Binary files /dev/null and b/packaging/sfx/slide.wav differ
diff --git a/packaging/sfx/step.wav b/packaging/sfx/step.wav
new file mode 100644 (file)
index 0000000..bb44de4
Binary files /dev/null and b/packaging/sfx/step.wav differ
diff --git a/packaging/sfx/step2.wav b/packaging/sfx/step2.wav
new file mode 100644 (file)
index 0000000..4891e41
Binary files /dev/null and b/packaging/sfx/step2.wav differ
diff --git a/packaging/sfx/step3.wav b/packaging/sfx/step3.wav
new file mode 100644 (file)
index 0000000..a8418d9
Binary files /dev/null and b/packaging/sfx/step3.wav differ
diff --git a/packaging/zucchinibread.js b/packaging/zucchinibread.js
new file mode 100644 (file)
index 0000000..f7b8ed3
--- /dev/null
@@ -0,0 +1,1150 @@
+"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 copy_list(list) {
+        let newlist = [];
+        for (let x of list) {
+            newlist.push(x);
+        }
+        return newlist;
+    }
+
+    function copy_flat_objlist(list) {
+        let newlist = [];
+        for (let x of list) {
+            newlist.push({ ...x });
+        }
+        return newlist;
+    }
+
+    /* ---- Resource loading / loading screen ---- */
+
+    let _audiocheck = document.createElement('audio');
+
+    let _SFX_ARRAY_SIZE = 10;
+
+    /* Returns a callback that should be called when a resource finishes loading. */
+    function _register_resource(name, game, callback) {
+        game._total_things_to_load ++;
+        console.log("Loading", name + ". Things to load:", game._total_things_to_load);
+        return function() {
+            if (!game.ready_to_go) {
+                game._things_loaded ++;
+                console.log("Loaded", name + ". Things loaded:", game._things_loaded, "/", game._total_things_to_load);
+                _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_array = new Array(_SFX_ARRAY_SIZE);
+            for (let i = 0; i < _SFX_ARRAY_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_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 */
+            },
+            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);
+            }
+        };
+
+        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.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.run_in_background) {
+            let pause_img = new Image();
+            pause_img.onload = _register_resource('pause.png', game);
+            pause_img.src = 'pause.png';
+            game.img._pause = pause_img;
+        }
+
+        if (game.hasOwnProperty('save_key')) {
+            let saveerror_img = new Image();
+            saveerror_img.onload = _register_resource('saveerror.png', game);
+            saveerror_img.src = 'saveerror.png';
+            game.img._saveerror = saveerror_img;
+        }
+
+        let clicktostart_img = new Image();
+        clicktostart_img.onload = _register_resource('clicktostart.png', game, function() {
+            if (game.ready_to_go) {
+                game.ctx.global.save();
+                game.ctx.global.scale(game.draw_scale, game.draw_scale);
+                game.ctx.global.drawImage(game.img._clicktostart, 0, 0);
+                game.ctx.global.restore();
+            }
+        });
+        clicktostart_img.src = 'clicktostart.png';
+        game.img._clicktostart = clicktostart_img;
+
+        return game;
+    }
+
+    /* ---- Audio ---- */
+
+    function _stop_music(game) {
+        if (game.muted) return;
+
+        for (let m in game.music) {
+            if (!game.music[m].paused) {
+                game.music[m].was_playing = true;
+                game.music[m].pause();
+            } else {
+                game.music[m].was_playing = false;
+            }
+        }
+    }
+
+    function _start_music(game, unmuting) {
+        if (game.muted && !unmuting) return;
+
+        for (let m in game.music) {
+            if (game.music[m].was_playing) {
+                game.music[m].play();
+            }
+        }
+    }
+
+    function _mute(game) {
+        _stop_music(game);
+        game.muted = true;
+    }
+
+    function _unmute(game) {
+        game.muted = false;
+        _start_music(game);
+    }
+
+    /* ---- Game update stuff ---- */
+
+    let _last_frame_time;
+    let _timedelta = 0;
+    function _loop(game, timestamp) {
+        if (timestamp == undefined) {
+            timestamp = 0;
+            _last_frame_time = timestamp;
+        }
+        _timedelta += timestamp - _last_frame_time;
+        _last_frame_time = timestamp;
+
+        while (_timedelta >= 1000 / game.frame_rate) {
+            _update(game, 1000 / game.frame_rate);
+            _timedelta -= 1000 / game.frame_rate;
+        }
+        _draw(game);
+
+        if (!game._norun) {
+            requestAnimFrame(function(timestamp) {
+                _loop(game, timestamp);
+            });
+        }
+    }
+
+    function _update(game, delta) {
+        game.update_func(delta);
+
+        if (game.transition.is_transitioning) {
+            game.transition.timer += delta;
+            if (game.transition.timer > game.transition.end_time) {
+                _finish_transition(game);
+            }
+        }
+    }
+
+    /* ---- Mouse ---- */
+
+    function _handle_mousedown(game, e) {
+        if (!game.playing) return;
+
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((e.clientX - rect.left) / game.draw_scale) - 1;
+        let y = Math.round((e.clientY - rect.top) / game.draw_scale) - 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;
+        }
+
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((e.clientX - rect.left) / game.draw_scale) - 1;
+        let y = Math.round((e.clientY - rect.top) / game.draw_scale) - 1;
+        if (e.button === 0 && game.events.mouseup) {
+            game.events.mouseup(game, e, x, y);
+        }
+    }
+
+    function _handle_mousemove(game, e) {
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((e.clientX - rect.left) / game.draw_scale) - 1;
+        let y = Math.round((e.clientY - rect.top) / game.draw_scale) - 1;
+        if (game.events.mousemove) {
+            game.events.mousemove(game, e, x, y);
+        }
+    }
+
+    /* ---- Drawing stuff ---- */
+
+    function _draw(game) {
+        let ctx = game.ctx.draw;
+
+        ctx.save();
+
+        ctx.fillStyle = game.background_color;
+
+        ctx.beginPath();
+        ctx.rect(0, 0, game.screen_w, game.screen_h);
+        ctx.fill();
+
+        game.draw_func(game.ctx.draw);
+
+        if (game.transition.mid_long) {
+            ctx.fillStyle = game.transition.color;
+            ctx.fillRect(-1, -1, game.canvas_w + 5, game.canvas_h + 5);
+        }
+
+        ctx.restore();
+
+        game.ctx.global.fillStyle = 'rgb(0, 0, 0)';
+        game.ctx.global.beginPath();
+        game.ctx.global.rect(0, 0, game.screen_w * game.draw_scale, game.screen_h * game.draw_scale);
+        game.ctx.global.fill();
+
+        game.ctx.global.save();
+
+        game.ctx.global.scale(game.draw_scale, game.draw_scale);
+
+        game.ctx.global.drawImage(ctx.canvas, 0, 0);
+
+        if (game.transition.is_transitioning) {
+            _draw_transition(game);
+        }
+
+        if (game._norun) {
+            game.ctx.global.drawImage(game.img._pause, 0, 0);
+        }
+
+        game.ctx.global.restore();
+    }
+
+    /* Draw a particular section of an image/spritesheet,
+     * without having to do as much math or type the destination size */
+    function sprite_draw(ctx, img, section_w, section_h, section_x, section_y, dest_x, dest_y) {
+        ctx.drawImage(img,
+            section_w * section_x, section_h * section_y, section_w, section_h,
+            dest_x, dest_y, section_w, section_h)
+    }
+
+    /* Draw an image over the whole screen lol */
+    function screen_draw(ctx, img) {
+        ctx.drawImage(img, 0, 0);
+    }
+
+    /* ---- 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 TransitionType = { DOTS: 1, SLIDE_DOWN: 2, SLIDE_UP: 3, FADE: 4, CIRCLE: 5 };
+
+    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: TransitionType.DOTS,
+        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.type = type;
+        game.transition.end_time = length;
+
+        game.ctx.copy.drawImage(game.ctx.draw.canvas, 0, 0);
+
+        game.transition.dir_invert_v = Math.random() < 0.5;
+        game.transition.dir_invert_h = Math.random() < 0.5;
+
+        callback();
+
+        game.transition.is_transitioning = true;
+        game.transition.timer = 0;
+    }
+
+    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) {
+        game.ctx.global.save();
+
+        if (game.transition.type == TransitionType.DOTS) {
+            game.ctx.mask.clearRect(0, 0, game.screen_w, game.screen_h);
+            _draw_transition_dot_mask(game, game.ctx.mask);
+
+            // Redraw to reduce antialiasing effects
+            for (let i = 0; i < 5; i++) {
+                game.ctx.mask.drawImage(game.ctx.mask.canvas, 0, 0);
+            }
+
+            game.ctx.mask.globalCompositeOperation = 'source-in';
+            game.ctx.mask.drawImage(game.ctx.copy.canvas, 0, 0);
+            game.ctx.mask.globalCompositeOperation = 'source-over';
+
+            game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0);
+        } else if (game.transition.type == TransitionType.SLIDE_DOWN) {
+            let offset = game.transition.timer / game.transition.end_time * game.screen_h;
+
+            game.ctx.global.drawImage(game.ctx.copy.canvas, 0, -offset);
+            game.ctx.global.drawImage(game.ctx.draw.canvas, 0, game.screen_h - offset);
+        } else if (game.transition.type == TransitionType.SLIDE_UP) {
+            let offset = game.transition.timer / game.transition.end_time * game.screen_h;
+
+            game.ctx.global.drawImage(game.ctx.copy.canvas, 0, offset);
+            game.ctx.global.drawImage(ctx.canvas, 0, - game.screen_h + offset);
+        } else if (game.transition.type == TransitionType.FADE) {
+            let alpha = 0;
+            alpha = 1 - game.transition.timer / game.transition.end_time;
+
+            game.ctx.mask.clearRect(0, 0, game.screen_w, game.screen_h);
+            game.ctx.mask.fillStyle = 'rgba(255,255,255,' + alpha + ')';
+            game.ctx.mask.fillRect(0, 0, game.screen_w, game.screen_h);
+            game.ctx.mask.globalCompositeOperation = 'source-in';
+            game.ctx.mask.drawImage(game.ctx.copy.canvas, 0, 0);
+            game.ctx.mask.globalCompositeOperation = 'source-over';
+
+            game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0);
+        } else if (game.transition.type == TransitionType.CIRCLE) {
+            game.ctx.mask.clearRect(0, 0, game.screen_w, game.screen_h);
+
+            let frac = game.transition.timer / game.transition.end_time;
+            if (!game.transition.invert_shape) {
+                frac = 1 - frac;
+            }
+            frac = Math.pow(frac, 1.5);
+
+            let cx = character.x + 0.5;
+            let cy = character.y + 0.5;
+            let lh = game.level_h + 1;
+            let distances_to_corners = [
+                Math.sqrt(Math.pow(cx * game.tile_size, 2) + Math.pow(cy * game.tile_size, 2)),
+                Math.sqrt(Math.pow((game.level_w - cx) * game.tile_size, 2) + Math.pow(cy * game.tile_size, 2)),
+                Math.sqrt(Math.pow(cx * game.tile_size, 2) + Math.pow((lh - cy) * game.tile_size, 2)),
+                Math.sqrt(Math.pow((game.level_w - cx) * game.tile_size, 2) + Math.pow((lh - cy) * game.tile_size, 2)),
+            ];
+            let max_radius = Math.max(...distances_to_corners);
+            let radius = frac * max_radius;
+
+            game.ctx.mask.globalCompositeOperation = 'source-over';
+            game.ctx.mask.drawImage(game.ctx.copy.canvas, 0, 0);
+
+            game.ctx.mask.globalCompositeOperation = 'destination-out';
+            game.ctx.mask.fillStyle = 'rgba(255,255,255)';
+            game.ctx.mask.beginPath();
+            if (!game.transition.invert_shape) {
+                game.ctx.mask.rect(-5, -5, game.screen_w + 5, game.screen_h + 5);
+            }
+            game.ctx.mask.arc(character.x * game.tile_size + game.tile_size / 2,
+                character.y * game.tile_size + game.tile_size / 2,
+                radius, 0, 2 * Math.PI,
+                !game.transition.invert_shape);
+            game.ctx.mask.fill();
+
+            game.ctx.mask.globalCompositeOperation = 'source-over';
+
+            game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0);
+        }
+
+        game.ctx.global.restore();
+    }
+
+    function _draw_transition_dot_mask(game, ctx) {
+        ctx.fillStyle = '#0000ff';
+        let cell_width = game.screen_w / game.transition.w;
+        let cell_height = game.screen_h / game.transition.h;
+        let max_radius = 0.75 * Math.max(cell_width, cell_height);
+
+        let transition_dot_length = game.transition.end_time * 3 / 8;
+
+        for (let x = -1; x < game.transition.w + 1; x++) {
+            for (let y = -1; y < game.transition.h + 1; y++) {
+                let radius;
+
+                let circle_start_time = (x + y) / (game.transition.w + game.transition.h)
+                        * (game.transition.end_time - transition_dot_length);
+                if (game.transition.timer - circle_start_time < 0) {
+                    if (game.transition.invert_shape) {
+                        radius = 0;
+                    } else {
+                        radius = max_radius;
+                    }
+                } else if (game.transition.timer - circle_start_time < transition_dot_length) {
+                    if (game.transition.invert_shape) {
+                        radius = (game.transition.timer - circle_start_time) / transition_dot_length * max_radius;
+                    } else {
+                        radius = (1 - (game.transition.timer - circle_start_time) / transition_dot_length) * max_radius;
+                    }
+                } else {
+                    if (game.transition.invert_shape) {
+                        radius = max_radius;
+                    } else {
+                        radius = 0;
+                    }
+                }
+
+                let draw_x = x;
+                let draw_y = y;
+                if (game.transition.dir_invert_v) draw_x = game.transition.w - 1 - x;
+                if (game.transition.dir_invert_h) draw_y = game.transition.h - 1 - y;
+
+                if (radius >= max_radius * 0.8) {
+                    if (!game.transition.invert_shape) {
+                        ctx.fillRect(draw_x * cell_width, draw_y * cell_width, cell_width + 1, cell_width + 1);
+                    }
+                } else if (radius > 0) {
+                    ctx.save();
+                    ctx.beginPath();
+                    if (game.transition.invert_shape) {
+                        ctx.rect(draw_x * cell_width, draw_y * cell_width, cell_width + 3, cell_width + 3);
+                    }
+                    ctx.moveTo(draw_x * cell_width + cell_width / 2, draw_y * cell_width + cell_width / 2);
+                    ctx.arc(draw_x * cell_width + cell_width / 2,
+                             draw_y * cell_width + cell_width / 2,
+                             radius, 0, 2 * Math.PI, game.transition.invert_shape);
+                    ctx.clip();
+                    ctx.fillRect(draw_x * cell_width, draw_y * cell_width, cell_width, cell_width);
+                    ctx.restore();
+                } else {
+                    if (game.transition.invert_shape) {
+                        ctx.fillRect(draw_x * cell_width, draw_y * cell_width, cell_width + 3, cell_width + 3);
+                    }
+                }
+            }
+        }
+    }
+
+    /* ---- former contents of ready.js ---- */
+
+    // Due to Timo Huovinen
+    // https://stackoverflow.com/questions/799981/document-ready-equivalent-without-jquery
+    var ready = (function(){
+
+        var readyList,
+            DOMContentLoaded,
+            class2type = {};
+            class2type["[object Boolean]"] = "boolean";
+            class2type["[object Number]"] = "number";
+            class2type["[object String]"] = "string";
+            class2type["[object Function]"] = "function";
+            class2type["[object Array]"] = "array";
+            class2type["[object Date]"] = "date";
+            class2type["[object RegExp]"] = "regexp";
+            class2type["[object Object]"] = "object";
+
+        var ReadyObj = {
+            // Is the DOM ready to be used? Set to true once it occurs.
+            isReady: false,
+            // A counter to track how many items to wait for before
+            // the ready event fires. See #6781
+            readyWait: 1,
+            // Hold (or release) the ready event
+            holdReady: function( hold ) {
+                if ( hold ) {
+                    ReadyObj.readyWait++;
+                } else {
+                    ReadyObj.ready( true );
+                }
+            },
+            // Handle when the DOM is ready
+            ready: function( wait ) {
+                // Either a released hold or an DOMready/load event and not yet ready
+                if ( (wait === true && !--ReadyObj.readyWait) || (wait !== true && !ReadyObj.isReady) ) {
+                    // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+                    if ( !document.body ) {
+                        return setTimeout( ReadyObj.ready, 1 );
+                    }
+
+                    // Remember that the DOM is ready
+                    ReadyObj.isReady = true;
+                    // If a normal DOM Ready event fired, decrement, and wait if need be
+                    if ( wait !== true && --ReadyObj.readyWait > 0 ) {
+                        return;
+                    }
+                    // If there are functions bound, to execute
+                    readyList.resolveWith( document, [ ReadyObj ] );
+
+                    // Trigger any bound ready events
+                    //if ( ReadyObj.fn.trigger ) {
+                    //    ReadyObj( document ).trigger( "ready" ).unbind( "ready" );
+                    //}
+                }
+            },
+            bindReady: function() {
+                if ( readyList ) {
+                    return;
+                }
+                readyList = ReadyObj._Deferred();
+
+                // Catch cases where $(document).ready() is called after the
+                // browser event has already occurred.
+                if ( document.readyState === "complete" ) {
+                    // Handle it asynchronously to allow scripts the opportunity to delay ready
+                    return setTimeout( ReadyObj.ready, 1 );
+                }
+
+                // Mozilla, Opera and webkit nightlies currently support this event
+                if ( document.addEventListener ) {
+                    // Use the handy event callback
+                    document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+                    // A fallback to window.onload, that will always work
+                    window.addEventListener( "load", ReadyObj.ready, false );
+
+                // If IE event model is used
+                } else if ( document.attachEvent ) {
+                    // ensure firing before onload,
+                    // maybe late but safe also for iframes
+                    document.attachEvent( "onreadystatechange", DOMContentLoaded );
+
+                    // A fallback to window.onload, that will always work
+                    window.attachEvent( "onload", ReadyObj.ready );
+
+                    // If IE and not a frame
+                    // continually check to see if the document is ready
+                    var toplevel = false;
+
+                    try {
+                        toplevel = window.frameElement == null;
+                    } catch(e) {}
+
+                    if ( document.documentElement.doScroll && toplevel ) {
+                        doScrollCheck();
+                    }
+                }
+            },
+            _Deferred: function() {
+                var // callbacks list
+                    callbacks = [],
+                    // stored [ context , args ]
+                    fired,
+                    // to avoid firing when already doing so
+                    firing,
+                    // flag to know if the deferred has been cancelled
+                    cancelled,
+                    // the deferred itself
+                    deferred  = {
+
+                        // done( f1, f2, ...)
+                        done: function() {
+                            if ( !cancelled ) {
+                                var args = arguments,
+                                    i,
+                                    length,
+                                    elem,
+                                    type,
+                                    _fired;
+                                if ( fired ) {
+                                    _fired = fired;
+                                    fired = 0;
+                                }
+                                for ( i = 0, length = args.length; i < length; i++ ) {
+                                    elem = args[ i ];
+                                    type = ReadyObj.type( elem );
+                                    if ( type === "array" ) {
+                                        deferred.done.apply( deferred, elem );
+                                    } else if ( type === "function" ) {
+                                        callbacks.push( elem );
+                                    }
+                                }
+                                if ( _fired ) {
+                                    deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
+                                }
+                            }
+                            return this;
+                        },
+
+                        // resolve with given context and args
+                        resolveWith: function( context, args ) {
+                            if ( !cancelled && !fired && !firing ) {
+                                // make sure args are available (#8421)
+                                args = args || [];
+                                firing = 1;
+                                try {
+                                    while( callbacks[ 0 ] ) {
+                                        callbacks.shift().apply( context, args );//shifts a callback, and applies it to document
+                                    }
+                                }
+                                finally {
+                                    fired = [ context, args ];
+                                    firing = 0;
+                                }
+                            }
+                            return this;
+                        },
+
+                        // resolve with this as context and given arguments
+                        resolve: function() {
+                            deferred.resolveWith( this, arguments );
+                            return this;
+                        },
+
+                        // Has this deferred been resolved?
+                        isResolved: function() {
+                            return !!( firing || fired );
+                        },
+
+                        // Cancel
+                        cancel: function() {
+                            cancelled = 1;
+                            callbacks = [];
+                            return this;
+                        }
+                    };
+
+                return deferred;
+            },
+            type: function( obj ) {
+                return obj == null ?
+                    String( obj ) :
+                    class2type[ Object.prototype.toString.call(obj) ] || "object";
+            }
+        }
+        // The DOM ready check for Internet Explorer
+        function doScrollCheck() {
+            if ( ReadyObj.isReady ) {
+                return;
+            }
+
+            try {
+                // If IE is used, use the trick by Diego Perini
+                // http://javascript.nwbox.com/IEContentLoaded/
+                document.documentElement.doScroll("left");
+            } catch(e) {
+                setTimeout( doScrollCheck, 1 );
+                return;
+            }
+
+            // and execute any waiting functions
+            ReadyObj.ready();
+        }
+        // Cleanup functions for the document ready method
+        if ( document.addEventListener ) {
+            DOMContentLoaded = function() {
+                document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+                ReadyObj.ready();
+            };
+
+        } else if ( document.attachEvent ) {
+            DOMContentLoaded = function() {
+                // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+                if ( document.readyState === "complete" ) {
+                    document.detachEvent( "onreadystatechange", DOMContentLoaded );
+                    ReadyObj.ready();
+                }
+            };
+        }
+        function ready( fn ) {
+            // Attach the listeners
+            ReadyObj.bindReady();
+
+            var type = ReadyObj.type( fn );
+
+            // Add the callback
+            readyList.done( fn );//readyList is result of _Deferred()
+        }
+        return ready;
+    })();
+
+    return {
+        create_game: create_game,
+        ready: ready,
+        mod: mod,
+        copy_list: copy_list,
+        copy_flat_objlist: copy_flat_objlist,
+        sprite_draw: sprite_draw,
+        screen_draw: screen_draw,
+        transition: TransitionType,
+    }
+})();
diff --git a/palette-test.png b/palette-test.png
new file mode 100644 (file)
index 0000000..e3ccf2a
Binary files /dev/null and b/palette-test.png differ
diff --git a/pause.png b/pause.png
new file mode 100644 (file)
index 0000000..2530de4
Binary files /dev/null and b/pause.png differ
diff --git a/saveerror.png b/saveerror.png
new file mode 100644 (file)
index 0000000..faa7606
Binary files /dev/null and b/saveerror.png differ
diff --git a/sfx/complete.wav b/sfx/complete.wav
new file mode 100644 (file)
index 0000000..0d9a0d3
Binary files /dev/null and b/sfx/complete.wav differ
diff --git a/sfx/connect.wav b/sfx/connect.wav
new file mode 100644 (file)
index 0000000..4aaa73e
Binary files /dev/null and b/sfx/connect.wav differ
diff --git a/sfx/disconnect.wav b/sfx/disconnect.wav
new file mode 100644 (file)
index 0000000..91545cc
Binary files /dev/null and b/sfx/disconnect.wav differ
diff --git a/sfx/go.wav b/sfx/go.wav
new file mode 100644 (file)
index 0000000..bd46924
Binary files /dev/null and b/sfx/go.wav differ
diff --git a/sfx/magic.wav b/sfx/magic.wav
new file mode 100644 (file)
index 0000000..0d347cd
Binary files /dev/null and b/sfx/magic.wav differ
diff --git a/sfx/slide.wav b/sfx/slide.wav
new file mode 100644 (file)
index 0000000..f9c9a81
Binary files /dev/null and b/sfx/slide.wav differ
diff --git a/sfx/step.wav b/sfx/step.wav
new file mode 100644 (file)
index 0000000..bb44de4
Binary files /dev/null and b/sfx/step.wav differ
diff --git a/sfx/step2.wav b/sfx/step2.wav
new file mode 100644 (file)
index 0000000..4891e41
Binary files /dev/null and b/sfx/step2.wav differ
diff --git a/sfx/step3.wav b/sfx/step3.wav
new file mode 100644 (file)
index 0000000..a8418d9
Binary files /dev/null and b/sfx/step3.wav differ
diff --git a/zucchinibread.js b/zucchinibread.js
new file mode 100644 (file)
index 0000000..f7b8ed3
--- /dev/null
@@ -0,0 +1,1150 @@
+"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 copy_list(list) {
+        let newlist = [];
+        for (let x of list) {
+            newlist.push(x);
+        }
+        return newlist;
+    }
+
+    function copy_flat_objlist(list) {
+        let newlist = [];
+        for (let x of list) {
+            newlist.push({ ...x });
+        }
+        return newlist;
+    }
+
+    /* ---- Resource loading / loading screen ---- */
+
+    let _audiocheck = document.createElement('audio');
+
+    let _SFX_ARRAY_SIZE = 10;
+
+    /* Returns a callback that should be called when a resource finishes loading. */
+    function _register_resource(name, game, callback) {
+        game._total_things_to_load ++;
+        console.log("Loading", name + ". Things to load:", game._total_things_to_load);
+        return function() {
+            if (!game.ready_to_go) {
+                game._things_loaded ++;
+                console.log("Loaded", name + ". Things loaded:", game._things_loaded, "/", game._total_things_to_load);
+                _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_array = new Array(_SFX_ARRAY_SIZE);
+            for (let i = 0; i < _SFX_ARRAY_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_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 */
+            },
+            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);
+            }
+        };
+
+        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.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.run_in_background) {
+            let pause_img = new Image();
+            pause_img.onload = _register_resource('pause.png', game);
+            pause_img.src = 'pause.png';
+            game.img._pause = pause_img;
+        }
+
+        if (game.hasOwnProperty('save_key')) {
+            let saveerror_img = new Image();
+            saveerror_img.onload = _register_resource('saveerror.png', game);
+            saveerror_img.src = 'saveerror.png';
+            game.img._saveerror = saveerror_img;
+        }
+
+        let clicktostart_img = new Image();
+        clicktostart_img.onload = _register_resource('clicktostart.png', game, function() {
+            if (game.ready_to_go) {
+                game.ctx.global.save();
+                game.ctx.global.scale(game.draw_scale, game.draw_scale);
+                game.ctx.global.drawImage(game.img._clicktostart, 0, 0);
+                game.ctx.global.restore();
+            }
+        });
+        clicktostart_img.src = 'clicktostart.png';
+        game.img._clicktostart = clicktostart_img;
+
+        return game;
+    }
+
+    /* ---- Audio ---- */
+
+    function _stop_music(game) {
+        if (game.muted) return;
+
+        for (let m in game.music) {
+            if (!game.music[m].paused) {
+                game.music[m].was_playing = true;
+                game.music[m].pause();
+            } else {
+                game.music[m].was_playing = false;
+            }
+        }
+    }
+
+    function _start_music(game, unmuting) {
+        if (game.muted && !unmuting) return;
+
+        for (let m in game.music) {
+            if (game.music[m].was_playing) {
+                game.music[m].play();
+            }
+        }
+    }
+
+    function _mute(game) {
+        _stop_music(game);
+        game.muted = true;
+    }
+
+    function _unmute(game) {
+        game.muted = false;
+        _start_music(game);
+    }
+
+    /* ---- Game update stuff ---- */
+
+    let _last_frame_time;
+    let _timedelta = 0;
+    function _loop(game, timestamp) {
+        if (timestamp == undefined) {
+            timestamp = 0;
+            _last_frame_time = timestamp;
+        }
+        _timedelta += timestamp - _last_frame_time;
+        _last_frame_time = timestamp;
+
+        while (_timedelta >= 1000 / game.frame_rate) {
+            _update(game, 1000 / game.frame_rate);
+            _timedelta -= 1000 / game.frame_rate;
+        }
+        _draw(game);
+
+        if (!game._norun) {
+            requestAnimFrame(function(timestamp) {
+                _loop(game, timestamp);
+            });
+        }
+    }
+
+    function _update(game, delta) {
+        game.update_func(delta);
+
+        if (game.transition.is_transitioning) {
+            game.transition.timer += delta;
+            if (game.transition.timer > game.transition.end_time) {
+                _finish_transition(game);
+            }
+        }
+    }
+
+    /* ---- Mouse ---- */
+
+    function _handle_mousedown(game, e) {
+        if (!game.playing) return;
+
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((e.clientX - rect.left) / game.draw_scale) - 1;
+        let y = Math.round((e.clientY - rect.top) / game.draw_scale) - 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;
+        }
+
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((e.clientX - rect.left) / game.draw_scale) - 1;
+        let y = Math.round((e.clientY - rect.top) / game.draw_scale) - 1;
+        if (e.button === 0 && game.events.mouseup) {
+            game.events.mouseup(game, e, x, y);
+        }
+    }
+
+    function _handle_mousemove(game, e) {
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((e.clientX - rect.left) / game.draw_scale) - 1;
+        let y = Math.round((e.clientY - rect.top) / game.draw_scale) - 1;
+        if (game.events.mousemove) {
+            game.events.mousemove(game, e, x, y);
+        }
+    }
+
+    /* ---- Drawing stuff ---- */
+
+    function _draw(game) {
+        let ctx = game.ctx.draw;
+
+        ctx.save();
+
+        ctx.fillStyle = game.background_color;
+
+        ctx.beginPath();
+        ctx.rect(0, 0, game.screen_w, game.screen_h);
+        ctx.fill();
+
+        game.draw_func(game.ctx.draw);
+
+        if (game.transition.mid_long) {
+            ctx.fillStyle = game.transition.color;
+            ctx.fillRect(-1, -1, game.canvas_w + 5, game.canvas_h + 5);
+        }
+
+        ctx.restore();
+
+        game.ctx.global.fillStyle = 'rgb(0, 0, 0)';
+        game.ctx.global.beginPath();
+        game.ctx.global.rect(0, 0, game.screen_w * game.draw_scale, game.screen_h * game.draw_scale);
+        game.ctx.global.fill();
+
+        game.ctx.global.save();
+
+        game.ctx.global.scale(game.draw_scale, game.draw_scale);
+
+        game.ctx.global.drawImage(ctx.canvas, 0, 0);
+
+        if (game.transition.is_transitioning) {
+            _draw_transition(game);
+        }
+
+        if (game._norun) {
+            game.ctx.global.drawImage(game.img._pause, 0, 0);
+        }
+
+        game.ctx.global.restore();
+    }
+
+    /* Draw a particular section of an image/spritesheet,
+     * without having to do as much math or type the destination size */
+    function sprite_draw(ctx, img, section_w, section_h, section_x, section_y, dest_x, dest_y) {
+        ctx.drawImage(img,
+            section_w * section_x, section_h * section_y, section_w, section_h,
+            dest_x, dest_y, section_w, section_h)
+    }
+
+    /* Draw an image over the whole screen lol */
+    function screen_draw(ctx, img) {
+        ctx.drawImage(img, 0, 0);
+    }
+
+    /* ---- 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 TransitionType = { DOTS: 1, SLIDE_DOWN: 2, SLIDE_UP: 3, FADE: 4, CIRCLE: 5 };
+
+    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: TransitionType.DOTS,
+        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.type = type;
+        game.transition.end_time = length;
+
+        game.ctx.copy.drawImage(game.ctx.draw.canvas, 0, 0);
+
+        game.transition.dir_invert_v = Math.random() < 0.5;
+        game.transition.dir_invert_h = Math.random() < 0.5;
+
+        callback();
+
+        game.transition.is_transitioning = true;
+        game.transition.timer = 0;
+    }
+
+    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) {
+        game.ctx.global.save();
+
+        if (game.transition.type == TransitionType.DOTS) {
+            game.ctx.mask.clearRect(0, 0, game.screen_w, game.screen_h);
+            _draw_transition_dot_mask(game, game.ctx.mask);
+
+            // Redraw to reduce antialiasing effects
+            for (let i = 0; i < 5; i++) {
+                game.ctx.mask.drawImage(game.ctx.mask.canvas, 0, 0);
+            }
+
+            game.ctx.mask.globalCompositeOperation = 'source-in';
+            game.ctx.mask.drawImage(game.ctx.copy.canvas, 0, 0);
+            game.ctx.mask.globalCompositeOperation = 'source-over';
+
+            game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0);
+        } else if (game.transition.type == TransitionType.SLIDE_DOWN) {
+            let offset = game.transition.timer / game.transition.end_time * game.screen_h;
+
+            game.ctx.global.drawImage(game.ctx.copy.canvas, 0, -offset);
+            game.ctx.global.drawImage(game.ctx.draw.canvas, 0, game.screen_h - offset);
+        } else if (game.transition.type == TransitionType.SLIDE_UP) {
+            let offset = game.transition.timer / game.transition.end_time * game.screen_h;
+
+            game.ctx.global.drawImage(game.ctx.copy.canvas, 0, offset);
+            game.ctx.global.drawImage(ctx.canvas, 0, - game.screen_h + offset);
+        } else if (game.transition.type == TransitionType.FADE) {
+            let alpha = 0;
+            alpha = 1 - game.transition.timer / game.transition.end_time;
+
+            game.ctx.mask.clearRect(0, 0, game.screen_w, game.screen_h);
+            game.ctx.mask.fillStyle = 'rgba(255,255,255,' + alpha + ')';
+            game.ctx.mask.fillRect(0, 0, game.screen_w, game.screen_h);
+            game.ctx.mask.globalCompositeOperation = 'source-in';
+            game.ctx.mask.drawImage(game.ctx.copy.canvas, 0, 0);
+            game.ctx.mask.globalCompositeOperation = 'source-over';
+
+            game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0);
+        } else if (game.transition.type == TransitionType.CIRCLE) {
+            game.ctx.mask.clearRect(0, 0, game.screen_w, game.screen_h);
+
+            let frac = game.transition.timer / game.transition.end_time;
+            if (!game.transition.invert_shape) {
+                frac = 1 - frac;
+            }
+            frac = Math.pow(frac, 1.5);
+
+            let cx = character.x + 0.5;
+            let cy = character.y + 0.5;
+            let lh = game.level_h + 1;
+            let distances_to_corners = [
+                Math.sqrt(Math.pow(cx * game.tile_size, 2) + Math.pow(cy * game.tile_size, 2)),
+                Math.sqrt(Math.pow((game.level_w - cx) * game.tile_size, 2) + Math.pow(cy * game.tile_size, 2)),
+                Math.sqrt(Math.pow(cx * game.tile_size, 2) + Math.pow((lh - cy) * game.tile_size, 2)),
+                Math.sqrt(Math.pow((game.level_w - cx) * game.tile_size, 2) + Math.pow((lh - cy) * game.tile_size, 2)),
+            ];
+            let max_radius = Math.max(...distances_to_corners);
+            let radius = frac * max_radius;
+
+            game.ctx.mask.globalCompositeOperation = 'source-over';
+            game.ctx.mask.drawImage(game.ctx.copy.canvas, 0, 0);
+
+            game.ctx.mask.globalCompositeOperation = 'destination-out';
+            game.ctx.mask.fillStyle = 'rgba(255,255,255)';
+            game.ctx.mask.beginPath();
+            if (!game.transition.invert_shape) {
+                game.ctx.mask.rect(-5, -5, game.screen_w + 5, game.screen_h + 5);
+            }
+            game.ctx.mask.arc(character.x * game.tile_size + game.tile_size / 2,
+                character.y * game.tile_size + game.tile_size / 2,
+                radius, 0, 2 * Math.PI,
+                !game.transition.invert_shape);
+            game.ctx.mask.fill();
+
+            game.ctx.mask.globalCompositeOperation = 'source-over';
+
+            game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0);
+        }
+
+        game.ctx.global.restore();
+    }
+
+    function _draw_transition_dot_mask(game, ctx) {
+        ctx.fillStyle = '#0000ff';
+        let cell_width = game.screen_w / game.transition.w;
+        let cell_height = game.screen_h / game.transition.h;
+        let max_radius = 0.75 * Math.max(cell_width, cell_height);
+
+        let transition_dot_length = game.transition.end_time * 3 / 8;
+
+        for (let x = -1; x < game.transition.w + 1; x++) {
+            for (let y = -1; y < game.transition.h + 1; y++) {
+                let radius;
+
+                let circle_start_time = (x + y) / (game.transition.w + game.transition.h)
+                        * (game.transition.end_time - transition_dot_length);
+                if (game.transition.timer - circle_start_time < 0) {
+                    if (game.transition.invert_shape) {
+                        radius = 0;
+                    } else {
+                        radius = max_radius;
+                    }
+                } else if (game.transition.timer - circle_start_time < transition_dot_length) {
+                    if (game.transition.invert_shape) {
+                        radius = (game.transition.timer - circle_start_time) / transition_dot_length * max_radius;
+                    } else {
+                        radius = (1 - (game.transition.timer - circle_start_time) / transition_dot_length) * max_radius;
+                    }
+                } else {
+                    if (game.transition.invert_shape) {
+                        radius = max_radius;
+                    } else {
+                        radius = 0;
+                    }
+                }
+
+                let draw_x = x;
+                let draw_y = y;
+                if (game.transition.dir_invert_v) draw_x = game.transition.w - 1 - x;
+                if (game.transition.dir_invert_h) draw_y = game.transition.h - 1 - y;
+
+                if (radius >= max_radius * 0.8) {
+                    if (!game.transition.invert_shape) {
+                        ctx.fillRect(draw_x * cell_width, draw_y * cell_width, cell_width + 1, cell_width + 1);
+                    }
+                } else if (radius > 0) {
+                    ctx.save();
+                    ctx.beginPath();
+                    if (game.transition.invert_shape) {
+                        ctx.rect(draw_x * cell_width, draw_y * cell_width, cell_width + 3, cell_width + 3);
+                    }
+                    ctx.moveTo(draw_x * cell_width + cell_width / 2, draw_y * cell_width + cell_width / 2);
+                    ctx.arc(draw_x * cell_width + cell_width / 2,
+                             draw_y * cell_width + cell_width / 2,
+                             radius, 0, 2 * Math.PI, game.transition.invert_shape);
+                    ctx.clip();
+                    ctx.fillRect(draw_x * cell_width, draw_y * cell_width, cell_width, cell_width);
+                    ctx.restore();
+                } else {
+                    if (game.transition.invert_shape) {
+                        ctx.fillRect(draw_x * cell_width, draw_y * cell_width, cell_width + 3, cell_width + 3);
+                    }
+                }
+            }
+        }
+    }
+
+    /* ---- former contents of ready.js ---- */
+
+    // Due to Timo Huovinen
+    // https://stackoverflow.com/questions/799981/document-ready-equivalent-without-jquery
+    var ready = (function(){
+
+        var readyList,
+            DOMContentLoaded,
+            class2type = {};
+            class2type["[object Boolean]"] = "boolean";
+            class2type["[object Number]"] = "number";
+            class2type["[object String]"] = "string";
+            class2type["[object Function]"] = "function";
+            class2type["[object Array]"] = "array";
+            class2type["[object Date]"] = "date";
+            class2type["[object RegExp]"] = "regexp";
+            class2type["[object Object]"] = "object";
+
+        var ReadyObj = {
+            // Is the DOM ready to be used? Set to true once it occurs.
+            isReady: false,
+            // A counter to track how many items to wait for before
+            // the ready event fires. See #6781
+            readyWait: 1,
+            // Hold (or release) the ready event
+            holdReady: function( hold ) {
+                if ( hold ) {
+                    ReadyObj.readyWait++;
+                } else {
+                    ReadyObj.ready( true );
+                }
+            },
+            // Handle when the DOM is ready
+            ready: function( wait ) {
+                // Either a released hold or an DOMready/load event and not yet ready
+                if ( (wait === true && !--ReadyObj.readyWait) || (wait !== true && !ReadyObj.isReady) ) {
+                    // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+                    if ( !document.body ) {
+                        return setTimeout( ReadyObj.ready, 1 );
+                    }
+
+                    // Remember that the DOM is ready
+                    ReadyObj.isReady = true;
+                    // If a normal DOM Ready event fired, decrement, and wait if need be
+                    if ( wait !== true && --ReadyObj.readyWait > 0 ) {
+                        return;
+                    }
+                    // If there are functions bound, to execute
+                    readyList.resolveWith( document, [ ReadyObj ] );
+
+                    // Trigger any bound ready events
+                    //if ( ReadyObj.fn.trigger ) {
+                    //    ReadyObj( document ).trigger( "ready" ).unbind( "ready" );
+                    //}
+                }
+            },
+            bindReady: function() {
+                if ( readyList ) {
+                    return;
+                }
+                readyList = ReadyObj._Deferred();
+
+                // Catch cases where $(document).ready() is called after the
+                // browser event has already occurred.
+                if ( document.readyState === "complete" ) {
+                    // Handle it asynchronously to allow scripts the opportunity to delay ready
+                    return setTimeout( ReadyObj.ready, 1 );
+                }
+
+                // Mozilla, Opera and webkit nightlies currently support this event
+                if ( document.addEventListener ) {
+                    // Use the handy event callback
+                    document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+                    // A fallback to window.onload, that will always work
+                    window.addEventListener( "load", ReadyObj.ready, false );
+
+                // If IE event model is used
+                } else if ( document.attachEvent ) {
+                    // ensure firing before onload,
+                    // maybe late but safe also for iframes
+                    document.attachEvent( "onreadystatechange", DOMContentLoaded );
+
+                    // A fallback to window.onload, that will always work
+                    window.attachEvent( "onload", ReadyObj.ready );
+
+                    // If IE and not a frame
+                    // continually check to see if the document is ready
+                    var toplevel = false;
+
+                    try {
+                        toplevel = window.frameElement == null;
+                    } catch(e) {}
+
+                    if ( document.documentElement.doScroll && toplevel ) {
+                        doScrollCheck();
+                    }
+                }
+            },
+            _Deferred: function() {
+                var // callbacks list
+                    callbacks = [],
+                    // stored [ context , args ]
+                    fired,
+                    // to avoid firing when already doing so
+                    firing,
+                    // flag to know if the deferred has been cancelled
+                    cancelled,
+                    // the deferred itself
+                    deferred  = {
+
+                        // done( f1, f2, ...)
+                        done: function() {
+                            if ( !cancelled ) {
+                                var args = arguments,
+                                    i,
+                                    length,
+                                    elem,
+                                    type,
+                                    _fired;
+                                if ( fired ) {
+                                    _fired = fired;
+                                    fired = 0;
+                                }
+                                for ( i = 0, length = args.length; i < length; i++ ) {
+                                    elem = args[ i ];
+                                    type = ReadyObj.type( elem );
+                                    if ( type === "array" ) {
+                                        deferred.done.apply( deferred, elem );
+                                    } else if ( type === "function" ) {
+                                        callbacks.push( elem );
+                                    }
+                                }
+                                if ( _fired ) {
+                                    deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
+                                }
+                            }
+                            return this;
+                        },
+
+                        // resolve with given context and args
+                        resolveWith: function( context, args ) {
+                            if ( !cancelled && !fired && !firing ) {
+                                // make sure args are available (#8421)
+                                args = args || [];
+                                firing = 1;
+                                try {
+                                    while( callbacks[ 0 ] ) {
+                                        callbacks.shift().apply( context, args );//shifts a callback, and applies it to document
+                                    }
+                                }
+                                finally {
+                                    fired = [ context, args ];
+                                    firing = 0;
+                                }
+                            }
+                            return this;
+                        },
+
+                        // resolve with this as context and given arguments
+                        resolve: function() {
+                            deferred.resolveWith( this, arguments );
+                            return this;
+                        },
+
+                        // Has this deferred been resolved?
+                        isResolved: function() {
+                            return !!( firing || fired );
+                        },
+
+                        // Cancel
+                        cancel: function() {
+                            cancelled = 1;
+                            callbacks = [];
+                            return this;
+                        }
+                    };
+
+                return deferred;
+            },
+            type: function( obj ) {
+                return obj == null ?
+                    String( obj ) :
+                    class2type[ Object.prototype.toString.call(obj) ] || "object";
+            }
+        }
+        // The DOM ready check for Internet Explorer
+        function doScrollCheck() {
+            if ( ReadyObj.isReady ) {
+                return;
+            }
+
+            try {
+                // If IE is used, use the trick by Diego Perini
+                // http://javascript.nwbox.com/IEContentLoaded/
+                document.documentElement.doScroll("left");
+            } catch(e) {
+                setTimeout( doScrollCheck, 1 );
+                return;
+            }
+
+            // and execute any waiting functions
+            ReadyObj.ready();
+        }
+        // Cleanup functions for the document ready method
+        if ( document.addEventListener ) {
+            DOMContentLoaded = function() {
+                document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+                ReadyObj.ready();
+            };
+
+        } else if ( document.attachEvent ) {
+            DOMContentLoaded = function() {
+                // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+                if ( document.readyState === "complete" ) {
+                    document.detachEvent( "onreadystatechange", DOMContentLoaded );
+                    ReadyObj.ready();
+                }
+            };
+        }
+        function ready( fn ) {
+            // Attach the listeners
+            ReadyObj.bindReady();
+
+            var type = ReadyObj.type( fn );
+
+            // Add the callback
+            readyList.done( fn );//readyList is result of _Deferred()
+        }
+        return ready;
+    })();
+
+    return {
+        create_game: create_game,
+        ready: ready,
+        mod: mod,
+        copy_list: copy_list,
+        copy_flat_objlist: copy_flat_objlist,
+        sprite_draw: sprite_draw,
+        screen_draw: screen_draw,
+        transition: TransitionType,
+    }
+})();