Initial commit
authorcassowarii <2374677+cassowarii@users.noreply.github.com>
Sun, 5 Oct 2025 22:15:03 +0000 (15:15 -0700)
committercassowarii <2374677+cassowarii@users.noreply.github.com>
Sun, 5 Oct 2025 22:15:03 +0000 (15:15 -0700)
48 files changed:
.gitignore [new file with mode: 0644]
clicktostart-sheet.png [new file with mode: 0644]
clicktostart.png [new file with mode: 0644]
game.js [new file with mode: 0644]
img/arm.png [new file with mode: 0644]
img/bars.png [new file with mode: 0644]
img/buttons.png [new file with mode: 0644]
img/continue.png [new file with mode: 0644]
img/hand.png [new file with mode: 0644]
img/indicator.png [new file with mode: 0644]
img/instructions.png [new file with mode: 0644]
img/level-complete.png [new file with mode: 0644]
img/levelnums.png [new file with mode: 0644]
img/stamp-large.png [new file with mode: 0644]
img/stamp.png [new file with mode: 0644]
img/tiles.png [new file with mode: 0644]
img/title.png [new file with mode: 0644]
img/titlegraphic.png [new file with mode: 0644]
img/youwin.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/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]
loading.png [new file with mode: 0644]
music/in_the_garden.mp3 [new file with mode: 0755]
pause.png [new file with mode: 0644]
progressbar.png [new file with mode: 0644]
saveerror.png [new file with mode: 0644]
sfx/grab.mp3 [new file with mode: 0644]
sfx/ouch.mp3 [new file with mode: 0644]
sfx/pull.mp3 [new file with mode: 0644]
sfx/retract.mp3 [new file with mode: 0644]
sfx/ungrab.mp3 [new file with mode: 0644]
sfx/win.mp3 [new file with mode: 0644]
zucchinibread.js [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..fd75d88
--- /dev/null
@@ -0,0 +1,7 @@
+*.xcf
+*.swp
+*.swo
+sfx_raw/*
+ase/*
+packaging.zip
+packaging/*
diff --git a/clicktostart-sheet.png b/clicktostart-sheet.png
new file mode 100644 (file)
index 0000000..0860cbd
Binary files /dev/null and b/clicktostart-sheet.png differ
diff --git a/clicktostart.png b/clicktostart.png
new file mode 100644 (file)
index 0000000..b42c06e
Binary files /dev/null and b/clicktostart.png differ
diff --git a/game.js b/game.js
new file mode 100644 (file)
index 0000000..06a88f1
--- /dev/null
+++ b/game.js
@@ -0,0 +1,1262 @@
+"use strict";
+
+let game;
+
+let level_number = 1;
+
+let continue_level = 1;
+
+let map;
+
+let buttons = {};
+
+/* --------- definitions ---------- */
+
+/* state:
+ * STAND: waiting for input
+ * EXTEND: extending arm
+ * RETRACT: retracting arm
+ * ALMOSTWIN: pulling offscreen to win
+ * WIN: won level
+ * CONTINUE: continuing after win
+ */
+let State = { STAND: 0, EXTEND: 1, RETRACT: 2, ALMOSTWIN: 3, WIN: 4, CONTINUE: 5 };
+
+let UIState = { MENU: 0 };
+
+let can_continue = false;
+
+let save_data = 1;
+const SAVE_KEY = "casso.stampcollector.save"
+
+zb.ready(function() {
+    game = zb.create_game({
+        canvas: 'canvas',
+        canvas_w: 720,
+        canvas_h: 576,
+        draw_scale: 3,
+        tile_size: 16,
+        level_w: 14,
+        level_h: 11,
+        background_color: '#e6dfc6',
+        run_in_background: true,
+        load_with_progress_bar: true,
+        save_key: SAVE_KEY,
+        state: State.STAND,
+        uistate: UIState.MENU,
+        events: {
+            keydown: handle_keydown,
+            keyup: handle_keyup,
+            mousemove: handle_mousemove,
+            mouseup: handle_mouseup,
+            mousedown: handle_mousedown,
+        },
+        modes: {
+            menu: {
+                draw: do_draw_menu,
+                update: do_update_menu,
+            },
+            youwon: {
+                draw: do_draw_youwon,
+            },
+            game: {
+                draw: do_draw,
+                draw_after_transition: do_draw_after_transition,
+                update: do_update,
+            },
+        },
+        mode: 'menu',
+        buttons: {},
+    });
+
+    game.register_images({
+        hand: 'img/hand.png',
+        arm: 'img/arm.png',
+        tiles: 'img/tiles.png',
+        indicator: 'img/indicator.png',
+        bars: 'img/bars.png',
+        stamp: 'img/stamp.png',
+        stamp_large: 'img/stamp-large.png',
+        instructions: 'img/instructions.png',
+        level_complete: 'img/level-complete.png',
+        press_space_to_continue: 'img/continue.png',
+        levelnums: 'img/levelnums.png',
+        title: 'img/title.png',
+        titlegraphic: 'img/titlegraphic.png',
+        youwin: 'img/youwin.png',
+        menubuttons: 'img/buttons.png',
+    });
+
+    game.register_sfx({
+        grab: {
+            path: 'sfx/grab.mp3',
+            volume: 1.0,
+        },
+        ungrab: {
+            path: 'sfx/ungrab.mp3',
+            volume: 1.0,
+        },
+        win: {
+            path: 'sfx/win.mp3',
+            volume: 1.0,
+        },
+        ouch: {
+            path: 'sfx/ouch.mp3',
+            volume: 1.0,
+        },
+        pull: {
+            path: 'sfx/pull.mp3',
+            volume: 0.3,
+        },
+    });
+
+    game.register_music({
+        retract: {
+            path: 'sfx/retract',
+            volume: 0.5,
+        },
+        bgm: {
+            path: 'music/in_the_garden',
+            volume: 0.7,
+        },
+    });
+
+    game.resources_ready();
+
+    continue_level = parseInt(game.load("continue_level") || "1");
+
+    game.buttons.menu = zb.buttons.create({
+        img: game.img.menubuttons,
+        button_w: 64,
+        button_h: 16,
+        buttons: [
+            {
+                /* New game */
+                x: game.screen_w / 2 - 32,
+                y: game.screen_h - 56,
+                id: 0,
+                callback: function() {
+                    game.long_transition(zb.transition.type.fade, 1000, function() {
+                        game.mode = 'game';
+                        level_number = 1;
+                        game.music.bgm.play();
+                        load_level();
+                    });
+                },
+            },
+            {
+                /* Continue */
+                x: game.screen_w / 2 - 32,
+                y: game.screen_h - 32,
+                id: 1,
+                callback: function() {
+                    game.long_transition(zb.transition.type.fade, 1000, function() {
+                        game.mode = 'game';
+                        level_number = continue_level;
+                        load_level();
+                    });
+                },
+                disabled: continue_level === 1,
+            },
+        ],
+    });
+
+    game.transition.color = game.background_color;
+
+    level_number = 1;
+    load_level();
+});
+
+/* ------ relevant constant types --------- */
+
+const tileID = {
+    open: 0,
+    wall1: 1,
+    wall2: 2,
+    wall3: 3,
+    wall4: 4,
+    dec1: 5,
+    dec2: 6,
+    dec3: 7,
+    dec4: 8,
+};
+
+const solid = {
+    [tileID.wall1]: 1,
+    [tileID.wall2]: 1,
+    [tileID.wall3]: 1,
+    [tileID.wall4]: 1,
+};
+
+const decorative = {
+    [tileID.dec2]: 1,
+    [tileID.dec3]: 1,
+};
+
+const STAMP_W = 128;
+const STAMP_H = 128;
+
+/* ------ timers & static timer values --------- */
+
+const NTILEFRAMES = 4;
+let tile_frame = 0;
+const TILE_FRAME_LENGTH = 800;
+let tile_frame_timer = 0;
+
+const HAND_MOVE_LENGTH = 100;
+
+const WIN_LENGTH = 1000;
+
+let instructions_timer = 0;
+const INSTRUCTIONS_PAUSE = 5000;
+const INSTRUCTIONS_FADE_LENGTH = 1000;
+
+let undo_timer = 0;
+const UNDO_HOLD = 250;
+const UNDO_SPACING = 80;
+
+let hue_shift = 0;
+let target_hue_shift = 0;
+let hue_shift_timer = 0;
+const HUE_SHIFT_LENGTH = 800;
+
+let title_timer = 0;
+const TITLE_BOUNCE_LENGTH = 4000;
+const TITLE_BOUNCE_DISTANCE = 8;
+
+/* ------- game global state -------- */
+
+let arrows = [];
+
+let space_pressed = false;
+let x_pressed = false;
+let z_pressed = false;
+
+let hand = {
+    x: 0,
+    y: 6,
+    variant: 0,
+    direction: zb.dir.right,
+
+    move_fraction: 0,
+};
+
+let arm = [];
+
+let gripping = false;
+
+let bars = [];
+let held_bar = null;
+
+let stamps = [];
+
+let move_speed = HAND_MOVE_LENGTH;
+let move_will_fail = false;
+let move_will_ouch = false;
+let move_failed = false;
+
+let won_level = false;
+let win_progress = 0;
+let win_offset = 0;
+
+/* ------- game behavior functions -------- */
+
+function tile_at(x, y) {
+    if (x < 0 || x >= game.level_w) return tileID.wall;
+    if (y < 0 || y >= game.level_h) return tileID.wall;
+
+    let result = map[y * game.level_w + x];
+    return result;
+}
+
+function arm_at(x, y) {
+    return arm.filter(a => a.x === x && a.y === y).length > 0;
+}
+
+function bar_at(x, y) {
+    let mybars = bars.filter(b => b.components.some(c => c.x === x && c.y === y));
+    if (mybars.length > 0) {
+        return mybars[0];
+    } else {
+        return null;
+    }
+}
+
+function stamp_at(x, y) {
+    let mystamps = stamps.filter(s => s.x === x && s.y === y);
+    if (mystamps.length > 0) {
+        return mystamps[0];
+    } else {
+        return null;
+    }
+}
+
+function bar_can_move_direction(bar, direction) {
+    for (let c of bar.components) {
+        if (c.x === hand.x && c.y === hand.y) continue; /* skip the part being grabbed, because it can move to arm */
+        if (solid[tile_at(c.x + direction.x, c.y + direction.y)]) return false;
+        if (arm_at(c.x + direction.x, c.y + direction.y)) return false;
+
+        let b = bar_at(c.x + direction.x, c.y + direction.y)
+        if (b && b != bar) {
+            if (!bar_can_move_direction(b, direction)) return false;
+        }
+
+        let s = stamp_at(c.x + direction.x, c.y + direction.y)
+        if (s) {
+            if (!stamp_can_move_direction(s, direction)) return false;
+        }
+    }
+    return true;
+}
+
+function stamp_can_move_direction(stamp, direction) {
+    if (solid[tile_at(stamp.x + direction.x, stamp.y + direction.y)]) return false;
+    if (arm_at(stamp.x + direction.x, stamp.y + direction.y)) return false;
+
+    let b = bar_at(stamp.x + direction.x, stamp.y + direction.y)
+    if (b) {
+        if (!bar_can_move_direction(b, direction)) return false;
+    }
+
+    let s = stamp_at(stamp.x + direction.x, stamp.y + direction.y)
+    if (s) {
+        if (!stamp_can_move_direction(s, direction)) return false;
+    }
+
+    return true;
+}
+
+function move_bar_in_direction(bar, direction, play_sfx) {
+    for (let c of bar.components) {
+        c.target_x = c.x + direction.x;
+        c.target_y = c.y + direction.y;
+
+        let b = bar_at(c.target_x, c.target_y);
+        if (b && b != bar) {
+            move_bar_in_direction(b, direction, false);
+        }
+
+        let s = stamp_at(c.target_x, c.target_y);
+        if (s) {
+            move_stamp_in_direction(s, direction, false);
+        }
+    }
+
+    if (play_sfx) {
+        game.sfx.pull.play();
+    }
+}
+
+function move_stamp_in_direction(stamp, direction, play_sfx) {
+    stamp.target_x = stamp.x + direction.x;
+    stamp.target_y = stamp.y + direction.y;
+
+    let b = bar_at(stamp.target_x, stamp.target_y);
+    if (b) {
+        move_bar_in_direction(b, direction, false);
+    }
+
+    let s = stamp_at(stamp.target_x, stamp.target_y);
+    if (s && s != stamp) {
+        move_stamp_in_direction(s, direction, false);
+    }
+
+    if (play_sfx) {
+        game.sfx.pull.play();
+    }
+}
+
+function retract(speed) {
+    hand.direction = arm[arm.length - 1].end_dir;
+    hand.target_x = arm[arm.length - 1].x;
+    hand.target_y = arm[arm.length - 1].y;
+    hand.move_fraction = 0;
+    game.state = State.RETRACT;
+    move_speed = speed;
+}
+
+function fail_move() {
+    for (let b of bars) {
+        for (let c of b.components) {
+            if (c.target_x === undefined) c.target_x = c.x;
+            if (c.target_y === undefined) c.target_y = c.y;
+            [c.x, c.target_x, c.y, c.target_y] = [c.target_x, c.x, c.target_y, c.y];
+        }
+    }
+
+    for (let s of stamps) {
+        if (s.target_x === undefined) s.target_x = s.x;
+        if (s.target_y === undefined) s.target_y = s.y;
+        [s.x, s.target_x, s.y, s.target_y] = [s.target_x, s.x, s.target_y, s.y];
+    }
+
+    [hand.x, hand.target_x, hand.y, hand.target_y] = [hand.target_x, hand.x, hand.target_y, hand.y];
+    if (game.state === State.EXTEND) {
+        game.state = State.RETRACT;
+    } else {
+        game.state = State.EXTEND;
+    }
+
+    hand.move_fraction = 1 - hand.move_fraction;
+
+    if (move_will_ouch) {
+        move_speed *= 2;
+        hand.variant = 2;
+        if (gripping) game.sfx.ungrab.play();
+        gripping = false;
+        space_pressed = false;
+        game.sfx.ouch.play();
+    }
+    move_will_ouch = false;
+
+    move_will_fail = false;
+    move_failed = true;
+}
+
+function finish_move() {
+    if (move_failed) {
+        if (game.state === State.RETRACT) {
+            arrows = arrows.filter(a => a.x !== hand.direction.x && a.y !== hand.direction.y);
+        } else {
+            arrows = arrows.filter(a => a.x !== hand.direction.opposite.x && a.y !== hand.direction.opposite.y);
+        }
+    }
+
+    for (let b of bars) {
+        for (let c of b.components) {
+            if (c.target_x !== undefined) c.x = c.target_x;
+            if (c.target_y !== undefined) c.y = c.target_y;
+        }
+    }
+
+    for (let s of stamps) {
+        if (s.target_x !== undefined) s.x = s.target_x;
+        if (s.target_y !== undefined) s.y = s.target_y;
+    }
+
+    hand.x = hand.target_x;
+    hand.y = hand.target_y;
+    hand.move_fraction = 0;
+
+    if (hand.variant == 2) hand.variant = 0;
+
+    hand.direction = arm[arm.length - 1].end_dir;
+
+    move_failed = false;
+}
+
+/* -------- standard puzzle game stuff ------------ */
+
+let undo_stack = [];
+
+function create_undo_point() {
+    let new_undo_point = {
+        bars: [],
+        stamps: zb.copy_flat_objlist(stamps),
+        arm: zb.copy_flat_objlist(arm),
+        hand: { ...hand },
+    };
+
+    for (let b of bars) {
+        let copied_bar = { ...b };
+        copied_bar.components = [];
+        for (let c of b.components) {
+            copied_bar.components.push({ ...c });
+        }
+        new_undo_point.bars.push(copied_bar);
+    }
+
+    undo_stack.push(new_undo_point);
+}
+
+function undo() {
+    console.log("undo")
+    instructions_timer = 0;
+    if (undo_stack.length > 0) {
+        let point = undo_stack.pop();
+
+        bars = point.bars;
+        stamps = point.stamps;
+        arm = point.arm;
+        hand = point.hand;
+
+        game.state = State.STAND;
+        win_progress = 0;
+    }
+}
+
+function reset() {
+    console.log("reset");
+    instructions_timer = 0;
+    create_undo_point();
+    game.start_transition(zb.transition.type.fade, 300, function() {
+        load_level();
+    });
+}
+
+function advance_level() {
+    if (!can_continue) return;
+
+    undo_stack = [];
+
+    if (level_number + 1 > Math.max(...Object.keys(levels).map(a => parseInt(a, 10) || 0))) {
+        console.log("hi");
+        reset_save();
+        game.mode = 'youwon';
+    } else {
+        level_number ++;
+        load_level();
+    }
+}
+
+function skip_to(num) {
+    level_number = num;
+    load_level();
+}
+
+function check_victory() {
+    if (arm.length === 0) {
+        win();
+    } else {
+        for (let s of stamps) {
+            if (s.x <= 0 || s.y <= 0 || s.x >= game.level_w - 1 || s.y >= game.level_h - 1) {
+                if (held_bar && held_bar.components.every(c => c.x <= 1 || c.y <= 1 || c.x >= game.level_w - 1 || c.y >= game.level_h - 1)) {
+                    if (bar_can_move_direction(held_bar, hand.direction.opposite)) {
+                        if (arm.every(a => a.start_dir === a.end_dir)) {
+                            move_bar_in_direction(held_bar, hand.direction.opposite, true);
+                            retract(HAND_MOVE_LENGTH);
+                            game.state = State.ALMOSTWIN;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+function win_everything() {
+    console.log("WIN!!");
+}
+
+function load_level() {
+    load_level_data(levels[level_number]);
+}
+
+function load_level_data(lvl) {
+    map = lvl.map;
+
+    hand.x = lvl.start_x;
+    hand.y = lvl.start_y;
+
+    win_progress = 0;
+
+    instructions_timer = 0;
+
+    arm = [];
+    bars = [];
+    stamps = [];
+
+    for (let b of lvl.bars) {
+        let vertical = false;
+        if (b[0][0] === b[1][0]) {
+            vertical = true;
+        }
+
+        let new_bar = {
+            vertical: vertical,
+            components: [],
+        };
+
+        for (let i = 0; i < b.length; i++) {
+            let new_component = {
+                x: b[i][0],
+                y: b[i][1],
+            };
+
+            if (i === 0) {
+                new_component.variant = 0;
+            } else if (i != b.length - 1) {
+                new_component.variant = 1;
+            } else {
+                new_component.variant = 2;
+            }
+
+            if (vertical) {
+                new_component.variant += 3;
+            }
+
+            new_bar.components.push(new_component);
+        }
+
+        bars.push(new_bar);
+    }
+
+    if (gripping) game.sfx.ungrab.play();
+    gripping = false;
+    hand.variant = 0;
+    space_pressed = false;
+
+    for (let s of lvl.stamps) {
+        stamps.push({
+            x: s[0],
+            y: s[1],
+        });
+    }
+
+    /* extend two square to start level */
+    if (hand.y <= 0) {
+        hand.direction = zb.dir.down;
+        hand.y = -1;
+        arm.push({
+            start_dir: hand.direction,
+            end_dir: hand.direction,
+            x: hand.x,
+            y: hand.y - 1,
+        });
+        hand.target_x = hand.x;
+        hand.target_y = hand.y + 1;
+    } else if (hand.x <= 0) {
+        hand.direction = zb.dir.right;
+        hand.x = -1;
+        arm.push({
+            start_dir: hand.direction,
+            end_dir: hand.direction,
+            x: hand.x - 1,
+            y: hand.y,
+        });
+        hand.target_x = hand.x + 1;
+        hand.target_y = hand.y;
+    } else if (hand.x >= game.level_w - 1) {
+        hand.direction = zb.dir.left;
+        hand.x = game.level_w;
+        arm.push({
+            start_dir: hand.direction,
+            end_dir: hand.direction,
+            x: hand.x + 1,
+            y: hand.y,
+        });
+        hand.target_x = hand.x - 1;
+        hand.target_y = hand.y;
+    } else if (hand.y >= game.level_h - 1) {
+        hand.direction = zb.dir.up;
+        hand.y = game.level_h;
+        arm.push({
+            start_dir: hand.direction,
+            end_dir: hand.direction,
+            x: hand.x,
+            y: hand.y + 1,
+        });
+        hand.target_x = hand.x;
+        hand.target_y = hand.y - 1;
+    }
+    arm.push({
+        start_dir: hand.direction,
+        end_dir: hand.direction,
+        x: hand.x,
+        y: hand.y,
+    });
+
+    game.state = State.EXTEND;
+}
+
+function save() {
+    console.log("Saving");
+    let save_data = level_number;
+    if (game.state === State.WIN) {
+        save_data = level_number + 1;
+    }
+    game.save('continue_level', save_data);
+}
+
+function reset_save() {
+    game.save('continue_level', 1);
+}
+
+function win() {
+    console.log("You won!");
+    game.state = State.WIN;
+    game.sfx.win.play();
+
+    save();
+
+    console.log("SWEET VICTORY!");
+    game.state = State.WIN;
+    win_progress = 0;
+    win_offset = game.screen_h;
+    space_pressed = false;
+    if (gripping) game.sfx.ungrab.play();
+    gripping = false;
+
+    window.setTimeout(function() {
+        can_continue = true;
+    }, 350);
+}
+
+/* ---------- update functions ------------ */
+
+/* MAIN UPDATE FUNCTION */
+function do_update(delta) {
+    tile_frame_timer += delta;
+
+    update_canvas_hue_filter(delta);
+
+    if (game.state === State.STAND && arrows.length === 0 && !space_pressed && !x_pressed) {
+        instructions_timer += delta;
+    } else {
+        instructions_timer = 0;
+    }
+
+    if (z_pressed && game.state === State.STAND) {
+        if (undo_timer === 0) {
+            undo();
+        }
+        undo_timer += delta;
+        if (undo_timer > UNDO_HOLD) {
+            if (undo_timer - UNDO_HOLD > UNDO_SPACING) {
+                undo();
+                undo_timer = UNDO_HOLD;
+            }
+        }
+    }
+
+    while (tile_frame_timer > TILE_FRAME_LENGTH) {
+        tile_frame_timer -= TILE_FRAME_LENGTH;
+        tile_frame ++;
+        tile_frame %= NTILEFRAMES;
+    }
+
+    if (game.state === State.WIN || game.state === State.CONTINUE) {
+        win_progress += delta / WIN_LENGTH;
+        if (win_progress > 1 && game.state === State.WIN) win_progress = 1;
+        win_offset = 1 - Math.pow(1 - win_progress, 3);
+        if (game.state === State.WIN && space_pressed) {
+            game.state = State.CONTINUE;
+        }
+        if (game.state === State.CONTINUE && win_progress > 1.2) {
+            game.long_transition(zb.transition.type.fade, 700, function() {
+                advance_level();
+            });
+        }
+    }
+
+    if (game.state === State.STAND) {
+        if (space_pressed) {
+            if (!gripping) {
+                game.sfx.grab.play();
+            }
+            gripping = true;
+            hand.variant = 1;
+        } else {
+            if (gripping) {
+                game.sfx.ungrab.play();
+            }
+            gripping = false;
+            hand.variant = 0;
+        }
+
+        if (arrows.length === 0) {
+            if (x_pressed && arm.length > 2) {
+                gripping = false;
+                hand.variant = 0;
+                space_pressed = false;
+                game.music.retract.play();
+                retract(HAND_MOVE_LENGTH/2);
+            }
+        }
+    }
+
+    if ((!x_pressed || arm.length < 3) && !game.music.retract.paused) {
+        game.music.retract.pause();
+    }
+
+    if (game.state === State.STAND && arrows.length > 0) {
+        let direction = arrows[arrows.length-1];
+        let next_x = hand.x + direction.x;
+        let next_y = hand.y + direction.y
+        if (arm.length > 0 && direction === arm[arm.length - 1].end_dir.opposite) {
+            let can_go = true;
+            move_will_fail = false;
+
+            if (!gripping) {
+                held_bar = null;
+            } else {
+                held_bar = bar_at(hand.x, hand.y);
+            }
+
+            if (gripping && held_bar) {
+                if (arm[arm.length - 1].start_dir !== arm[arm.length - 1].end_dir) move_will_fail = true;
+                if (held_bar.vertical === direction.vertical) can_go = false;
+                if (!bar_can_move_direction(held_bar, direction)) move_will_fail = true;
+            }
+
+            if (arm.length <= 2) can_go = false;
+
+            if (can_go) {
+                if (!move_will_fail) {
+                    create_undo_point();
+                }
+
+                if (move_will_fail) {
+                    retract(HAND_MOVE_LENGTH * 2);
+                } else {
+                    retract(HAND_MOVE_LENGTH);
+                }
+
+                if (gripping && held_bar) {
+                    move_bar_in_direction(held_bar, direction, true);
+                }
+            }
+        } else {
+            let can_go = true;
+            move_will_fail = false;
+
+            let current_bar = bar_at(hand.x, hand.y);
+            if (current_bar) can_go = false;
+
+            if (solid[tile_at(next_x, next_y)]) can_go = false;
+            if (arm_at(next_x, next_y)) move_will_fail = true;
+
+            let next_bar = bar_at(next_x, next_y);
+            if (next_bar) {
+                if (next_bar.vertical === direction.vertical) can_go = false;
+                if (gripping) can_go = false;
+            }
+
+            if (stamp_at(next_x, next_y) && can_go) {
+                move_will_fail = true;
+                move_will_ouch = true;
+            } else {
+                move_will_ouch = false;
+            }
+
+            if (next_x <= -2 || next_y <= -2 || next_x >= game.level_w + 1 || next_y >= game.level_h + 1) can_go = false;
+
+            if (can_go) {
+                if (!move_will_fail) {
+                    create_undo_point();
+                }
+
+                arm.push({
+                    start_dir: hand.direction,
+                    end_dir: direction,
+                    x: hand.x,
+                    y: hand.y,
+                });
+                hand.target_x = next_x;
+                hand.target_y = next_y;
+                if (hand.direction === direction) {
+                    hand.move_fraction = 0;
+                } else {
+                    hand.move_fraction = 0.3;
+                }
+                hand.direction = direction;
+                game.state = State.EXTEND;
+
+                if (next_bar) {
+                    held_bar = next_bar;
+                } else {
+                    held_bar = null;
+                }
+
+                if (move_will_fail) {
+                    move_speed = HAND_MOVE_LENGTH * 2;
+                } else {
+                    move_speed = HAND_MOVE_LENGTH;
+                }
+            }
+        }
+    } else if (game.state === State.EXTEND) {
+        hand.move_fraction += delta / move_speed;
+        if (move_will_fail && hand.move_fraction >= 0.6) {
+            fail_move();
+        } else if (hand.move_fraction >= 1) {
+            game.state = State.STAND;
+
+            finish_move();
+        }
+    } else if (game.state === State.RETRACT || game.state === State.ALMOSTWIN) {
+        hand.move_fraction += delta / move_speed;
+        let threshold = 1;
+        if (arm[arm.length - 1].end_dir != arm[arm.length - 1].start_dir) {
+            /* reversing bend in arm */
+            threshold = 0.7;
+        }
+        if (move_will_fail && hand.move_fraction >= 0.4) {
+            fail_move();
+        } else if (hand.move_fraction >= threshold) {
+            hand.x = hand.target_x;
+            hand.y = hand.target_y;
+            hand.move_fraction = 0;
+            if (game.state !== State.ALMOSTWIN) game.state = State.STAND;
+
+            finish_move();
+
+            hand.direction = arm[arm.length - 1].start_dir;
+            arm.pop();
+
+            check_victory();
+        }
+    }
+}
+
+function update_canvas_hue_filter(delta) {
+    if (hue_shift !== target_hue_shift) {
+        hue_shift_timer += delta / HUE_SHIFT_LENGTH;
+
+        let actual_hue_shift = (1 - hue_shift_timer) * hue_shift + hue_shift_timer * target_hue_shift;
+
+        if (hue_shift_timer >= 1) {
+            hue_shift = zb.mod(target_hue_shift, 360);
+            target_hue_shift = hue_shift;
+            actual_hue_shift = target_hue_shift;
+        }
+
+        game.canvas.style.filter = `hue-rotate(${actual_hue_shift}deg)`;
+    }
+}
+
+function do_update_menu(delta) {
+    title_timer += delta;
+}
+
+/* ---------- draw functions ----------- */
+
+/* DRAW */
+function do_draw(ctx) {
+    ctx.save();
+    ctx.translate(game.tile_size/2, game.tile_size/2);
+
+    draw_map(ctx);
+
+    draw_levelnum(ctx);
+
+    draw_help(ctx);
+
+    draw_arm(ctx, 0);
+
+    draw_stamps(ctx);
+
+    draw_hand(ctx, 0);
+
+    draw_bars(ctx);
+
+    draw_hand(ctx, 1);
+
+    ctx.restore();
+}
+
+function do_draw_after_transition(ctx) {
+    draw_win(ctx);
+}
+
+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 frame = tile_frame;
+            if ((x + y) % 2 == 1 && decorative[tile]) {
+                frame = zb.mod(tile_frame + 2, NTILEFRAMES);
+            }
+            zb.sprite_draw(ctx, game.img.tiles, game.tile_size, game.tile_size, tile, frame, x * game.tile_size, y * game.tile_size);
+        }
+    }
+
+    for (let y = 0; y < game.level_h; y++) {
+        for (let x = 0; x < game.level_w; x++) {
+            if (map[y * game.level_w + x] === 0
+                    && map[(y + 1) * game.level_w + x] === 0
+                    && map[y * game.level_w + (x + 1)] === 0
+                    && map[(y + 1) * game.level_w + (x + 1)] === 0
+                    && x < game.level_w - 1 && y < game.level_h - 1) {
+                ctx.drawImage(game.img.indicator,
+                    (x + 1) * game.tile_size - game.img.indicator.width / 2,
+                    (y + 1) * game.tile_size - game.img.indicator.height / 2);
+            }
+        }
+    }
+}
+
+function draw_hand(ctx, column) {
+    let mf = hand.move_fraction || 0;
+    let x = hand.x;
+    let y = hand.y;
+    let tx = hand.target_x != undefined ? hand.target_x : x;
+    let ty = hand.target_y != undefined ? hand.target_y : y;
+    let px = (tx * mf + x * (1 - mf)) * game.tile_size;
+    let py = (ty * mf + y * (1 - mf)) * game.tile_size;
+
+    ctx.save();
+    ctx.translate(px + game.tile_size / 2, py + game.tile_size / 2);
+    zb.sprite_draw(ctx, game.img.hand,
+        game.tile_size + 2, game.tile_size + 2,
+        hand.direction.index * 2 + column, hand.variant,
+        -game.tile_size / 2 - 1, -game.tile_size / 2 - 1);
+    ctx.restore();
+}
+
+function draw_arm(ctx) {
+    for (let a of arm) {
+        let arm_id;
+        if (a.start_dir === a.end_dir) {
+            if (a.start_dir === zb.dir.up || a.start_dir === zb.dir.down) {
+                arm_id = 0;
+            } else {
+                arm_id = 1;
+            }
+        } else {
+            if (a.start_dir === zb.dir.right && a.end_dir === zb.dir.up
+                    || a.start_dir === zb.dir.down && a.end_dir === zb.dir.left) {
+                arm_id = 2;
+            } else if (a.start_dir === zb.dir.left && a.end_dir === zb.dir.up
+                    || a.start_dir === zb.dir.down && a.end_dir === zb.dir.right) {
+                arm_id = 3;
+            } else if (a.start_dir === zb.dir.left && a.end_dir === zb.dir.down
+                    || a.start_dir === zb.dir.up && a.end_dir === zb.dir.right) {
+                arm_id = 4;
+            } else if (a.start_dir === zb.dir.right && a.end_dir === zb.dir.down
+                    || a.start_dir === zb.dir.up && a.end_dir === zb.dir.left) {
+                arm_id = 5;
+            }
+        }
+        zb.sprite_draw(ctx, game.img.arm, game.tile_size, game.tile_size, arm_id, 0, a.x * game.tile_size, a.y * game.tile_size);
+    }
+}
+
+function draw_bars(ctx) {
+    for (let b of bars) {
+        for (let c of b.components) {
+            let mf = hand.move_fraction || 0; /* move in sync with hand */
+            let x = c.x;
+            let y = c.y;
+            let tx = c.target_x != undefined ? c.target_x : x;
+            let ty = c.target_y != undefined ? c.target_y : y;
+            let px = (tx * mf + x * (1 - mf)) * game.tile_size;
+            let py = (ty * mf + y * (1 - mf)) * game.tile_size;
+
+            zb.sprite_draw(ctx, game.img.bars, game.tile_size, game.tile_size, c.variant, 0, px, py);
+        }
+    }
+}
+
+function draw_stamps(ctx) {
+    for (let s of stamps) {
+        let mf = hand.move_fraction || 0; /* move in sync with hand */
+        let x = s.x;
+        let y = s.y;
+        let tx = s.target_x != undefined ? s.target_x : x;
+        let ty = s.target_y != undefined ? s.target_y : y;
+        let px = (tx * mf + x * (1 - mf)) * game.tile_size;
+        let py = (ty * mf + y * (1 - mf)) * game.tile_size;
+
+        zb.sprite_draw(ctx, game.img.stamp, game.tile_size, game.tile_size, 0, level_number - 1, px, py);
+    }
+}
+
+function draw_win(ctx) {
+    if (game.state === State.WIN || game.state === State.CONTINUE) {
+        let title_x = game.screen_w - win_offset * game.screen_w;
+        ctx.save();
+        ctx.translate(title_x, 0);
+        zb.screen_draw(ctx, game.img.level_complete);
+        ctx.restore();
+
+        let pstc_x = -game.screen_w + win_offset * game.screen_w;
+        ctx.save();
+        ctx.translate(pstc_x, 0);
+        zb.screen_draw(ctx, game.img.press_space_to_continue);
+        ctx.restore();
+
+        let stamp_y = game.screen_h - (win_offset * game.screen_h) + (game.screen_h - STAMP_H) / 2;
+        zb.sprite_draw(ctx, game.img.stamp_large, STAMP_W, STAMP_H, 0, level_number - 1, (game.screen_w - STAMP_W) / 2, stamp_y);
+    }
+}
+
+function draw_levelnum(ctx) {
+    ctx.save();
+    ctx.translate(-game.tile_size / 2, -game.tile_size / 2);
+
+    if (levels[level_number].start_y > game.level_h / 2) {
+        if (levels[level_number].start_x > game.level_w / 2) {
+            zb.sprite_draw(ctx, game.img.levelnums,
+                game.img.levelnums.width, 8,
+                0, level_number - 1,
+                0, game.screen_h - game.tile_size / 2);
+        } else {
+            zb.sprite_draw(ctx, game.img.levelnums,
+                game.img.levelnums.width, 8,
+                0, level_number - 1,
+                game.screen_w - game.img.levelnums.width, game.screen_h - game.tile_size / 2);
+        }
+    } else {
+        if (levels[level_number].start_x > game.level_w / 2) {
+            zb.sprite_draw(ctx, game.img.levelnums,
+                game.img.levelnums.width, 8,
+                0, level_number - 1,
+                0, -1);
+        } else {
+            zb.sprite_draw(ctx, game.img.levelnums,
+                game.img.levelnums.width, 8,
+                0, level_number - 1,
+                game.screen_w - game.img.levelnums.width, -1);
+        }
+    }
+
+    ctx.restore();
+}
+
+function draw_help(ctx) {
+    if (level_number === 1 || instructions_timer > INSTRUCTIONS_PAUSE) {
+        ctx.save();
+        ctx.globalAlpha = Math.min((instructions_timer - INSTRUCTIONS_PAUSE) / INSTRUCTIONS_FADE_LENGTH, 1);
+        if (level_number === 1) ctx.globalAlpha = 1;
+        ctx.translate(-game.tile_size / 2, -game.tile_size / 2);
+
+        if (level_number !== 1 && arm[0].y > game.level_h / 2) {
+            ctx.drawImage(game.img.instructions,
+                0, 0, game.screen_w, game.screen_h / 2,
+                0, -1, game.screen_w, game.screen_h / 2);
+        } else {
+            ctx.drawImage(game.img.instructions,
+                0, game.screen_h / 2, game.screen_w, game.screen_h / 2,
+                0, game.screen_h / 2, game.screen_w, game.screen_h / 2);
+        }
+
+        ctx.restore();
+    }
+}
+
+function do_draw_menu(ctx) {
+    ctx.save();
+    ctx.translate(0, Math.sin(title_timer / TITLE_BOUNCE_LENGTH * 2 * Math.PI) * TITLE_BOUNCE_DISTANCE);
+    zb.screen_draw(ctx, game.img.title);
+    ctx.restore();
+
+    zb.screen_draw(ctx, game.img.titlegraphic);
+    zb.buttons.draw(ctx, game.buttons.menu);
+}
+
+function do_draw_youwon(ctx) {
+    zb.screen_draw(ctx, game.img.youwin);
+}
+
+/* ---------- event handlers ------------ */
+
+function handle_gamestart(game) {
+    console.log("Game start!");
+
+    //game.music.bgm_menu.play();
+}
+
+function toggle_mute_button() {
+    /*if (game.buttons.ingame.buttons[2].id === 2) {
+        game.buttons.ingame.buttons[2].id = 3;
+    } else {
+        game.buttons.ingame.buttons[2].id = 2;
+    }*/
+    game.toggle_mute();
+}
+
+function handle_keydown(game, e) {
+    if (game.transition.is_transitioning) return;
+    if (e.repeat) return;
+
+    switch (e.key) {
+        case 'ArrowRight':
+            arrows.push(zb.dir.right);
+            instructions_timer = 0;
+            break;
+        case 'ArrowDown':
+            arrows.push(zb.dir.down);
+            instructions_timer = 0;
+            break;
+        case 'ArrowLeft':
+            arrows.push(zb.dir.left);
+            instructions_timer = 0;
+            break;
+        case 'ArrowUp':
+            arrows.push(zb.dir.up);
+            instructions_timer = 0;
+            break;
+        case ' ':
+            space_pressed = true;
+            instructions_timer = 0;
+            break;
+        case 'x':
+            x_pressed = true;
+            instructions_timer = 0;
+            break;
+        case 'z':
+            z_pressed = true;
+            undo_timer = 0;
+            break;
+    }
+}
+
+function handle_keyup(game, e) {
+    if (game.transition.is_transitioning) return;
+
+    switch (e.key) {
+        case 'ArrowRight':
+            arrows = arrows.filter(a => a !== zb.dir.right);
+            instructions_timer = 0;
+            break;
+        case 'ArrowDown':
+            arrows = arrows.filter(a => a !== zb.dir.down);
+            instructions_timer = 0;
+            break;
+        case 'ArrowLeft':
+            arrows = arrows.filter(a => a !== zb.dir.left);
+            instructions_timer = 0;
+            break;
+        case 'ArrowUp':
+            arrows = arrows.filter(a => a !== zb.dir.up);
+            instructions_timer = 0;
+            break;
+        case ' ':
+            space_pressed = false;
+            instructions_timer = 0;
+            break;
+        case 'x':
+            x_pressed = false;
+            instructions_timer = 0;
+            break;
+        case 'm':
+            toggle_mute_button();
+            e.preventDefault();
+            break;
+        case 'r':
+            reset();
+            e.preventDefault();
+            instructions_timer = 0;
+            break;
+        case 'z':
+            z_pressed = false;
+            e.preventDefault();
+            instructions_timer = 0;
+            break;
+    }
+}
+
+function handle_mousedown(game, e, x, y) {
+    if (game.mode === 'menu') {
+        if (zb.buttons.click(game.buttons.menu, x, y)) return;
+    }
+}
+
+function handle_mousemove(game, e, x, y) {
+    if (game.mode === 'menu') {
+        zb.buttons.update(game.buttons.menu, x, y);
+    }
+}
+
+function handle_mouseup(game, e, x, y) {
+    if (game.mode === 'menu') {
+        zb.buttons.update(game.buttons.menu, x, y);
+    } else if (game.mode === 'youwon') {
+        game.long_transition(zb.transition.type.fade, 1000, function() {
+            game.mode = 'menu';
+            continue_level = 1;
+        });
+    }
+}
diff --git a/img/arm.png b/img/arm.png
new file mode 100644 (file)
index 0000000..1d88b7d
Binary files /dev/null and b/img/arm.png differ
diff --git a/img/bars.png b/img/bars.png
new file mode 100644 (file)
index 0000000..9179fbc
Binary files /dev/null and b/img/bars.png differ
diff --git a/img/buttons.png b/img/buttons.png
new file mode 100644 (file)
index 0000000..b089897
Binary files /dev/null and b/img/buttons.png differ
diff --git a/img/continue.png b/img/continue.png
new file mode 100644 (file)
index 0000000..3bb7f3d
Binary files /dev/null and b/img/continue.png differ
diff --git a/img/hand.png b/img/hand.png
new file mode 100644 (file)
index 0000000..80cf96d
Binary files /dev/null and b/img/hand.png differ
diff --git a/img/indicator.png b/img/indicator.png
new file mode 100644 (file)
index 0000000..e414ba3
Binary files /dev/null and b/img/indicator.png differ
diff --git a/img/instructions.png b/img/instructions.png
new file mode 100644 (file)
index 0000000..841f5a0
Binary files /dev/null and b/img/instructions.png differ
diff --git a/img/level-complete.png b/img/level-complete.png
new file mode 100644 (file)
index 0000000..cae66ef
Binary files /dev/null and b/img/level-complete.png differ
diff --git a/img/levelnums.png b/img/levelnums.png
new file mode 100644 (file)
index 0000000..d2d5379
Binary files /dev/null and b/img/levelnums.png differ
diff --git a/img/stamp-large.png b/img/stamp-large.png
new file mode 100644 (file)
index 0000000..2bb11d6
Binary files /dev/null and b/img/stamp-large.png differ
diff --git a/img/stamp.png b/img/stamp.png
new file mode 100644 (file)
index 0000000..1b8c3e0
Binary files /dev/null and b/img/stamp.png differ
diff --git a/img/tiles.png b/img/tiles.png
new file mode 100644 (file)
index 0000000..36a782c
Binary files /dev/null and b/img/tiles.png differ
diff --git a/img/title.png b/img/title.png
new file mode 100644 (file)
index 0000000..e64703e
Binary files /dev/null and b/img/title.png differ
diff --git a/img/titlegraphic.png b/img/titlegraphic.png
new file mode 100644 (file)
index 0000000..1ddfac7
Binary files /dev/null and b/img/titlegraphic.png differ
diff --git a/img/youwin.png b/img/youwin.png
new file mode 100644 (file)
index 0000000..a7a1e70
Binary files /dev/null and b/img/youwin.png differ
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..fc31109
--- /dev/null
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head><title>
+    alien postage stamp game
+</title>
+<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, width=100%, target-densitydpi=device-dpi, user-scalable=no" />
+<style>
+* { border: 0; margin: 0; overflow: hidden }
+/*body { height: 100vh }
+canvas { object-fit: contain; width: 100%; height: 100% }*/
+</style>
+</head>
+<body style="background: black">
+    <canvas id="canvas" width="720" height="576" 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..5418779
--- /dev/null
+++ b/levels.js
@@ -0,0 +1,14 @@
+var levels={
+        10: { map: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,3,3,3,3,3,3,3,3,0,6,6,6,3,0,0,0,0,0,0,0,0,3,0,6,6,6,3,0,0,0,0,0,0,0,0,3,0,0,0,0,3,0,0,0,0,0,0,0,0,3,0,8,0,8,3,1,1,1,1,0,0,0,0,3,0,0,8,0,0,0,0,0,0,0,0,0,1,3,0,8,0,8,0,0,0,0,0,0,0,0,0,3,0,0,0,0,3,0,0,0,0,0,0,0,0,3,0,6,6,6,3,3,3,3,3,3,3,3,3,3,0,6,6,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,], bars: [[[5,2],[6,2],[7,2],],[[1,3],[1,4],],], stamps: [[6,5],], start_x: 0, start_y: 6 },
+        11: { map: [1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,7,7,7,0,0,0,0,0,0,0,0,0,1,0,5,5,5,1,0,2,2,2,0,0,0,2,1,0,8,8,8,1,0,0,0,0,0,0,0,0,1,0,6,6,6,1,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,6,6,6,1,0,0,0,0,0,0,0,0,1,0,8,8,8,1,0,0,0,0,0,0,0,0,1,0,5,5,5,1,0,0,0,0,0,0,0,0,1,0,7,7,7,1,1,1,1,1,1,1,1,1,1,0,0,0,0,], bars: [[[3,4],[4,4],],[[5,4],[6,4],[7,4],],[[5,5],[5,6],],], stamps: [[4,5],], start_x: 0, start_y: 2 },
+        12: { map: [7,0,2,2,2,2,2,2,2,2,2,2,0,5,6,0,2,0,0,0,0,0,0,0,0,2,0,8,5,0,2,0,0,0,0,0,0,0,0,2,0,7,8,0,2,0,0,0,0,0,0,0,0,2,0,6,7,0,2,4,0,0,0,0,0,0,0,2,0,5,6,0,2,0,0,0,0,0,0,4,4,2,0,8,5,0,2,0,0,0,0,0,0,0,0,2,0,7,8,0,2,0,4,0,4,0,0,0,0,2,0,6,7,0,2,0,0,0,0,0,0,0,0,2,0,5,6,0,2,0,0,0,0,0,0,0,0,2,0,8,5,0,2,2,2,2,2,2,0,0,2,2,0,7,], bars: [[[8,2],[9,2],],[[10,3],[10,4],],[[3,8],[3,9],],], stamps: [[5,8],], start_x: 8, start_y: 10 },
+        1: { map: [0,7,0,0,1,1,0,0,1,1,0,0,7,0,7,0,7,0,1,0,0,0,0,1,0,7,0,7,0,7,0,0,1,0,0,0,0,1,0,0,7,0,7,0,7,0,1,0,0,0,0,1,0,7,0,7,0,7,0,0,1,0,0,0,0,1,0,0,7,0,7,0,7,0,1,0,0,0,0,1,0,7,0,7,0,7,0,0,1,0,0,0,0,1,0,0,7,0,7,0,7,0,1,0,0,0,0,1,0,7,0,7,0,7,0,0,1,0,0,0,0,1,0,0,7,0,7,0,7,0,1,0,0,0,0,1,0,7,0,7,0,7,0,0,1,1,1,1,1,1,0,0,7,0,], bars: [[[6,8],[7,8],],], stamps: [[7,7],], start_x: 6, start_y: 0 },
+        2: { map: [6,0,0,6,0,4,4,4,4,4,0,0,4,4,6,0,0,6,0,4,0,0,0,0,0,0,0,4,6,0,0,6,0,4,0,0,0,0,0,0,0,4,6,0,0,6,0,4,0,0,0,0,0,0,0,4,6,0,0,6,0,4,0,0,0,0,0,0,0,4,6,0,0,6,0,4,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,5,0,6,6,6,6,6,6,6,6,6,5,0,5,0,0,0,0,0,0,0,0,0,0,0,0,5,0,5,0,0,0,0,0,0,0,0,0,0,5,0,5,0,0,6,6,6,6,6,6,6,6,6,], bars: [[[7,1],[7,2],[7,3],],[[8,3],[9,3],],], stamps: [[9,2],], start_x: 11, start_y: 0 },
+        3: { map: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,5,0,2,2,2,2,2,2,2,2,2,5,0,5,0,0,2,0,0,0,0,0,0,0,0,0,5,0,5,0,2,0,0,0,1,0,0,0,0,5,0,5,0,0,2,0,0,0,0,0,0,0,2,0,5,0,5,0,2,0,0,0,0,0,0,0,2,5,0,5,0,0,2,0,0,0,0,0,0,0,2,0,5,0,5,0,2,0,0,0,0,0,0,0,2,5,0,5,0,0,2,0,0,0,0,0,0,0,2,0,5,0,5,0,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,], bars: [[[7,5],[7,6],],[[8,7],[9,7],[10,7],[11,7],],], stamps: [[8,6],], start_x: 13, start_y: 2 },
+        4: { map: [0,0,0,0,0,3,3,3,3,3,3,0,0,3,0,8,8,8,0,3,0,0,0,0,0,0,0,3,0,8,8,8,0,3,0,0,0,4,0,0,0,3,0,0,0,0,0,3,0,0,0,4,0,0,0,3,0,0,6,0,0,3,0,0,0,4,0,0,0,3,0,6,0,6,0,3,0,0,0,0,0,0,0,3,0,0,6,0,0,3,0,0,0,0,0,0,0,3,0,0,0,0,0,3,0,4,4,4,0,0,0,3,0,8,8,8,0,3,0,0,0,0,0,0,0,3,0,8,8,8,0,3,0,0,0,4,0,0,0,3,0,0,0,0,0,3,3,3,3,3,3,3,3,3,], bars: [[[7,1],[8,1],],[[7,4],[7,5],],[[11,5],[12,5],],], stamps: [[10,6],], start_x: 11, start_y: 0 },
+        5: { map: [1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,6,6,6,0,1,0,0,0,0,0,4,0,1,0,6,0,6,0,1,0,0,0,0,0,0,0,1,0,6,6,6,0,1,4,0,0,0,4,4,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,5,0,5,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,4,4,0,0,0,1,0,6,6,6,0,1,0,0,0,0,0,0,0,1,0,6,0,6,0,1,0,0,0,0,0,0,0,1,0,6,6,6,0,1,0,0,1,1,1,1,1,1,0,0,0,0,0,], bars: [[[2,1],[3,1],[4,1],[5,1],],[[1,2],[1,3],],[[2,6],[3,6],],], stamps: [[2,7],], start_x: 1, start_y: 10 },
+        6: { map: [6,6,6,0,6,6,6,0,6,6,6,0,0,7,6,0,6,0,6,0,6,0,6,0,6,0,0,7,6,6,6,0,6,6,6,0,6,6,6,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,7,4,4,4,4,4,4,4,4,4,4,4,0,0,7,4,0,0,0,0,0,0,0,0,0,4,0,0,7,4,0,2,2,2,0,0,0,0,0,4,0,0,7,4,0,0,0,0,0,0,0,0,0,4,0,0,7,4,0,0,0,0,0,0,0,0,0,4,0,0,7,4,0,0,0,0,0,0,0,0,0,4,0,0,7,4,4,4,4,4,4,4,0,0,0,4,0,0,7,], bars: [[[1,7],[1,8],],[[6,8],[7,8],[8,8],],], stamps: [[7,7],], start_x: 7, start_y: 10 },
+        7: { map: [6,0,3,0,0,3,3,3,3,3,3,3,0,6,6,0,3,0,0,0,0,0,0,0,0,3,0,6,6,0,3,0,0,2,2,2,2,2,0,3,0,6,6,0,3,0,0,0,0,0,0,2,0,3,0,6,6,0,3,0,0,0,0,0,0,2,0,3,0,6,6,0,3,0,0,0,0,0,0,2,0,3,0,6,6,0,3,0,0,0,0,0,0,2,0,3,0,6,6,0,3,0,0,0,0,0,0,0,0,3,0,6,6,0,3,0,0,0,0,0,0,0,0,3,0,6,6,0,3,0,0,0,0,0,0,0,0,3,0,6,6,0,3,3,3,3,3,3,3,3,3,3,0,6,], bars: [[[7,3],[8,3],],[[7,4],[7,5],[7,6],],], stamps: [[6,7],], start_x: 3, start_y: 0 },
+        8: { map: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,7,0,2,2,2,2,2,2,2,2,2,2,2,7,7,0,2,0,0,0,0,0,0,0,0,0,0,7,7,0,2,0,0,0,0,0,0,0,0,0,0,7,7,0,2,0,0,0,3,0,0,0,0,0,2,7,7,0,2,0,0,0,3,0,0,0,0,0,2,7,7,0,2,0,0,0,0,0,0,0,0,0,2,7,7,0,2,0,0,0,0,0,0,0,0,0,2,7,7,0,2,0,0,0,0,0,0,0,0,0,2,7,7,0,2,2,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,], bars: [[[8,2],[9,2],],[[6,4],[6,5],],[[12,6],[12,7],[12,8],],], stamps: [[10,5],], start_x: 13, start_y: 3 },
+        9: { map: [7,7,7,7,7,7,7,7,7,7,7,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,4,4,4,4,4,4,4,4,4,4,0,5,5,0,4,0,0,0,0,0,0,0,0,4,0,5,5,0,4,0,0,0,0,0,0,0,0,4,0,5,5,0,4,0,0,0,0,0,0,0,0,4,0,5,5,0,4,0,0,0,0,0,0,0,0,4,0,5,5,0,4,0,0,0,0,1,1,1,0,4,0,5,5,0,4,0,0,0,0,0,0,0,0,4,0,5,5,0,4,0,0,0,0,0,0,0,0,4,0,5,5,0,4,4,4,4,0,0,4,4,4,4,0,5,], bars: [[[6,3],[7,3],],[[10,5],[10,6],],], stamps: [[6,8],], start_x: 7, start_y: 10 },
+}
diff --git a/levels/1.tmx b/levels/1.tmx
new file mode 100644 (file)
index 0000000..3b5efde
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+1,16,1,1,2,2,6,1,2,2,1,1,16,1,
+16,1,16,1,2,1,1,1,1,2,1,16,1,16,
+1,16,1,1,2,1,1,1,1,2,1,1,16,1,
+16,1,16,1,2,1,1,1,1,2,1,16,1,16,
+1,16,1,1,2,1,1,1,1,2,1,1,16,1,
+16,1,16,1,2,1,1,1,1,2,1,16,1,16,
+1,16,1,1,2,1,1,1,1,2,1,1,16,1,
+16,1,16,1,2,1,1,13,1,2,1,16,1,16,
+1,16,1,1,2,1,7,9,1,2,1,1,16,1,
+16,1,16,1,2,1,1,1,1,2,1,16,1,16,
+1,16,1,1,2,2,2,2,2,2,1,1,16,1
+</data>
+ </layer>
+</map>
diff --git a/levels/10.tmx b/levels/10.tmx
new file mode 100644 (file)
index 0000000..bfa0ef6
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+4,4,4,4,4,4,4,4,4,4,1,15,15,15,
+4,1,1,1,1,7,8,9,1,4,1,15,15,15,
+4,10,1,1,1,1,1,1,1,4,1,1,1,1,
+4,12,1,1,1,1,1,1,1,4,1,17,1,17,
+4,2,2,2,2,1,13,1,1,4,1,1,17,1,
+6,1,1,1,1,1,1,1,2,4,1,17,1,17,
+1,1,1,1,1,1,1,1,1,4,1,1,1,1,
+4,1,1,1,1,1,1,1,1,4,1,15,15,15,
+4,4,4,4,4,4,4,4,4,4,1,15,15,15,
+1,1,1,1,1,1,1,1,1,1,1,1,1,1
+</data>
+ </layer>
+</map>
diff --git a/levels/11.tmx b/levels/11.tmx
new file mode 100644 (file)
index 0000000..a9a4c6f
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+2,2,2,2,2,2,2,2,2,2,1,1,1,1,
+1,1,1,1,1,1,1,1,1,2,1,16,16,16,
+6,1,1,1,1,1,1,1,1,2,1,14,14,14,
+2,1,3,3,3,1,1,1,3,2,1,17,17,17,
+2,1,1,7,9,7,8,9,1,2,1,15,15,15,
+2,1,1,1,13,10,1,1,1,2,1,1,1,1,
+2,1,1,1,1,12,1,1,1,2,1,15,15,15,
+2,1,1,1,1,1,1,1,1,2,1,17,17,17,
+2,1,1,1,1,1,1,1,1,2,1,14,14,14,
+2,1,1,1,1,1,1,1,1,2,1,16,16,16,
+2,2,2,2,2,2,2,2,2,2,1,1,1,1
+</data>
+ </layer>
+</map>
diff --git a/levels/12.tmx b/levels/12.tmx
new file mode 100644 (file)
index 0000000..1d6a445
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+16,1,3,3,3,3,3,3,3,3,3,3,1,14,
+15,1,3,1,1,1,1,1,1,1,1,3,1,17,
+14,1,3,1,1,1,1,1,7,9,1,3,1,16,
+17,1,3,1,1,1,1,1,1,1,10,3,1,15,
+16,1,3,5,1,1,1,1,1,1,12,3,1,14,
+15,1,3,1,1,1,1,1,1,5,5,3,1,17,
+14,1,3,1,1,1,1,1,1,1,1,3,1,16,
+17,1,3,1,5,1,5,1,1,1,1,3,1,15,
+16,1,3,10,1,13,1,1,1,1,1,3,1,14,
+15,1,3,12,1,1,1,1,1,1,1,3,1,17,
+14,1,3,3,3,3,3,3,6,1,3,3,1,16
+</data>
+ </layer>
+</map>
diff --git a/levels/2.tmx b/levels/2.tmx
new file mode 100644 (file)
index 0000000..39789d2
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+15,1,1,15,1,5,5,5,5,5,1,6,5,5,
+15,1,1,15,1,5,1,10,1,1,1,1,1,5,
+15,1,1,15,1,5,1,11,1,13,1,1,1,5,
+15,1,1,15,1,5,1,12,7,9,1,1,1,5,
+15,1,1,15,1,5,1,1,1,1,1,1,1,5,
+15,1,1,15,1,5,5,5,5,5,5,5,5,5,
+1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+1,14,1,14,1,15,15,15,15,15,15,15,15,15,
+14,1,14,1,1,1,1,1,1,1,1,1,1,1,
+1,14,1,14,1,1,1,1,1,1,1,1,1,1,
+14,1,14,1,1,15,15,15,15,15,15,15,15,15
+</data>
+ </layer>
+</map>
diff --git a/levels/3.tmx b/levels/3.tmx
new file mode 100644 (file)
index 0000000..7f04def
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+1,14,1,14,1,3,3,3,3,3,3,3,3,3,
+14,1,14,1,1,3,1,1,1,1,1,1,1,6,
+1,14,1,14,1,3,1,1,1,2,1,1,1,1,
+14,1,14,1,1,3,1,1,1,1,1,1,1,3,
+1,14,1,14,1,3,1,10,1,1,1,1,1,3,
+14,1,14,1,1,3,1,12,13,1,1,1,1,3,
+1,14,1,14,1,3,1,1,7,8,8,9,1,3,
+14,1,14,1,1,3,1,1,1,1,1,1,1,3,
+1,14,1,14,1,3,3,3,3,3,3,3,3,3,
+1,1,1,1,1,1,1,1,1,1,1,1,1,1
+</data>
+ </layer>
+</map>
diff --git a/levels/4.tmx b/levels/4.tmx
new file mode 100644 (file)
index 0000000..5c5a078
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+1,1,1,1,1,4,4,4,4,4,4,6,1,4,
+1,17,17,17,1,4,1,7,9,1,1,1,1,4,
+1,17,17,17,1,4,1,1,1,5,1,1,1,4,
+1,1,1,1,1,4,1,1,1,5,1,1,1,4,
+1,1,15,1,1,4,1,10,1,5,1,1,1,4,
+1,15,1,15,1,4,1,12,1,1,1,7,9,4,
+1,1,15,1,1,4,1,1,1,1,13,1,1,4,
+1,1,1,1,1,4,1,5,5,5,1,1,1,4,
+1,17,17,17,1,4,1,1,1,1,1,1,1,4,
+1,17,17,17,1,4,1,1,1,5,1,1,1,4,
+1,1,1,1,1,4,4,4,4,4,4,4,4,4
+</data>
+ </layer>
+</map>
diff --git a/levels/5.tmx b/levels/5.tmx
new file mode 100644 (file)
index 0000000..e1aed5a
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+2,2,2,2,2,2,2,2,2,1,1,1,1,1,
+2,1,7,8,8,9,1,1,2,1,15,15,15,1,
+2,10,1,1,1,1,5,1,2,1,15,1,15,1,
+2,12,1,1,1,1,1,1,2,1,15,15,15,1,
+2,5,1,1,1,5,5,1,2,1,1,1,1,1,
+2,1,1,1,1,1,1,1,2,1,14,1,14,1,
+2,1,7,9,1,1,1,1,2,1,1,1,1,1,
+2,1,13,5,5,1,1,1,2,1,15,15,15,1,
+2,1,1,1,1,1,1,1,2,1,15,1,15,1,
+2,1,1,1,1,1,1,1,2,1,15,15,15,1,
+2,6,1,2,2,2,2,2,2,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..2777bd8
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+15,15,15,1,15,15,15,1,15,15,15,1,1,16,
+15,1,15,1,15,1,15,1,15,1,15,1,1,16,
+15,15,15,1,15,15,15,1,15,15,15,1,1,16,
+1,1,1,1,1,1,1,1,1,1,1,1,1,16,
+5,5,5,5,5,5,5,5,5,5,5,1,1,16,
+5,1,1,1,1,1,1,1,1,1,5,1,1,16,
+5,1,3,3,3,1,1,1,1,1,5,1,1,16,
+5,10,1,1,1,1,1,13,1,1,5,1,1,16,
+5,12,1,1,1,1,7,8,9,1,5,1,1,16,
+5,1,1,1,1,1,1,1,1,1,5,1,1,16,
+5,5,5,5,5,5,5,6,1,1,5,1,1,16
+</data>
+ </layer>
+</map>
diff --git a/levels/7.tmx b/levels/7.tmx
new file mode 100644 (file)
index 0000000..44b317f
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+15,1,4,6,1,4,4,4,4,4,4,4,1,15,
+15,1,4,1,1,1,1,1,1,1,1,4,1,15,
+15,1,4,1,1,3,3,3,3,3,1,4,1,15,
+15,1,4,1,1,1,1,7,9,3,1,4,1,15,
+15,1,4,1,1,1,1,10,1,3,1,4,1,15,
+15,1,4,1,1,1,1,11,1,3,1,4,1,15,
+15,1,4,1,1,1,1,12,1,3,1,4,1,15,
+15,1,4,1,1,1,13,1,1,1,1,4,1,15,
+15,1,4,1,1,1,1,1,1,1,1,4,1,15,
+15,1,4,1,1,1,1,1,1,1,1,4,1,15,
+15,1,4,4,4,4,4,4,4,4,4,4,1,15
+</data>
+ </layer>
+</map>
diff --git a/levels/8.tmx b/levels/8.tmx
new file mode 100644 (file)
index 0000000..c1b6e45
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+16,16,1,3,3,3,3,3,3,3,3,3,3,3,
+16,16,1,3,1,1,1,1,7,9,1,1,1,1,
+16,16,1,3,1,1,1,1,1,1,1,1,1,6,
+16,16,1,3,1,1,10,4,1,1,1,1,1,3,
+16,16,1,3,1,1,12,4,1,1,13,1,1,3,
+16,16,1,3,1,1,1,1,1,1,1,1,10,3,
+16,16,1,3,1,1,1,1,1,1,1,1,11,3,
+16,16,1,3,1,1,1,1,1,1,1,1,12,3,
+16,16,1,3,3,3,3,3,3,3,3,3,3,3,
+1,1,1,1,1,1,1,1,1,1,1,1,1,1
+</data>
+ </layer>
+</map>
diff --git a/levels/9.tmx b/levels/9.tmx
new file mode 100644 (file)
index 0000000..4a970dc
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.2" tiledversion="1.3.2" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="14" height="11" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
+ <tileset firstgid="1" source="tmptile.tsx"/>
+ <layer id="1" name="Tile Layer 1" width="14" height="11">
+  <data encoding="csv">
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+14,1,5,5,5,5,5,5,5,5,5,5,1,14,
+14,1,5,1,1,1,7,9,1,1,1,5,1,14,
+14,1,5,1,1,1,1,1,1,1,1,5,1,14,
+14,1,5,1,1,1,1,1,1,1,10,5,1,14,
+14,1,5,1,1,1,1,1,1,1,12,5,1,14,
+14,1,5,1,1,1,1,2,2,2,1,5,1,14,
+14,1,5,1,1,1,13,1,1,1,1,5,1,14,
+14,1,5,1,1,1,1,1,1,1,1,5,1,14,
+14,1,5,5,5,5,1,6,5,5,5,5,1,14
+</data>
+ </layer>
+</map>
diff --git a/levels/mapcompile.py b/levels/mapcompile.py
new file mode 100755 (executable)
index 0000000..3047320
--- /dev/null
@@ -0,0 +1,79 @@
+#!/usr/bin/python3
+
+import ulvl
+import sys
+
+if len(sys.argv) < 2:
+    print("usage:", sys.argv[0], "<infiles>")
+    sys.exit(1)
+
+screenwidth, screenheight = 14, 11
+
+tilemapping = { 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 5, 14: 6, 15: 7, 16: 8 }
+
+decomapping = { }
+
+print("var levels={")
+for filename in sys.argv[1:]:
+    m = ulvl.TMX.load(filename)
+
+    w = m.meta['width']
+    h = m.meta['height']
+
+    start_x = 0
+    start_y = 0
+
+    bars = []
+    stamps = []
+
+    print('\t', filename.replace('.tmx', '').replace('levels/', ''), end=': { ')
+
+    print('map: [', end='')
+    for y in range(screenheight):
+        for x in range(screenwidth):
+            thing = m.layers[0].tiles[y * w + x] - 1
+
+            if thing == 5:
+                start_x = x
+                start_y = y
+
+            if thing == 6: # left side
+                look_x, look_y = x, y
+                bar = [[look_x, look_y]]
+                while m.layers[0].tiles[look_y * w + look_x] - 1 != 8: # right side
+                    look_x += 1
+                    bar.append([ look_x, look_y ])
+                bars.append(bar)
+
+            if thing == 9: # top side
+                look_x, look_y = x, y
+                bar = [(look_x, look_y)]
+                while m.layers[0].tiles[look_y * w + look_x] - 1 != 11: # bottom side
+                    look_y += 1
+                    bar.append((look_x, look_y))
+                bars.append(bar)
+
+            if thing == 12: # stamp
+                stamps.append((x, y))
+
+            print(tilemapping.get(thing, thing), end='')
+            print(',', end='')
+    print('], bars: [', end='')
+    for bar in bars:
+        print('[', end='')
+        for coords in bar:
+            print('[', end='')
+            print(coords[0], end=',')
+            print(coords[1], end='],')
+        print('],', end='')
+    print('], stamps: [', end='')
+    for stamp in stamps:
+        print('[', end='')
+        print(stamp[0], end=',')
+        print(stamp[1], end='],')
+    print('], start_x: ', end='')
+    print(start_x, end='')
+    print(', start_y: ', end='')
+    print(start_y, end='')
+    print(' },');
+print("}")
diff --git a/levels/tmptile.png b/levels/tmptile.png
new file mode 100644 (file)
index 0000000..d5ba5d9
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..2c39c67
--- /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="2" columns="2">
+ <image source="tmptile.png" width="32" height="16"/>
+</tileset>
diff --git a/loading.png b/loading.png
new file mode 100644 (file)
index 0000000..a4b9d58
Binary files /dev/null and b/loading.png differ
diff --git a/music/in_the_garden.mp3 b/music/in_the_garden.mp3
new file mode 100755 (executable)
index 0000000..3e351f4
Binary files /dev/null and b/music/in_the_garden.mp3 differ
diff --git a/pause.png b/pause.png
new file mode 100644 (file)
index 0000000..7479e89
Binary files /dev/null and b/pause.png differ
diff --git a/progressbar.png b/progressbar.png
new file mode 100644 (file)
index 0000000..24a6aba
Binary files /dev/null and b/progressbar.png differ
diff --git a/saveerror.png b/saveerror.png
new file mode 100644 (file)
index 0000000..fd1f8e7
Binary files /dev/null and b/saveerror.png differ
diff --git a/sfx/grab.mp3 b/sfx/grab.mp3
new file mode 100644 (file)
index 0000000..8d191fd
Binary files /dev/null and b/sfx/grab.mp3 differ
diff --git a/sfx/ouch.mp3 b/sfx/ouch.mp3
new file mode 100644 (file)
index 0000000..9681aea
Binary files /dev/null and b/sfx/ouch.mp3 differ
diff --git a/sfx/pull.mp3 b/sfx/pull.mp3
new file mode 100644 (file)
index 0000000..0707008
Binary files /dev/null and b/sfx/pull.mp3 differ
diff --git a/sfx/retract.mp3 b/sfx/retract.mp3
new file mode 100644 (file)
index 0000000..acfadbe
Binary files /dev/null and b/sfx/retract.mp3 differ
diff --git a/sfx/ungrab.mp3 b/sfx/ungrab.mp3
new file mode 100644 (file)
index 0000000..fe905a7
Binary files /dev/null and b/sfx/ungrab.mp3 differ
diff --git a/sfx/win.mp3 b/sfx/win.mp3
new file mode 100644 (file)
index 0000000..7b077c5
Binary files /dev/null and b/sfx/win.mp3 differ
diff --git a/zucchinibread.js b/zucchinibread.js
new file mode 100644 (file)
index 0000000..df44235
--- /dev/null
@@ -0,0 +1,1339 @@
+"use strict";
+
+/* Please forgive my strictly procedural/functional style here.
+ * Thanks for reading though! */
+
+let zb = (function() {
+    /* ---- Util ---- */
+
+    function mod(x, n) {
+         return ((x%n)+n)%n;
+    }
+
+    function sgn(x) {
+         if (x === 0) {
+             return 0;
+         } else {
+             return x / Math.abs(x);
+         }
+    }
+
+    function as_hex(num, length) {
+        let result = Math.round(num).toString(16);
+
+        if (length) {
+            while (result.length < length) {
+                result = "0" + result;
+            }
+        }
+
+        return result;
+    }
+
+    function copy_list(list) {
+        let newlist = [];
+        for (let x of list) {
+            newlist.push(x);
+        }
+        return newlist;
+    }
+
+    function copy_flat_objlist(list) {
+        let newlist = [];
+        for (let x of list) {
+            newlist.push({ ...x });
+        }
+        return newlist;
+    }
+
+    function rand_int(a, b) {
+    }
+
+    /* ---- Resource loading / loading screen ---- */
+
+    let _audiocheck = document.createElement('audio');
+
+    let _SFX_ARRAY_SIZE = 10;
+
+    /* Returns a callback that should be called when a resource finishes loading. */
+    function _register_resource(name, game, callback) {
+        game._total_things_to_load ++;
+        console.log("Loading", name + ". Things to load:", game._total_things_to_load);
+        return function() {
+            if (!game.ready_to_go) {
+                game._things_loaded ++;
+                console.log("Loaded", name + ". Things loaded:", game._things_loaded, "/", game._total_things_to_load);
+                if (game.load_with_progress_bar) {
+                    if (game._progressbar_img_loaded) {
+                        game.ctx.global.save();
+                        game.ctx.global.scale(game.draw_scale, game.draw_scale);
+                        game.ctx.global.drawImage(
+                            game.img._progressbar,
+                            0,
+                            0,
+                            Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width),
+                            game.img._progressbar.height,
+                            Math.round(game.canvas_w / 2 / game.draw_scale - game.img._progressbar.width / 2),
+                            Math.round(game.canvas_h / 2 / game.draw_scale - game.img._progressbar.height / 2),
+                            Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width),
+                            game.img._progressbar.height,
+                        );
+                        game.ctx.global.restore();
+                    }
+                }
+                _check_if_loaded(game);
+                if (callback) {
+                    callback();
+                }
+            }
+        }
+    }
+
+    /* Check if audio failed to load bc file doesn't exist and print error message. */
+    /* TODO add the same thing for an image */
+    function _sound_resource_error(name, game, audio) {
+        return function(e) {
+            if (audio === undefined) {
+                console.error("Error loading resource, skipping:", name);
+                game._things_loaded ++;
+                _check_if_loaded(game);
+            }
+        }
+    }
+
+    /* Check if the game has loaded everything and if so show the 'click to start' image */
+    function _check_if_loaded(game) {
+        if (game.ready_to_go) return;
+
+        if (game._things_loaded >= game._total_things_to_load) {
+            console.log("Ready");
+            game.ready_to_go = true;
+            game._on_ready();
+        }
+    }
+
+    function _register_sfx(sfxdata, game) {
+        for (let key in sfxdata) {
+            let sfx_size = sfxdata[key].copies || _SFX_ARRAY_SIZE;
+            let sfx_array = new Array(sfx_size);
+            for (let i = 0; i < sfx_size; i++) {
+                sfx_array[i] = new Audio(sfxdata[key].path);
+                sfx_array[i].addEventListener('canplaythrough',
+                    _register_resource(sfxdata[key].path + '#' + (i+1), game), false);
+                sfx_array[i].addEventListener('error',
+                    _sound_resource_error(sfxdata[key].path + '#' + (i+1), game), false, sfx_array[i]);
+                if (sfxdata[key].hasOwnProperty('volume')) {
+                    sfx_array[i].volume = sfxdata[key].volume;
+                }
+            }
+            game.sfx[key] = {
+                _array: sfx_array,
+                _index: 0,
+                play: function() {
+                    if (!game.muted) {
+                        this._array[this._index].currentTime = 0;
+                        this._array[this._index].play();
+                        this._index ++;
+                        this._index = mod(this._index, _SFX_ARRAY_SIZE);
+                    }
+                }
+            }
+        }
+    }
+
+    function _register_music(musicdata, game) {
+        for (let key in musicdata) {
+            let musicpath;
+            if (musicdata[key].path.match(/\.[a-z]{3}$/)) {
+                musicpath = musicdata[key].path;
+            } else if (_audiocheck.canPlayType('audio/mpeg')) {
+                musicpath = musicdata[key].path + '.mp3';
+            } else if (_audiocheck.canPlayType('audio/ogg')) {
+                musicpath = musicdata[key].path + '.ogg';
+            } else {
+                console.log("No supported music type known :(");
+                return;
+            }
+            let music = new Audio(musicpath);
+            if (musicdata[key].hasOwnProperty('volume')) {
+                music.volume = musicdata[key].volume;
+            }
+
+            if (!musicdata[key].hasOwnProperty('loop') || musicdata[key].loop) {
+                /* We loop by default unless 'loop: false' is specified. */
+                music.loop = true;
+            }
+            music.addEventListener('canplaythrough', _register_resource(musicpath, game), false);
+            music.addEventListener('error', _sound_resource_error(musicpath, game, music), false);
+
+            /* Handle music changes when muted, so when we unmute,
+             * the correct music will be playing. */
+            music._og_play = music.play;
+            music.play = function() {
+                if (game.muted) {
+                    music.was_playing = true;
+                } else {
+                    music._og_play();
+                }
+            }
+
+            music._og_pause = music.pause;
+            music.pause = function() {
+                if (game.muted) {
+                    music.was_playing = false;
+                } else {
+                    music._og_pause();
+                }
+            }
+
+            game.music[key] = music;
+        }
+    }
+
+    function _register_images(imgdata, game) {
+        function _recursive_load_images(pathmap) {
+            let result = {};
+            for (let key in pathmap) {
+                if (typeof pathmap[key] === 'object') {
+                    result[key] = _recursive_load_images(pathmap[key]);
+                } else {
+                    result[key] = new Image();
+                    result[key].onload = _register_resource(pathmap[key], game);
+                    result[key].src = pathmap[key];
+                }
+            }
+            return result;
+        }
+
+        let loaded_imgs = _recursive_load_images(imgdata);
+        for (let k in loaded_imgs) {
+            game.img[k] = loaded_imgs[k];
+        }
+    }
+
+    function _create_canvas_context(canvas) {
+        let new_canvas = document.createElement('canvas');
+        new_canvas.width = canvas.width;
+        new_canvas.height = canvas.height;
+
+        let new_ctx = new_canvas.getContext('2d');
+        new_ctx.imageSmoothingEnabled = false;
+        new_ctx.webkitImageSmoothingEnabled = false;
+        new_ctx.mozImageSmoothingEnabled = false;
+
+        return new_ctx;
+    }
+
+    function create_game(params) {
+        let game_props = {...params};
+
+        game_props.frame_rate = params.frame_rate || 60;
+
+        game_props.canvas_w = params.canvas_w || 640;
+        game_props.canvas_h = params.canvas_h || 480;
+        game_props.draw_scale = params.draw_scale || 4;
+        game_props.top_border = params.top_border || 0;
+        game_props.bottom_border = params.bottom_border || 8;
+        game_props.left_border = params.left_border || 0;
+        game_props.right_border = params.right_border || 0;
+
+        game_props.tile_size = params.tile_size || 16;
+        game_props.level_w = params.level_w || 10;
+        game_props.level_h = params.level_h || 7;
+
+        game_props.screen_w = game_props.canvas_w / game_props.draw_scale;
+        game_props.screen_h = game_props.canvas_h / game_props.draw_scale;
+
+        game_props.background_color = params.background_color || '#000000';
+
+        let canvas = document.getElementById(game_props.canvas);
+
+        let global_ctx = canvas.getContext('2d');
+        global_ctx.imageSmoothingEnabled = false;
+        global_ctx.webkitImageSmoothingEnabled = false;
+        global_ctx.mozImageSmoothingEnabled = false;
+
+        let mask_canvas = document.createElement('canvas');
+        mask_canvas.width = canvas.width;
+        mask_canvas.height = canvas.height;
+        let mask_ctx = mask_canvas.getContext('2d');
+        mask_ctx.imageSmoothingEnabled = false;
+        mask_ctx.webkitImageSmoothingEnabled = false;
+        mask_ctx.mozImageSmoothingEnabled = false;
+
+        let copy_canvas = document.createElement('canvas');
+        copy_canvas.width = canvas.width;
+        copy_canvas.height = canvas.height;
+        let copy_ctx = copy_canvas.getContext('2d');
+        copy_ctx.imageSmoothingEnabled = false;
+        copy_ctx.webkitImageSmoothingEnabled = false;
+        copy_ctx.mozImageSmoothingEnabled = false;
+
+        let draw_canvas = document.createElement('canvas');
+        draw_canvas.width = canvas.width;
+        draw_canvas.height = canvas.height;
+        let draw_ctx = draw_canvas.getContext('2d');
+        draw_ctx.imageSmoothingEnabled = false;
+        draw_ctx.webkitImageSmoothingEnabled = false;
+        draw_ctx.mozImageSmoothingEnabled = false;
+
+        let game = {
+            /* General properties */
+            ...game_props,
+
+            canvas: canvas,
+
+            /* Loading */
+            ready_to_go: false,
+            _total_things_to_load: 1,
+            _things_loaded: 0,
+            resources_ready: function() {
+                this._things_loaded ++;
+                console.log("Finished enumerating resources to load. Things loaded:",
+                    this._things_loaded, "/", this._total_things_to_load);
+                _check_if_loaded(this);
+            },
+            _on_ready: function() {
+                this.ready_to_go = true;
+                if (game.img._clicktostart && game.img._clicktostart.complete) {
+                    game.ctx.global.save();
+                    game.ctx.global.scale(game.draw_scale, game.draw_scale);
+                    game.ctx.global.drawImage(game.img._clicktostart, 0, 0);
+                    game.ctx.global.restore();
+                }
+            },
+            playing: false,
+            play: function() {
+                this.playing = true;
+                if (this.events.gamestart) {
+                    this.events.gamestart(this);
+                }
+                _loop(this);
+            },
+            _norun: false,
+
+            /* Audio */
+            sfx: {},
+            music: {},
+            muted: false,
+            mute: function() {
+                _mute(this);
+            },
+            unmute: function() {
+                _unmute(this);
+            },
+            toggle_mute: function() {
+                if (this.muted) {
+                    _unmute(this);
+                } else {
+                    _mute(this);
+                }
+            },
+            register_sfx: function(sfxdata) {
+                _register_sfx(sfxdata, this);
+            },
+            register_music: function(musicdata) {
+                _register_music(musicdata, this);
+            },
+
+            /* Drawing */
+            ctx: {
+                global: global_ctx, /* context for the actual real canvas */
+                mask: mask_ctx,     /* context for drawing the transition mask, gets scaled up */
+                copy: copy_ctx,     /* context for copying the old screen on transition */
+                draw: draw_ctx,     /* context for drawing the real level */
+            },
+            create_canvas_context: function() {
+                /* Function to return a fresh screen-sized canvas context, if we need it. */
+                return _create_canvas_context(this.canvas);
+            },
+            img: {},
+            register_images: function(imgdata) {
+                _register_images(imgdata, this);
+            },
+
+            /* Save */
+            save: function(key, data) {
+                _save(game, key, data);
+            },
+            load: function(key) {
+                return _load(game, key);
+            },
+
+            /* Transition system */
+            transition: _transition,
+            start_transition: function(type, length, callback, on_done) {
+                _start_transition(this, type, length, callback, on_done);
+            },
+            long_transition: function(type, length, callback, on_done) {
+                _long_transition(this, type, length, callback, on_done);
+            },
+
+            /* Input */
+            touchmode: false,
+
+            /* Mode */
+            change_mode: function(newmode) {
+                _change_mode(game, newmode);
+            }
+        };
+
+        window.requestAnimFrame = (function() {
+            return window.requestAnimationFrame      ||
+                window.webkitRequestAnimationFrame   ||
+                window.mozRequestAnimationFrame      ||
+                window.oRequestAnimationFrame        ||
+                window.msRequestAnimationFrame       ||
+                function(callback, element) {
+                    window.setTimeout(callback, 1000/game.frame_rate);
+                };
+        })();
+
+        /* Register event listeners */
+        for (let ev in params.events) {
+            /* Register any other events I guess */
+            canvas['on' + ev] = function(e) {
+                if (game.playing && !game._norun) {
+                    params.events[ev](game, e);
+                }
+            }
+        }
+
+        /* Override with special events */
+        canvas.onmousedown = function(e) {
+            if (!game._norun) {
+                _handle_mousedown(game, e);
+            }
+        }
+
+        canvas.onmousemove = function(e) {
+            if (!game._norun) {
+                _handle_mousemove(game, e);
+            }
+        }
+
+        canvas.onmouseup = function(e) {
+            if (!game._norun) {
+                _handle_mouseup(game, e);
+            }
+        }
+
+        canvas.ontouchstart = function(e) {
+            if (!game._norun) {
+                game.touchmode = true;
+                _handle_touchstart(game, e);
+            }
+            e.preventDefault();
+        }
+
+        canvas.ontouchmove = function(e) {
+            if (!game._norun) {
+                game.touchmode = true;
+                _handle_touchmove(game, e);
+            }
+            e.preventDefault();
+        }
+
+        canvas.ontouchend = function(e) {
+            if (!game._norun) {
+                game.touchmode = true;
+                _handle_touchend(game, e);
+            }
+            e.preventDefault();
+        }
+
+        canvas.onkeydown = function(e) {
+            if (!game._norun && game.events.keydown) {
+                game.events.keydown(game, e);
+                e.preventDefault();
+            }
+        }
+
+        canvas.onkeyup = function(e) {
+            if (!game._norun && game.events.keyup) {
+                game.events.keyup(game, e);
+                e.preventDefault();
+            }
+        }
+
+        canvas.onblur = function(e) {
+            if (game.playing && !game.run_in_background) {
+                game._norun = true;
+                _stop_music(game);
+            }
+        }
+
+        canvas.onfocus = function(e) {
+            if (game.playing && !game.run_in_background) {
+                game._norun = false;
+                if (!game.muted) {
+                    _start_music(game);
+                }
+                _loop(game);
+            }
+        }
+
+        /* Set loading stuff */
+        let loading_img = new Image();
+        loading_img.onload = _register_resource('loading.png', game, function() {
+            if (!game.ready_to_go) {
+                game.ctx.global.save();
+                game.ctx.global.scale(game.draw_scale, game.draw_scale);
+                game.ctx.global.drawImage(loading_img, 0, 0);
+                game.ctx.global.restore();
+            }
+        });
+        loading_img.src = 'loading.png';
+
+        if (game.load_with_progress_bar) {
+            game._progressbar_img_loaded = false;
+            game._progressbar_width = 0;
+            game._progressbar_height = 0;
+
+            let progressbar_img = new Image();
+            progressbar_img.onload = _register_resource('progressbar.png', game, function() {
+                game._progressbar_img_loaded = true;
+                game._progressbar_width = progressbar_img.width;
+                game._progressbar_height = progressbar_img.height;
+                if (!game.ready_to_go) {
+                    game.ctx.global.save();
+                    game.ctx.global.scale(game.draw_scale, game.draw_scale);
+                    game.ctx.global.drawImage(loading_img, 0, 0);
+                    game.ctx.global.restore();
+                }
+            });
+            progressbar_img.src = 'progressbar.png';
+            game.img._progressbar = progressbar_img;
+        }
+
+        if (!game.run_in_background) {
+            let pause_img = new Image();
+            pause_img.onload = _register_resource('pause.png', game);
+            pause_img.src = 'pause.png';
+            game.img._pause = pause_img;
+        }
+
+        if (game.hasOwnProperty('save_key')) {
+            let saveerror_img = new Image();
+            saveerror_img.onload = _register_resource('saveerror.png', game);
+            saveerror_img.src = 'saveerror.png';
+            game.img._saveerror = saveerror_img;
+        }
+
+        let clicktostart_img = new Image();
+        clicktostart_img.onload = _register_resource('clicktostart.png', game, function() {
+            if (game.ready_to_go) {
+                game.ctx.global.save();
+                game.ctx.global.scale(game.draw_scale, game.draw_scale);
+                game.ctx.global.drawImage(game.img._clicktostart, 0, 0);
+                game.ctx.global.restore();
+            }
+        });
+        clicktostart_img.src = 'clicktostart.png';
+        game.img._clicktostart = clicktostart_img;
+
+        return game;
+    }
+
+    /* ---- Audio ---- */
+
+    function _stop_music(game) {
+        if (game.muted) return;
+
+        for (let m in game.music) {
+            if (!game.music[m].paused) {
+                game.music[m].was_playing = true;
+                game.music[m].pause();
+            } else {
+                game.music[m].was_playing = false;
+            }
+        }
+    }
+
+    function _start_music(game, unmuting) {
+        if (game.muted && !unmuting) return;
+
+        for (let m in game.music) {
+            if (game.music[m].was_playing) {
+                game.music[m].play();
+            }
+        }
+    }
+
+    function _mute(game) {
+        _stop_music(game);
+        game.muted = true;
+    }
+
+    function _unmute(game) {
+        game.muted = false;
+        _start_music(game);
+    }
+
+    /* ---- Game update stuff ---- */
+
+    let _last_frame_time;
+    let _timedelta = 0;
+    function _loop(game, timestamp) {
+        if (timestamp == undefined) {
+            timestamp = 0;
+            _last_frame_time = timestamp;
+        }
+        _timedelta += timestamp - _last_frame_time;
+        _last_frame_time = timestamp;
+
+        while (_timedelta >= 1000 / game.frame_rate) {
+            _update(game, 1000 / game.frame_rate);
+            _timedelta -= 1000 / game.frame_rate;
+        }
+        _draw(game);
+
+        if (!game._norun) {
+            requestAnimFrame(function(timestamp) {
+                _loop(game, timestamp);
+            });
+        }
+    }
+
+    function _update(game, delta) {
+        if (game.update_func) {
+            game.update_func(delta);
+        }
+
+        if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].update) {
+            game.modes[game.mode].update(delta);
+        }
+
+        if (game.transition.is_transitioning) {
+            game.transition.timer += delta;
+            if (game.transition.timer > game.transition.end_time) {
+                _finish_transition(game);
+            }
+        }
+    }
+
+    function _change_mode(game, newmode) {
+        game.mode = newmode;
+    }
+
+    /* ---- UI ---- */
+
+    function create_buttons(button_data) {
+        for (let b of button_data.buttons) {
+            b.state = 0;
+        }
+
+        return button_data;
+    }
+
+    /* Call with a button set and given x / y mouse coordinates.
+     * Will update the button set in place.
+     * Should generally be called in mouse move and mouse up event handlers. */
+    function update_buttons(button_data, x, y) {
+        for (let button of button_data.buttons) {
+            if (button.state === 2) continue;
+            if (button.disabled) continue;
+
+            if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) {
+                if (!game.touchmode) {
+                    button.state = 1;
+                }
+            } else if (button.state !== 2) {
+                button.state = 0;
+            }
+        }
+    }
+
+    /* Call with a button set and given x / y mouse coordinates.
+     * If a button is clicked, triggers its callback and returns true.
+     * Otherwise, returns false.
+     * Should be called in mouse down event handler. */
+    function click_button(button_data, x, y) {
+        for (let button of button_data.buttons) {
+            if (button.disabled) continue;
+
+            if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) {
+                button.state = 2;
+                game._clicked_button = button;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    function _handle_clicked_button(game) {
+        if (game._clicked_button) {
+            game._clicked_button.state = 0;
+            _draw(game);
+            if (game._clicked_button.callback) {
+                game._clicked_button.callback();
+            }
+            game._clicked_button = null;
+            return;
+        }
+    }
+
+    /* ---- Mouse ---- */
+
+    function _handle_mousedown(game, e) {
+        if (!game.playing) return;
+
+        game.touchmode = false;
+
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+        let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+        if (e.button === 0 && game.events.mousedown) {
+            game.events.mousedown(game, e, x, y);
+        }
+    }
+
+    function _handle_mouseup(game, e) {
+        if (!game.playing && game.ready_to_go) {
+            /* Click to start */
+            if (game.hasOwnProperty('save_key') && !game._show_save_error) {
+                /* Test if save/load is working */
+                try {
+                    console.log("Test saving");
+                    localStorage.setItem(game.save_key + ".test", "savetest");
+                    let savetest = localStorage.getItem(game.save_key + ".test");
+                    if (savetest !== "savetest") {
+                        throw new Error("oops");
+                    }
+                } catch (e) {
+                    /* If error saving, show save error message first */
+                    /* We set _show_save_error so that the second time the user clicks,
+                     * the game will actually start */
+                    console.log("- SAVE FAILED -");
+                    game._show_save_error = true;
+                    game.ctx.global.save();
+                    game.ctx.global.scale(game.draw_scale, game.draw_scale);
+                    game.ctx.global.drawImage(game.img._saveerror, 0, 0);
+                    game.ctx.global.restore();
+                    return;
+                }
+            }
+            game.play();
+            return;
+        }
+
+        game.touchmode = false;
+
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+        let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+
+        _handle_clicked_button(game);
+
+        if (e.button === 0 && game.events.mouseup) {
+            game.events.mouseup(game, e, x, y);
+        }
+    }
+
+    function _handle_mousemove(game, e) {
+        game.touchmode = false;
+
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+        let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+        if (game.events.mousemove) {
+            game.events.mousemove(game, e, x, y);
+        }
+    }
+
+    /* ---- Touch (defaults to same as mouse) ---- */
+
+    let _last_touch_event;
+
+    function _handle_touchstart(game, e) {
+        if (!game.playing) return;
+
+        game.touchmode = true;
+
+        if (e.touches) e = e.touches[0];
+
+        _last_touch_event = e;
+
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+        let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+        if (game.events.touchstart) {
+            game.events.touchstart(game, e, x, y);
+        } else if (game.events.mousedown) {
+            if (game.events.mousemove) {
+                game.events.mousemove(game, e, x, y);
+            }
+            game.events.mousedown(game, e, x, y);
+        }
+    }
+
+    function _handle_touchend(game, e) {
+        if (!game.playing && game.ready_to_go) {
+            /* Click to start */
+            game.play();
+            return;
+        }
+
+        game.touchmode = true;
+
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((_last_touch_event.clientX - rect.left) / rect.width * game.screen_w) - 1;
+        let y = Math.round((_last_touch_event.clientY - rect.top) / rect.height * game.screen_h) - 1;
+
+        _handle_clicked_button(game);
+
+        if (game.events.touchend) {
+            game.events.touchend(game, e, x, y);
+        } else if (game.events.mouseup) {
+            if (game.events.mousemove) {
+                game.events.mousemove(game, e, x, y);
+            }
+            game.events.mouseup(game, e, x, y);
+        }
+    }
+
+    function _handle_touchmove(game, e) {
+        if (!game.playing) return;
+
+        game.touchmode = true;
+
+        if (e.touches) e = e.touches[0];
+
+        _last_touch_event = e;
+
+        const rect = game.canvas.getBoundingClientRect();
+        let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+        let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+        if (game.events.touchmove) {
+            game.events.touchmove(game, e, x, y);
+        } else if (game.events.mousemove) {
+            game.events.mousemove(game, e, x, y);
+        }
+    }
+
+    /* ---- Drawing stuff ---- */
+
+    function _draw(game) {
+        game.ctx.draw.save();
+
+        game.ctx.draw.fillStyle = game.background_color;
+
+        game.ctx.draw.fillRect(0, 0, game.screen_w, game.screen_h);
+
+        if (game.draw_func) {
+            game.draw_func(game.ctx.draw);
+        }
+
+        if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw) {
+            game.modes[game.mode].draw(game.ctx.draw);
+        }
+
+        if (game.transition.mid_long) {
+            game.ctx.draw.fillStyle = game.transition.color;
+            game.ctx.draw.fillRect(-1, -1, game.canvas_w + 5, game.canvas_h + 5);
+        }
+
+        game.ctx.draw.restore();
+
+        if (game.transition.is_transitioning) {
+            _draw_transition(game, game.ctx.mask);
+        } else {
+            game.ctx.mask.drawImage(game.ctx.draw.canvas, 0, 0);
+        }
+
+        if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw_after_transition) {
+            game.modes[game.mode].draw_after_transition(game.ctx.mask);
+        }
+
+
+        game.ctx.global.fillStyle = 'rgb(0, 0, 0)';
+        game.ctx.global.fillRect(0, 0, game.screen_w * game.draw_scale, game.screen_h * game.draw_scale);
+
+        game.ctx.global.save();
+
+        game.ctx.global.scale(game.draw_scale, game.draw_scale);
+
+        game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0);
+
+        if (game._norun) {
+            game.ctx.global.drawImage(game.img._pause, 0, 0);
+        }
+
+        game.ctx.global.restore();
+    }
+
+    /* Draw a particular section of an image/spritesheet,
+     * without having to do as much math or type the destination size */
+    function sprite_draw(ctx, img, section_w, section_h, section_x, section_y, dest_x, dest_y) {
+        ctx.drawImage(img,
+            section_w * section_x, section_h * section_y, section_w, section_h,
+            Math.round(dest_x), Math.round(dest_y), section_w, section_h)
+    }
+
+    /* Draw an image over the whole screen lol */
+    function screen_draw(ctx, img) {
+        ctx.drawImage(img, 0, 0);
+    }
+
+    /* Draw a set of buttons. */
+    function button_draw(ctx, button_data) {
+        for (let button of button_data.buttons) {
+            if (!button.state) button.state = 0;
+            let button_state = button.state;
+            if (button.disabled) button_state = 3;
+            sprite_draw(ctx, button_data.img, button_data.button_w, button_data.button_h, button.id, button_state, button.x, button.y);
+        }
+    }
+
+    /* ---- Save ---- */
+
+    function _save(game, key, data) {
+        let save_data;
+
+        if (!game.hasOwnProperty('save_key')) {
+            throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object");
+        }
+
+        try {
+            save_data = localStorage.getItem(game.save_key) || "{}";
+        } catch (e) {
+            console.error("Saving is disabled; cannot save " + key, e);
+            return;
+        }
+
+        try {
+            save_data = JSON.parse(save_data);
+        } catch (e) {
+            console.error("Cannot parse stored save data (when trying to save " + key + ")", e);
+            return;
+        }
+
+        save_data[key] = JSON.stringify(data);
+
+        try {
+            save_data = JSON.stringify(save_data);
+        } catch (e) {
+            console.error("Failed to stringify save data (while trying to save " + key + ")", e);
+            return;
+        }
+
+        try {
+            localStorage.setItem(game.save_key, save_data);
+        } catch (e) {
+            console.error("Failed to save to localStorage (while saving key " + key + ")", e);
+            return;
+        }
+    }
+
+    function _load(game, key) {
+        let save_data;
+
+        if (!game.hasOwnProperty('save_key')) {
+            throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object");
+        }
+
+        try {
+            save_data = localStorage.getItem(game.save_key);
+        } catch (e) {
+            console.error("Loading is disabled; cannot load " + key, e);
+            return;
+        }
+
+        try {
+            save_data = JSON.parse(save_data);
+        } catch (e) {
+            console.error("Cannot parse stored save data (when trying to load " + key + ")", e);
+            return;
+        }
+
+        if (!save_data) {
+            return null;
+        }
+
+        return save_data[key];
+    }
+
+    /* ---- Transition stuff ---- */
+
+    let _transition = {
+        is_transitioning: false,
+        timer: 0,
+        color: '#000000',
+        w: 20,
+        h: 14,
+        dir_invert_v: false,
+        dir_invert_h: false,
+        invert_shape: true,
+        mid_long: false,
+        done_func: null,
+        type: _draw_fade_transition,
+        nodraw: false,
+        end_time: 100,
+    }
+
+    function _long_transition(game, type, length, callback) {
+        if (game.transition.is_transitioning) return;
+
+        _draw(game);
+
+        game.transition.invert_shape = false;
+        _internal_start_transition(game, type, length, function() {
+            game.transition.mid_long = true;
+        }, function() {
+            game.transition.invert_shape = true;
+            game.transition.is_transitioning = true;
+            let tdiv = game.transition.dir_invert_v;
+            let tdih = game.transition.dir_invert_h;
+            _internal_start_transition(game, type, length, function() {
+                game.transition.mid_long = false;
+                callback();
+                game.transition.dir_invert_v = tdiv;
+                game.transition.dir_invert_h = tdih;
+            });
+        });
+    }
+
+    function _start_transition(game, type, length, callback, on_done) {
+        if (game.transition.is_transitioning) return;
+        if (!game.transition.nodraw) _draw(game);
+
+        _internal_start_transition(game, type, length, callback, on_done);
+    }
+
+    function _internal_start_transition(game, type, length, callback, on_done) {
+        if (on_done) {
+            game.transition.done_func = on_done;
+        }
+
+        game.transition.timer = 0;
+        game.transition.type = type;
+        game.transition.end_time = length;
+
+        game.ctx.copy.drawImage(game.ctx.draw.canvas, 0, 0);
+
+        callback();
+
+        game.transition.is_transitioning = true;
+    }
+
+    function _finish_transition(game) {
+        game.transition.is_transitioning = false;
+        game.transition.timer = 0;
+
+        if (game.transition.done_func) {
+            setTimeout(function() {
+                game.transition.done_func();
+                game.transition.done_func = null;
+            }, 400);
+        }
+    }
+
+    function _draw_transition(game, ctx) {
+        let frac = game.transition.timer / game.transition.end_time;
+
+        ctx.save();
+        ctx.clearRect(0, 0, game.screen_w, game.screen_h);
+        game.transition.type(game, ctx, game.ctx.copy, game.ctx.draw, frac, game.transition.invert_shape);
+        ctx.restore();
+    }
+
+    function _draw_slide_down_transition(game, ctx, oldc, newc, frac, reverse) {
+        let offset = frac * game.screen_h;
+
+        ctx.drawImage(oldc.canvas, 0, -offset);
+        ctx.drawImage(newc.canvas, 0, game.screen_h - offset);
+    }
+
+    function _draw_slide_up_transition(game, ctx, oldc, newc, frac, reverse) {
+        let offset = frac * game.screen_h;
+
+        ctx.drawImage(oldc.canvas, 0, offset);
+        ctx.drawImage(newc.canvas, 0, - game.screen_h + offset);
+    }
+
+    function _draw_fade_transition(game, ctx, oldc, newc, frac, reverse) {
+        ctx.globalAlpha = 1;
+        ctx.drawImage(oldc.canvas, 0, 0);
+        ctx.globalAlpha = frac;
+        ctx.drawImage(newc.canvas, 0, 0);
+    }
+
+    /* ---- former contents of ready.js ---- */
+
+    // Due to Timo Huovinen
+    // https://stackoverflow.com/questions/799981/document-ready-equivalent-without-jquery
+    var ready = (function(){
+
+        var readyList,
+            DOMContentLoaded,
+            class2type = {};
+            class2type["[object Boolean]"] = "boolean";
+            class2type["[object Number]"] = "number";
+            class2type["[object String]"] = "string";
+            class2type["[object Function]"] = "function";
+            class2type["[object Array]"] = "array";
+            class2type["[object Date]"] = "date";
+            class2type["[object RegExp]"] = "regexp";
+            class2type["[object Object]"] = "object";
+
+        var ReadyObj = {
+            // Is the DOM ready to be used? Set to true once it occurs.
+            isReady: false,
+            // A counter to track how many items to wait for before
+            // the ready event fires. See #6781
+            readyWait: 1,
+            // Hold (or release) the ready event
+            holdReady: function( hold ) {
+                if ( hold ) {
+                    ReadyObj.readyWait++;
+                } else {
+                    ReadyObj.ready( true );
+                }
+            },
+            // Handle when the DOM is ready
+            ready: function( wait ) {
+                // Either a released hold or an DOMready/load event and not yet ready
+                if ( (wait === true && !--ReadyObj.readyWait) || (wait !== true && !ReadyObj.isReady) ) {
+                    // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+                    if ( !document.body ) {
+                        return setTimeout( ReadyObj.ready, 1 );
+                    }
+
+                    // Remember that the DOM is ready
+                    ReadyObj.isReady = true;
+                    // If a normal DOM Ready event fired, decrement, and wait if need be
+                    if ( wait !== true && --ReadyObj.readyWait > 0 ) {
+                        return;
+                    }
+                    // If there are functions bound, to execute
+                    readyList.resolveWith( document, [ ReadyObj ] );
+
+                    // Trigger any bound ready events
+                    //if ( ReadyObj.fn.trigger ) {
+                    //    ReadyObj( document ).trigger( "ready" ).unbind( "ready" );
+                    //}
+                }
+            },
+            bindReady: function() {
+                if ( readyList ) {
+                    return;
+                }
+                readyList = ReadyObj._Deferred();
+
+                // Catch cases where $(document).ready() is called after the
+                // browser event has already occurred.
+                if ( document.readyState === "complete" ) {
+                    // Handle it asynchronously to allow scripts the opportunity to delay ready
+                    return setTimeout( ReadyObj.ready, 1 );
+                }
+
+                // Mozilla, Opera and webkit nightlies currently support this event
+                if ( document.addEventListener ) {
+                    // Use the handy event callback
+                    document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+                    // A fallback to window.onload, that will always work
+                    window.addEventListener( "load", ReadyObj.ready, false );
+
+                // If IE event model is used
+                } else if ( document.attachEvent ) {
+                    // ensure firing before onload,
+                    // maybe late but safe also for iframes
+                    document.attachEvent( "onreadystatechange", DOMContentLoaded );
+
+                    // A fallback to window.onload, that will always work
+                    window.attachEvent( "onload", ReadyObj.ready );
+
+                    // If IE and not a frame
+                    // continually check to see if the document is ready
+                    var toplevel = false;
+
+                    try {
+                        toplevel = window.frameElement == null;
+                    } catch(e) {}
+
+                    if ( document.documentElement.doScroll && toplevel ) {
+                        doScrollCheck();
+                    }
+                }
+            },
+            _Deferred: function() {
+                var // callbacks list
+                    callbacks = [],
+                    // stored [ context , args ]
+                    fired,
+                    // to avoid firing when already doing so
+                    firing,
+                    // flag to know if the deferred has been cancelled
+                    cancelled,
+                    // the deferred itself
+                    deferred  = {
+
+                        // done( f1, f2, ...)
+                        done: function() {
+                            if ( !cancelled ) {
+                                var args = arguments,
+                                    i,
+                                    length,
+                                    elem,
+                                    type,
+                                    _fired;
+                                if ( fired ) {
+                                    _fired = fired;
+                                    fired = 0;
+                                }
+                                for ( i = 0, length = args.length; i < length; i++ ) {
+                                    elem = args[ i ];
+                                    type = ReadyObj.type( elem );
+                                    if ( type === "array" ) {
+                                        deferred.done.apply( deferred, elem );
+                                    } else if ( type === "function" ) {
+                                        callbacks.push( elem );
+                                    }
+                                }
+                                if ( _fired ) {
+                                    deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
+                                }
+                            }
+                            return this;
+                        },
+
+                        // resolve with given context and args
+                        resolveWith: function( context, args ) {
+                            if ( !cancelled && !fired && !firing ) {
+                                // make sure args are available (#8421)
+                                args = args || [];
+                                firing = 1;
+                                try {
+                                    while( callbacks[ 0 ] ) {
+                                        callbacks.shift().apply( context, args );//shifts a callback, and applies it to document
+                                    }
+                                }
+                                finally {
+                                    fired = [ context, args ];
+                                    firing = 0;
+                                }
+                            }
+                            return this;
+                        },
+
+                        // resolve with this as context and given arguments
+                        resolve: function() {
+                            deferred.resolveWith( this, arguments );
+                            return this;
+                        },
+
+                        // Has this deferred been resolved?
+                        isResolved: function() {
+                            return !!( firing || fired );
+                        },
+
+                        // Cancel
+                        cancel: function() {
+                            cancelled = 1;
+                            callbacks = [];
+                            return this;
+                        }
+                    };
+
+                return deferred;
+            },
+            type: function( obj ) {
+                return obj == null ?
+                    String( obj ) :
+                    class2type[ Object.prototype.toString.call(obj) ] || "object";
+            }
+        }
+        // The DOM ready check for Internet Explorer
+        function doScrollCheck() {
+            if ( ReadyObj.isReady ) {
+                return;
+            }
+
+            try {
+                // If IE is used, use the trick by Diego Perini
+                // http://javascript.nwbox.com/IEContentLoaded/
+                document.documentElement.doScroll("left");
+            } catch(e) {
+                setTimeout( doScrollCheck, 1 );
+                return;
+            }
+
+            // and execute any waiting functions
+            ReadyObj.ready();
+        }
+        // Cleanup functions for the document ready method
+        if ( document.addEventListener ) {
+            DOMContentLoaded = function() {
+                document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+                ReadyObj.ready();
+            };
+
+        } else if ( document.attachEvent ) {
+            DOMContentLoaded = function() {
+                // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+                if ( document.readyState === "complete" ) {
+                    document.detachEvent( "onreadystatechange", DOMContentLoaded );
+                    ReadyObj.ready();
+                }
+            };
+        }
+        function ready( fn ) {
+            // Attach the listeners
+            ReadyObj.bindReady();
+
+            var type = ReadyObj.type( fn );
+
+            // Add the callback
+            readyList.done( fn );//readyList is result of _Deferred()
+        }
+        return ready;
+    })();
+
+    /* ---- direction objects ---- */
+    const _dir_up = { x: 0, y: -1, index: 0, rotate: 0, horizontal: false, vertical: true };
+    const _dir_right = { x: 1, y: 0, index: 1, rotate: Math.PI/2, horizontal: true, vertical: false };
+    const _dir_down = { x: 0, y: 1, index: 2, rotate: Math.PI, horizontal: false, vertical: true };
+    const _dir_left = { x: -1, y: 0, index: 3, rotate: 3*Math.PI/2, horizontal: true, vertical: false };
+    _dir_up.opposite = _dir_down;
+    _dir_down.opposite = _dir_up;
+    _dir_right.opposite = _dir_left;
+    _dir_left.opposite = _dir_right;
+
+    return {
+        create_game: create_game,
+
+        ready: ready,
+        mod: mod,
+        sgn: sgn,
+        as_hex: as_hex,
+        copy_list: copy_list,
+        copy_flat_objlist: copy_flat_objlist,
+
+        sprite_draw: sprite_draw,
+        screen_draw: screen_draw,
+
+        buttons: {
+            create: create_buttons,
+            update: update_buttons,
+            click: click_button,
+            draw: button_draw,
+        },
+
+        dir: {
+            up: _dir_up,
+            down: _dir_down,
+            left: _dir_left,
+            right: _dir_right,
+        },
+
+        phi: (1 + Math.sqrt(5)) / 2,
+
+        transition: {
+            type: {
+                fade: _draw_fade_transition,
+                slide_up: _draw_slide_up_transition,
+                slide_down: _draw_slide_down_transition,
+            },
+        },
+    }
+})();