From: cassowarii <2374677+cassowarii@users.noreply.github.com> Date: Sun, 5 Oct 2025 22:15:03 +0000 (-0700) Subject: Initial commit X-Git-Url: https://www.git.cassowary.me/gitweb.cgi?a=commitdiff_plain;h=e62900b3bf2372450405d1b1650bbde0be0abb9b;p=ld58.git Initial commit --- e62900b3bf2372450405d1b1650bbde0be0abb9b diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd75d88 --- /dev/null +++ b/.gitignore @@ -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 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 index 0000000..b42c06e Binary files /dev/null and b/clicktostart.png differ diff --git a/game.js b/game.js new file mode 100644 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 index 0000000..fc31109 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + alien postage stamp game + + + + + + + + + + + diff --git a/levels.js b/levels.js new file mode 100644 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 index 0000000..3b5efde --- /dev/null +++ b/levels/1.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/10.tmx b/levels/10.tmx new file mode 100644 index 0000000..bfa0ef6 --- /dev/null +++ b/levels/10.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/11.tmx b/levels/11.tmx new file mode 100644 index 0000000..a9a4c6f --- /dev/null +++ b/levels/11.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/12.tmx b/levels/12.tmx new file mode 100644 index 0000000..1d6a445 --- /dev/null +++ b/levels/12.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/2.tmx b/levels/2.tmx new file mode 100644 index 0000000..39789d2 --- /dev/null +++ b/levels/2.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/3.tmx b/levels/3.tmx new file mode 100644 index 0000000..7f04def --- /dev/null +++ b/levels/3.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/4.tmx b/levels/4.tmx new file mode 100644 index 0000000..5c5a078 --- /dev/null +++ b/levels/4.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/5.tmx b/levels/5.tmx new file mode 100644 index 0000000..e1aed5a --- /dev/null +++ b/levels/5.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/6.tmx b/levels/6.tmx new file mode 100644 index 0000000..2777bd8 --- /dev/null +++ b/levels/6.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/7.tmx b/levels/7.tmx new file mode 100644 index 0000000..44b317f --- /dev/null +++ b/levels/7.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/8.tmx b/levels/8.tmx new file mode 100644 index 0000000..c1b6e45 --- /dev/null +++ b/levels/8.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/9.tmx b/levels/9.tmx new file mode 100644 index 0000000..4a970dc --- /dev/null +++ b/levels/9.tmx @@ -0,0 +1,19 @@ + + + + + +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 + + + diff --git a/levels/mapcompile.py b/levels/mapcompile.py new file mode 100755 index 0000000..3047320 --- /dev/null +++ b/levels/mapcompile.py @@ -0,0 +1,79 @@ +#!/usr/bin/python3 + +import ulvl +import sys + +if len(sys.argv) < 2: + print("usage:", sys.argv[0], "") + 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 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 index 0000000..2c39c67 --- /dev/null +++ b/levels/tmptile.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/loading.png b/loading.png new file mode 100644 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 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 index 0000000..7479e89 Binary files /dev/null and b/pause.png differ diff --git a/progressbar.png b/progressbar.png new file mode 100644 index 0000000..24a6aba Binary files /dev/null and b/progressbar.png differ diff --git a/saveerror.png b/saveerror.png new file mode 100644 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 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 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 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 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 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 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 index 0000000..df44235 --- /dev/null +++ b/zucchinibread.js @@ -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, + }, + }, + } +})();