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