From: cassowarii Date: Mon, 7 Apr 2025 03:45:00 +0000 (-0700) Subject: Initial commit X-Git-Url: https://git.cassowary.me/gitweb.cgi?a=commitdiff_plain;h=a3c4838ff870e2466eafaa00e03fc4abe5617ad6;p=ld57.git Initial commit --- a3c4838ff870e2466eafaa00e03fc4abe5617ad6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37dbf1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.xcf +*.swp +*.swo diff --git a/ase/bubble.aseprite b/ase/bubble.aseprite new file mode 100644 index 0000000..fe81ac3 Binary files /dev/null and b/ase/bubble.aseprite differ diff --git a/ase/buttons.aseprite b/ase/buttons.aseprite new file mode 100644 index 0000000..6b00531 Binary files /dev/null and b/ase/buttons.aseprite differ diff --git a/ase/decoration.aseprite b/ase/decoration.aseprite new file mode 100644 index 0000000..70bcbad Binary files /dev/null and b/ase/decoration.aseprite differ diff --git a/ase/failure.aseprite b/ase/failure.aseprite new file mode 100644 index 0000000..b3388f6 Binary files /dev/null and b/ase/failure.aseprite differ diff --git a/ase/fish.aseprite b/ase/fish.aseprite new file mode 100644 index 0000000..ddc99d2 Binary files /dev/null and b/ase/fish.aseprite differ diff --git a/ase/fish.gif b/ase/fish.gif new file mode 100644 index 0000000..3572850 Binary files /dev/null and b/ase/fish.gif differ diff --git a/ase/heart.aseprite b/ase/heart.aseprite new file mode 100644 index 0000000..795366e Binary files /dev/null and b/ase/heart.aseprite differ diff --git a/ase/level1.aseprite b/ase/level1.aseprite new file mode 100644 index 0000000..7eca6e6 Binary files /dev/null and b/ase/level1.aseprite differ diff --git a/ase/level2.aseprite b/ase/level2.aseprite new file mode 100644 index 0000000..2983b55 Binary files /dev/null and b/ase/level2.aseprite differ diff --git a/ase/level4.aseprite b/ase/level4.aseprite new file mode 100644 index 0000000..b012708 Binary files /dev/null and b/ase/level4.aseprite differ diff --git a/ase/levelnums.aseprite b/ase/levelnums.aseprite new file mode 100644 index 0000000..8fe4ab8 Binary files /dev/null and b/ase/levelnums.aseprite differ diff --git a/ase/menubuttons.aseprite b/ase/menubuttons.aseprite new file mode 100644 index 0000000..5d91746 Binary files /dev/null and b/ase/menubuttons.aseprite differ diff --git a/ase/menuimage.aseprite b/ase/menuimage.aseprite new file mode 100644 index 0000000..3e75531 Binary files /dev/null and b/ase/menuimage.aseprite differ diff --git a/ase/move_tile.aseprite b/ase/move_tile.aseprite new file mode 100644 index 0000000..cf9e289 Binary files /dev/null and b/ase/move_tile.aseprite differ diff --git a/ase/palette.aseprite b/ase/palette.aseprite new file mode 100644 index 0000000..73483ec Binary files /dev/null and b/ase/palette.aseprite differ diff --git a/ase/selector.aseprite b/ase/selector.aseprite new file mode 100644 index 0000000..f0b30f6 Binary files /dev/null and b/ase/selector.aseprite differ diff --git a/ase/success.aseprite b/ase/success.aseprite new file mode 100644 index 0000000..d4f42c7 Binary files /dev/null and b/ase/success.aseprite differ diff --git a/ase/wall_tile.aseprite b/ase/wall_tile.aseprite new file mode 100644 index 0000000..91e6e4b Binary files /dev/null and b/ase/wall_tile.aseprite differ diff --git a/ase/x.aseprite b/ase/x.aseprite new file mode 100644 index 0000000..d643e7f Binary files /dev/null and b/ase/x.aseprite differ diff --git a/ase/youwon.aseprite b/ase/youwon.aseprite new file mode 100644 index 0000000..1973508 Binary files /dev/null and b/ase/youwon.aseprite differ diff --git a/clicktostart.png b/clicktostart.png new file mode 100644 index 0000000..9454614 Binary files /dev/null and b/clicktostart.png differ diff --git a/cover.png b/cover.png new file mode 100644 index 0000000..4ebdd5e Binary files /dev/null and b/cover.png differ diff --git a/game.js b/game.js new file mode 100644 index 0000000..dfa9f2b --- /dev/null +++ b/game.js @@ -0,0 +1,1514 @@ +"use strict"; + +let game; + +let game_started = false; + +let just_started = true; + +let level_number = 1; + +let map; + +let buttons = {}; + +let max_unlocked_level; +let continue_level; + +let how_to_play = false; + +let disable_transitions = false; +let disable_colors = false; + +let won_everything = false; + +let KINDS_OF_FISH = 5; + +/* --------- definitions ---------- */ + +/* state: + * STAND: waiting for input + * BLOCK: moving block + * DRAG: actually dragging block + * FISH: moving fish or shark + * CONGREGATE: swim together to win + * WIN: level complete + * LOSESWIM: shark swimming to eat us + * LOSE: got eaten by shark + */ +let State = { STAND: 0, BLOCK: 1, DRAG: 2, FISH: 3, SWIM: 4, CONGREGATE: 5, WIN: 6, LOSESWIM: 7, LOSE: 8 }; + +let UIState = { MENU: 0, LEVELSELECT: 1, OPTIONS: 2, GAME: 3, REALLYQUIT: 4, REALLYDELETE: 5 }; + +let can_continue = false; + +let save_data = 1; +const SAVE_KEY = "casso.continentalshelf.save" + +zb.ready(function() { + game = zb.create_game({ + canvas: 'canvas', + canvas_w: 576, + canvas_h: 864, + draw_scale: 3, + tile_size: 24, + level_w: 8, + level_h: 12, + background_color: '#192c4a', + run_in_background: true, + load_with_progress_bar: false, + save_key: SAVE_KEY, + state: State.STAND, + uistate: UIState.MENU, + events: { + keyup: handle_keyup, + mouseup: handle_mouseup, + mouseleave: handle_mouseleave, + mousedown: handle_mousedown, + mousemove: handle_mousemove, + gamestart: handle_gamestart, + }, + modes: { + menu: { + draw: do_draw_menu, + }, + youwon: { + draw: do_draw_youwon, + }, + game: { + draw: do_draw, + update: do_update, + }, + }, + mode: 'menu', + buttons: {}, + }); + + game.register_images({ + fish: 'img/fish.png', + selector: 'img/selector.png', + x: 'img/x.png', + heart: 'img/heart.png', + bubble: 'img/bubble.png', + move_tile: 'img/move_tile.png', + wall_tile: 'img/wall_tile.png', + decorations: 'img/decoration.png', + levelnums: 'img/levelnums.png', + success: 'img/success.png', + failure: 'img/failure.png', + buttons: 'img/buttons.png', + menubuttons: 'img/menubuttons.png', + menuimage: 'img/menuimage.png', + youwon: 'img/youwon.png', + levels: { + 1: 'img/level/1.png', + 2: 'img/level/2.png', + 4: 'img/level/4.png', + } + }); + + game.register_sfx({ + victory: { + path: 'sfx/victory.wav', + volume: 0.3, + }, + fail: { + path: 'sfx/fail.wav', + volume: 0.8, + }, + drip: { + path: 'sfx/drip.wav', + volume: 1, + }, + drag: { + path: 'sfx/drag.wav', + volume: 0.19, + }, + bubble: { + path: 'sfx/bubble.wav', + volume: 0.5, + }, + bubble2: { + path: 'sfx/bubble2.wav', + volume: 0.5, + }, + bubble3: { + path: 'sfx/bubble3.wav', + volume: 0.5, + }, + bubble4: { + path: 'sfx/bubble4.wav', + volume: 0.5, + }, + }); + + game.register_music({ + bgm: { + path: 'music/sharkgamebeachmusic', + volume: 0.1, + }, + }); + + game.resources_ready(); + + continue_level = parseInt(game.load("continue_level") || "1"); + + game.buttons.ingame = zb.buttons.create({ + img: game.img.buttons, + button_w: 24, + button_h: 24, + buttons: [ + { + /* Undo */ + x: game.screen_w - 72, + y: game.screen_h - 24, + id: 0, + callback: undo, + }, + { + /* Reset */ + x: game.screen_w - 48, + y: game.screen_h - 24, + id: 1, + callback: reset, + }, + { + /* Toggle mute */ + x: game.screen_w - 24, + y: game.screen_h - 24, + id: 2, + callback: toggle_mute_button, + }, + ], + }); + + game.buttons.menu = zb.buttons.create({ + img: game.img.menubuttons, + button_w: 72, + button_h: 16, + buttons: [ + { + /* New game */ + x: game.screen_w / 2 - 36, + y: game.screen_h / 2 + 36, + id: 0, + callback: new_game_button, + }, + { + /* Continue */ + x: game.screen_w / 2 - 36, + y: game.screen_h / 2 + 60, + id: 1, + callback: continue_button, + disabled: continue_level === 1, + }, + ], + }); +}); + +let tileID = { + blank: 0, + wall: 1, +}; + +let creatureID = { + fish: 0, + shark: 1, +}; + +let creature_type_mapping = { + 0: creatureID.fish, + 1: creatureID.fish, + 2: creatureID.fish, + 3: creatureID.fish, + 4: creatureID.fish, + 5: creatureID.shark, +}; + +/* ------ timers & static timer values --------- */ + +const STAND_FRAME_LENGTH = 500; +const SWIM_FRAME_LENGTH = 200; + +const SELECTOR_FRAME_LENGTH = 300; + +const BLOCK_MOVE_SPEED = 12; + +const SWIM_SPEED = 6; +const LOSE_SWIM_SPEED = 8; +const WIN_SWIM_SPEED = 5; + +const X_FRAME_LENGTH = 100; +const MAX_X_FRAME = 12; + +const BUBBLE_FRAME_LENGTH = 300; +const BUBBLE_UP_SPEED = 15; + +const SCREEN_MESSAGE_DELAY = 600; +const SCREEN_MESSAGE_FADEIN_LENGTH = 1000; + +const DRIFT_SPEED = 3000; + +/* ------- game global state -------- */ + +let mouse_x = -1, mouse_y = -1, mouse_is_down = false; + +let fish = []; + +let blocks = []; + +let hearts = []; + +let bubbles = []; + +let decorations = []; + +let hovered_block = null; + +let hovered_fish = null; + +let eaten_fish = null; + +let start_drag_x = 0, start_drag_y = 0; + +let selector_timer = 0; +let selector_frame = 0; + +let potential_fish_target = null; +let fish_path = null; + +let x_timer = 0; +let x_frame = 0; + +let just_clicked_block = true; + +let screen_message_delay = 0; +let screen_message_alpha = 0; + +/* ------- game behavior functions -------- */ + +function tile_at(x, y) { + if (x < 0 || x >= game.level_w) return tileID.wall; + if (y < 0 || y >= game.level_h) return tileID.wall; + + let result = map[y * game.level_w + x]; + return result; +} + +function fish_at(x, y) { + let filtered_fish = fish.filter(o => o.x === x && o.y === y); + if (filtered_fish.length > 0) { + return filtered_fish[0]; + } + return null; +} + +function block_at(x, y) { + for (let b of blocks) { + for (let t of b.tiles) { + if (t[0] === x && t[1] === y) { + return true; + } + } + } + return false; +} + +function tile_or_block_at(x, y) { + return tile_at(x, y) === tileID.wall || block_at(x, y); +} + +function anything_at(x, y, ignore_block) { + if (tile_at(x, y) === tileID.wall) { + return true; + } + + for (let f of fish) { + if (f.x === x && f.y === y) { + return true; + } + } + + for (let b of blocks) { + if (ignore_block && ignore_block === b) { + continue; + } + for (let t of b.tiles) { + if (t[0] === x && t[1] === y) { + return true; + } + } + } + + return false; +} + +function anything_except_fish_at(x, y, ignore_block) { + if (tile_at(x, y) === tileID.wall) { + return true; + } + + for (let f of fish) { + if (f.x === x && f.y === y && f.species !== creatureID.fish) { + return true; + } + } + + for (let b of blocks) { + if (ignore_block && ignore_block === b) { + continue; + } + for (let t of b.tiles) { + if (t[0] === x && t[1] === y) { + return true; + } + } + } + + return false; +} + +function sofc(coords) { + return "" + coords[0] + "," + coords[1]; +} + +function cofs(string) { + return string.split(",").map(a => parseInt(a, 10)); +} + +function path_between(start_x, start_y, end_x, end_y, end_can_be_blocked, ignore_fish) { + /* Pathfinding, for fish */ + + if (anything_at(end_x, end_y) && end_can_be_blocked) { + console.log("very no"); + return null; + } + + let queue = [[start_x, start_y, null]]; + let checked = {}; + + while (queue.length > 0) { + let next_pos = queue.shift(); + + if (checked.hasOwnProperty(sofc(next_pos))) { + continue; + } + + checked[sofc(next_pos)] = next_pos[2]; + + if (next_pos[0] === end_x && next_pos[1] === end_y) { + console.log("yes"); + let path = []; + while (next_pos !== null) { + path.unshift([next_pos[0], next_pos[1]]); + next_pos = checked[sofc(next_pos)]; + } + return path; + } + + if ((next_pos[0] !== start_x || next_pos[1] !== start_y) + && (ignore_fish || anything_at(next_pos[0], next_pos[1])) + && (!ignore_fish || anything_except_fish_at(next_pos[0], next_pos[1]))) { + continue; + } + + queue.push([next_pos[0] + 1, next_pos[1], [next_pos[0], next_pos[1]]]); + queue.push([next_pos[0] - 1, next_pos[1], [next_pos[0], next_pos[1]]]); + queue.push([next_pos[0], next_pos[1] + 1, [next_pos[0], next_pos[1]]]); + queue.push([next_pos[0], next_pos[1] - 1, [next_pos[0], next_pos[1]]]); + } + + console.log("no"); + return null; +} + +let undo_stack = []; + +function create_undo_point() { + console.log("cup"); + + let blocks_copy = []; + for (let b of blocks) { + let new_block = { + state: 0, + tiles: [], + target_dx: 0, + target_dy: 0, + move_fraction: 0, + }; + for (let coord of b.tiles) { + new_block.tiles.push( [ coord[0], coord[1] ] ); + } + blocks_copy.push(new_block); + } + + undo_stack.push({ + fish: zb.copy_flat_objlist(fish), + blocks: blocks_copy, + }); +} + +function make_normal_state() { + game.state = State.STAND; + x_frame = 0; + hearts = []; + bubbles = []; + potential_fish_target = null; + hovered_fish = null; + screen_message_alpha = 0; + screen_message_delay = SCREEN_MESSAGE_DELAY; +} + +function undo() { + console.log("undo") + if (undo_stack.length > 0) { + let undo_point = undo_stack.pop(); + fish = undo_point.fish; + blocks = undo_point.blocks; + make_normal_state(); + } +} + +function reset() { + console.log("reset"); + create_undo_point(); + game.start_transition(zb.transition.type.fade, 300, function() { + load_level(); + }); +} + +function advance_level() { + if (!can_continue) return; + + undo_stack = []; + + if (level_number + 1 > Math.max(...Object.keys(levels).map(a => parseInt(a, 10) || 0))) { + game.long_transition(zb.transition.type.fade, 1000, function() { + reset_save(); + game.mode = 'youwon'; + }); + } else { + game.start_transition(zb.transition.type.fade, 1000, function() { + level_number ++; + load_level(); + make_normal_state(); + }); + } +} + +function skip_to(num) { + level_number = num; + load_level(); +} + +function win_everything() { + console.log("WIN!!"); +} + +function load_level() { + load_level_data(levels[level_number]); +} + +function load_level_data(lvl) { + map = lvl.map; + + fish = []; + blocks = []; + + make_normal_state(); + + decorations = []; + + let fish_type = zb.mod(level_number || 0, KINDS_OF_FISH); + if (isNaN(level_number)) { + fish_type = 0; + } + for (let f of lvl.fish) { + fish.push({ + x: f[0], + y: f[1], + type: fish_type, + species: creatureID.fish, + frame: 0, + timer: Math.random() * STAND_FRAME_LENGTH, + drift: Math.random(), + }); + fish_type ++; + fish_type %= KINDS_OF_FISH; + } + + for (let s of lvl.sharks) { + fish.push({ + x: s[0], + y: s[1], + type: KINDS_OF_FISH, + species: creatureID.shark, + frame: 0, + timer: Math.random() * STAND_FRAME_LENGTH, + drift: Math.random(), + }); + } + + for (let b of lvl.blocks) { + let new_block = { + state: 0, + tiles: [], + target_dx: 0, + target_dy: 0, + move_fraction: 0, + }; + for (let coord of b) { + new_block.tiles.push( [ coord[0], coord[1] ] ); + } + blocks.push(new_block); + } + + for (let d of lvl.decorations) { + decorations.push({ + x: d[0], + y: d[1], + type: d[2], + }); + } + + game.state = State.STAND; +} + +function save() { + console.log("Saving"); + let save_data = level_number; + if (game.state === State.WIN) { + save_data = level_number + 1; + } + game.save('continue_level', save_data); +} + +function reset_save() { + game.save('continue_level', 1); +} + +function win() { + //game.sfx.victory.play(); + console.log("You won!"); + game.state = State.WIN; + game.sfx.victory.play(); + + let heart_x = 0; + let heart_y = 0; + for (let f of fish) { + if (f.species === creatureID.fish) { + f.hidden = true; + heart_x = f.x; + heart_y = f.y; + } + } + + for (let i = 0; i < 20; i++) { + create_heart(heart_x * game.tile_size + game.tile_size / 2 + Math.random() * 10 - 5, + heart_y * game.tile_size + game.tile_size / 2 + Math.random() * 10 - 5); + } + + save(); + + window.setTimeout(function() { + can_continue = true; + }, 350); +} + +function lose() { + game.state = State.LOSE; + game.sfx.fail.play(); + x_timer = 0; + x_frame = 0; + eaten_fish.hidden = true; +} + +function check_status() { + /* Check lose: a shark and a fish in the same area */ + for (let f of fish) { + if (f.species === creatureID.shark) { + for (let g of fish) { + if (g.species === creatureID.fish) { + let path = path_between(f.x, f.y, g.x, g.y, false); + if (path) { + game.state = State.LOSESWIM; + hovered_fish = f; + eaten_fish = g; + fish_path = path; + start_swimming(hovered_fish); + break; + } + } + } + if (game.state === State.LOSESWIM) break; + } + } + + /* Check win: all fish in same area */ + /* find first fish and see if it paths to all others */ + let first_fish = null; + for (let f of fish) { + if (f.species === creatureID.fish) { + first_fish = f; + break; + } + } + + let won = true; + for (let f of fish) { + if (f === first_fish) continue; + if (f.species !== creatureID.fish) continue; + let path = path_between(first_fish.x, first_fish.y, f.x, f.y, false, true); + if (path === null) { + won = false; + break; + } + } + + if (won) { + console.log("omg win"); + game.state = State.CONGREGATE; + /* DFS to find spot closest to all fish */ + for (let f of fish) { + if (f.species !== creatureID.fish) continue; + f.explored_territory = new Set(); + f.explored_territory.add(sofc([f.x, f.y])); + } + + let found_thing = false; + let common_square = null; + let bailout = 0; + while (!found_thing) { + for (let f of fish) { + if (f.species !== creatureID.fish) continue; + let new_explored_territory = new Set(); + for (let e of f.explored_territory) { + let coords = cofs(e); + let neighbors = [ + [coords[0] + 1, coords[1]], + [coords[0] - 1, coords[1]], + [coords[0], coords[1] + 1], + [coords[0], coords[1] - 1], + ]; + for (let n of neighbors) { + if (n[0] < 0 || n[0] >= game.level_w || n[1] < 0 || n[1] >= game.level_h) continue; + if (f.explored_territory.has(sofc(n))) continue; + if (anything_except_fish_at(n[0], n[1])) continue; + new_explored_territory.add(sofc(n)); + } + } + f.explored_territory = f.explored_territory.union(new_explored_territory); + } + + let common_squares = fish[0].explored_territory; + for (let f of fish) { + if (f.species !== creatureID.fish) continue; + common_squares = common_squares.intersection(f.explored_territory); + } + + bailout ++; + if (bailout > 1000) found_thing = true; + + if (common_squares.size > 0) { + common_square = cofs(Array.from(common_squares)[0]); + found_thing = true; + } + } + + if (common_square === null) { + console.error("Assert this should not happen!"); + } + + /* ok, now we found where all the fish will swim to. and we know they all have a path. so figure out the path for each fish */ + let max_win_path_length = 0; + for (let f of fish) { + if (f.species !== creatureID.fish) continue; + f.win_path = path_between(f.x, f.y, common_square[0], common_square[1], false, true); + if (f.win_path.length > max_win_path_length) { + max_win_path_length = f.win_path.length; + } + } + + /* now we want them to reach it at the same time. so scale each one's speed proportionately to their path length */ + for (let f of fish) { + if (f.species !== creatureID.fish) continue; + f.win_swim_speed = WIN_SWIM_SPEED * (f.win_path.length - 1) / (max_win_path_length - 1); + start_swimming(f); + } + } +} + +function start_swimming(fish) { + fish.target_x = fish.x; + fish.target_y = fish.y; + fish.swimming = true; + fish.move_fraction = 1; +} + +function create_heart(x, y) { + hearts.push({ + x: Math.round(x), + y: Math.round(y), + xspeed: Math.random() * 60 - 30, + yspeed: Math.random() * -30 - 30, + gravity: 50, + }); +} + +/* ---------- update functions ------------ */ + +/* MAIN UPDATE FUNCTION */ +function do_update(delta) { + update_bubbles(delta); + + if (game.state === State.WIN || game.state === State.LOSE) { + if (screen_message_delay > 0) { + screen_message_delay -= delta; + } else { + if (screen_message_alpha < 1) { + screen_message_alpha += delta / SCREEN_MESSAGE_FADEIN_LENGTH; + } else { + screen_message_alpha = 1; + } + } + } + + for (let f of fish) { + f.drift += delta / DRIFT_SPEED; + + f.timer += delta; + + let max_time = STAND_FRAME_LENGTH; + if (f.swimming) { + max_time = SWIM_FRAME_LENGTH; + } + while (f.timer > max_time) { + f.timer -= max_time; + f.frame ++; + f.frame %= 2; + } + } + + selector_timer += delta; + while (selector_timer > SELECTOR_FRAME_LENGTH) { + selector_timer -= SELECTOR_FRAME_LENGTH; + selector_frame ++; + selector_frame %= 2; + } + + if (game.state === State.DRAG) { + hovered_block.move_fraction += BLOCK_MOVE_SPEED * delta / 1000; + if (hovered_block.move_fraction > 1) { + for (let t of hovered_block.tiles) { + t[0] += hovered_block.target_dx; + t[1] += hovered_block.target_dy; + } + hovered_block.move_fraction = 0; + start_drag_x += hovered_block.target_dx; + start_drag_y += hovered_block.target_dy; + hovered_block.target_dx = 0; + hovered_block.target_dy = 0; + if (mouse_is_down) { + game.state = State.BLOCK; + } else { + game.state = State.STAND; + } + + /* Check if we won or lost */ + check_status(); + + if (game.state !== State.CONGREGATE && game.state !== State.LOSESWIM) { + /* Check if we need to move more */ + if (mouse_is_down) { + handle_mousemove(game, {}, mouse_x, mouse_y); + } + } else { + hovered_block = null; + for (let b of blocks) { + b.state = 0; + } + } + } + } else if (game.state === State.SWIM || game.state === State.LOSESWIM) { + let speed = SWIM_SPEED; + + if (game.state === State.LOSESWIM) { + speed = LOSE_SWIM_SPEED; + } + + let done = update_fish_swim(delta, hovered_fish, fish_path, speed); + + if (done) { + if (game.state === State.SWIM) { + game.state = State.STAND; + } else if (game.state === State.LOSESWIM) { + lose(); + } + if (game.state === State.SWIM) { + fish = null; + } + potential_fish_target = null; + handle_mousemove(game, {}, mouse_x, mouse_y); + } + } else if (game.state === State.CONGREGATE) { + let all_done = true; + for (let f of fish) { + if (f.species === creatureID.fish) { + let done = update_fish_swim(delta, f, f.win_path, f.win_swim_speed); + if (f.win_path.length > 0) { + all_done = false; + } + } + } + + if (all_done) { + win(); + } + } else if (game.state === State.LOSE) { + if (x_frame < MAX_X_FRAME) { + x_timer += delta; + while (x_timer > X_FRAME_LENGTH) { + x_timer -= X_FRAME_LENGTH; + x_frame ++; + } + } + } else if (game.state === State.WIN) { + update_hearts(delta); + } +} + +function update_fish_swim(delta, fish, path, speed) { + fish.move_fraction += speed * delta / 1000; + if (fish.move_fraction > 1) { + fish.x = fish.target_x; + fish.y = fish.target_y; + fish.move_fraction = 0; + if (path.length > 0 && path[0][0] === fish.x && path[0][1] === fish.y) { + /* Pop off start coordinate from path... */ + path.shift(); + } + + if (path.length == 0) { + console.log("Swim done"); + if (game.state === State.SWIM) { + fish.rotate = 0; + } + fish.swimming = false; + return true; + } else { + fish.target_x = path[0][0]; + fish.target_y = path[0][1]; + + let old_rotate = fish.rotate; + let old_hflip = fish.hflip; + + if (fish.target_x > fish.x) { + fish.hflip = false; + } else if (fish.target_x < fish.x) { + fish.hflip = true; + } + + if (fish.target_y > fish.y) { + fish.rotate = 90; + } else if (fish.target_y < fish.y) { + fish.rotate = -90; + } else { + fish.rotate = 0; + } + + if (fish.rotate !== old_rotate || fish.hflip !== old_hflip) { + game.sfx.drip.play(); + } + } + } + return false; +} + +function update_hearts(delta) { + for (let h of hearts) { + h.x += h.xspeed * delta / 1000; + h.y += h.yspeed * delta / 1000; + h.yspeed += h.gravity * delta / 1000; + } +} + +let bubble_sfx_timeout = 0; +function create_bubble(x, y) { + bubbles.push({ + x: x, + y: y, + frame: Math.floor(Math.random() * 2), + timer: Math.random() * BUBBLE_FRAME_LENGTH, + }); + + if (Math.random() < 0.8 && bubble_sfx_timeout <= 0) { + let decider = Math.random(); + if (decider < 0.25) { + game.sfx.bubble.play(); + } else if (decider < 0.5) { + game.sfx.bubble2.play(); + } else if (decider < 0.75) { + game.sfx.bubble3.play(); + } else { + game.sfx.bubble4.play(); + } + bubble_sfx_timeout = 800; + } +} + +function update_bubbles(delta) { + bubble_sfx_timeout -= delta; + + for (let f of fish) { + if (!f.hasOwnProperty("bubble_timeout")) { + f.bubble_timeout = 0; + } + f.bubble_timeout -= delta; + + if (f.swimming) continue; + if (f.hidden) continue; + if (f.bubble_timeout > 0) continue; + + if (Math.random() < 0.002) { + let x_offset = 0; + + if (f.species === creatureID.fish) { + if (f.hflip) { + x_offset = 6; + } else { + x_offset = 18; + } + } else if (f.species === creatureID.shark) { + if (f.hflip) { + x_offset = 3; + } else { + x_offset = 21; + } + } + + create_bubble(f.x * game.tile_size + x_offset, f.y * game.tile_size + game.tile_size / 2); + + f.bubble_timeout = 1500; + } + } + + for (let d of decorations) { + if (!d.hasOwnProperty("bubble_timeout")) { + d.bubble_timeout = 0; + } + d.bubble_timeout -= delta; + + if (d.bubble_timeout > 0) continue; + if (tile_at(d.x, d.y) === tileID.wall) continue; + + if (Math.random() < 0.001) { + let x_offset = Math.random() * game.tile_size * 2 / 3 - game.tile_size / 3; + + create_bubble(d.x * game.tile_size + game.tile_size / 2 + x_offset, d.y * game.tile_size + game.tile_size / 2); + + d.bubble_timeout = 3000; + } + } + + for (let b of bubbles) { + b.timer += delta; + while (b.timer > BUBBLE_FRAME_LENGTH) { + b.timer -= BUBBLE_FRAME_LENGTH; + b.frame ++; + b.frame %= 2; + } + b.y -= BUBBLE_UP_SPEED * delta / 1000; + if (zb.mod(b.y, game.tile_size) < game.tile_size / 2) { + if (tile_or_block_at(Math.floor(b.x / game.tile_size), Math.floor(b.y / game.tile_size))) { + b.deleteme = true; + } + } + } + + bubbles = bubbles.filter(b => !b.deleteme); +} + +/* ---------- draw functions ----------- */ + +/* DRAW */ +function do_draw(ctx) { + draw_bubbles(ctx); + + draw_map(ctx); + + if (!isNaN(parseInt(level_number))) { + zb.sprite_draw(ctx, game.img.levelnums, game.tile_size, game.tile_size, 0, level_number - 1, 0, (game.level_h - 1) * game.tile_size); + } + + draw_decorations(ctx); + + zb.buttons.draw(ctx, game.buttons.ingame); + + if (game.img.levels.hasOwnProperty(level_number)) { + zb.screen_draw(ctx, game.img.levels[level_number]); + } + + draw_blocks(ctx); + + draw_creatures(ctx); + + draw_hearts(ctx); + + if (potential_fish_target) { + zb.sprite_draw(ctx, game.img.selector, 28, 28, 0, zb.mod(selector_frame + 1, 2), + potential_fish_target[0] * game.tile_size - 2, potential_fish_target[1] * game.tile_size - 2); + } + + if (game.state === State.LOSE) { + zb.sprite_draw(ctx, game.img.x, 48, 48, 0, x_frame, hovered_fish.x * game.tile_size - 12, hovered_fish.y * game.tile_size - 12); + } + + if (game.state === State.WIN || game.state === State.LOSE) { + ctx.save(); + ctx.globalAlpha = screen_message_alpha; + if (game.state === State.WIN) { + zb.screen_draw(ctx, game.img.success); + } else { + zb.screen_draw(ctx, game.img.failure); + } + ctx.restore(); + } +} + +function do_draw_menu(ctx) { + zb.screen_draw(ctx, game.img.menuimage); + zb.buttons.draw(ctx, game.buttons.menu); +} + +function do_draw_youwon(ctx) { + zb.screen_draw(ctx, game.img.youwon); +} + +function rpgmaker_tile_format_thing(x, y, tile_check_func) { + let u = tile_check_func(x, y - 1); + let d = tile_check_func(x, y + 1); + let l = tile_check_func(x - 1, y); + let r = tile_check_func(x + 1, y); + let ul = tile_check_func(x - 1, y - 1); + let ur = tile_check_func(x + 1, y - 1); + let dl = tile_check_func(x - 1, y + 1); + let dr = tile_check_func(x + 1, y + 1); + let ulx, uly, urx, ury, dlx, dly, drx, dry; + + /* Draw top left */ + if (u) { + if (l) { + if (ul) { + /* Open water */ + ulx = 2; uly = 4; + } else { + /* Upper left corner */ + ulx = 2; uly = 0; + } + } else { + /* Left border */ + ulx = 0; uly = 4; + } + } else { + if (l) { + /* Top border */ + ulx = 2; uly = 2; + } else { + /* Very corner */ + ulx = 0; uly = 2; + } + } + + /* Draw top right */ + if (u) { + if (r) { + if (ur) { + /* Open water */ + urx = 1; ury = 4; + } else { + /* Upper right corner */ + urx = 3; ury = 0; + } + } else { + /* Right border */ + urx = 3; ury = 4; + } + } else { + if (r) { + /* Top border */ + urx = 1; ury = 2; + } else { + /* Very corner */ + urx = 3; ury = 2; + } + } + + /* Draw bottom left */ + if (d) { + if (l) { + if (dl) { + /* Open water */ + dlx = 2; dly = 3; + } else { + /* Bottom left corner */ + dlx = 2; dly = 1; + } + } else { + /* Left border */ + dlx = 0; dly = 3; + } + } else { + if (l) { + /* Bottom border */ + dlx = 2; dly = 5; + } else { + /* Very corner */ + dlx = 0; dly = 5; + } + } + + /* Draw bottom right */ + if (d) { + if (r) { + if (dr) { + /* Open water */ + drx = 1; dry = 3; + } else { + /* Bottom right corner */ + drx = 3; dry = 1; + } + } else { + /* Right border */ + drx = 3; dry = 3; + } + } else { + if (r) { + /* Bottom border */ + drx = 1; dry = 5; + } else { + /* Very corner */ + drx = 3; dry = 5; + } + } + + return { ulx: ulx, uly: uly, urx: urx, ury: ury, dlx: dlx, dly: dly, drx: drx, dry: dry }; +} + +function draw_rpgmaker_tile(ctx, img, x, y, rpgmaker_tft, state) { + let ts = game.tile_size; + let { ulx, uly, urx, ury, dlx, dly, drx, dry } = rpgmaker_tft; + zb.sprite_draw(ctx, img, ts / 2, ts / 2, ulx, uly + state * 6, x, y); + zb.sprite_draw(ctx, img, ts / 2, ts / 2, urx, ury + state * 6, x + ts / 2, y); + zb.sprite_draw(ctx, img, ts / 2, ts / 2, dlx, dly + state * 6, x, y + ts / 2); + zb.sprite_draw(ctx, img, ts / 2, ts / 2, drx, dry + state * 6, x + ts / 2, y + ts / 2); +} + +function draw_map(ctx) { + for (let y = 0; y < game.level_h; y++) { + for (let x = 0; x < game.level_w; x++) { + if (tile_at(x, y) === tileID.wall) { + let tileinfo = rpgmaker_tile_format_thing(x, y, (x, y) => tile_at(x, y) === tileID.wall); + draw_rpgmaker_tile(ctx, game.img.wall_tile, x * game.tile_size, y * game.tile_size, tileinfo, 0); + } + } + } +} + +function draw_blocks(ctx) { + for (let block of blocks) { + for (let tile of block.tiles) { + let tileinfo = rpgmaker_tile_format_thing(tile[0], tile[1], (x, y) => { + for (let tile of block.tiles) { + if (tile[0] === x && tile[1] === y) { + return true; + } + } + return false; + }); + draw_rpgmaker_tile(ctx, game.img.move_tile, + tile[0] * game.tile_size + Math.round(block.target_dx * game.tile_size * block.move_fraction), + tile[1] * game.tile_size + Math.round(block.target_dy * game.tile_size * block.move_fraction), + tileinfo, block.state); + } + } +} + +function draw_decorations(ctx) { + for (let d of decorations) { + ctx.save(); + if (d.type === 2) { + ctx.translate(0, 1); + } + zb.sprite_draw(ctx, game.img.decorations, game.tile_size, game.tile_size, d.type, 0, d.x * game.tile_size, d.y * game.tile_size); + ctx.restore(); + } +} + +function draw_creatures(ctx) { + for (let f of fish) { + if (f.hidden) continue; + + ctx.save(); + + let mf = f.move_fraction || 0; + let tx = f.target_x !== undefined ? f.target_x : f.x; + let ty = f.target_y !== undefined ? f.target_y : f.y; + + let drift_y_offset = Math.round(1.9 * Math.sin(f.drift * 2 * Math.PI)); + + ctx.translate(f.x * game.tile_size * (1 - mf) + tx * game.tile_size * mf, + f.y * game.tile_size * (1 - mf) + ty * game.tile_size * mf); + + if (f.hflip) { + ctx.translate(game.tile_size, 0); + ctx.scale(-1, 1); + } + + if (f.vflip) { + ctx.translate(0, game.tile_size); + ctx.scale(1, -1); + } + + if (f.rotate) { + ctx.translate(game.tile_size / 2, game.tile_size / 2); + ctx.rotate(f.rotate * Math.PI / 180); + ctx.translate(-game.tile_size / 2, -game.tile_size / 2); + } + + zb.sprite_draw(ctx, game.img.fish, game.tile_size, game.tile_size, f.type, f.frame, 0, drift_y_offset); + if (f === hovered_fish && (game.state === State.STAND || game.state === State.FISH)) { + zb.sprite_draw(ctx, game.img.selector, 28, 28, 0, selector_frame, -2, -2); + } + + ctx.restore(); + } +} + +function draw_bubbles(ctx) { + for (let b of bubbles) { + zb.sprite_draw(ctx, game.img.bubble, 5, 11, 0, b.frame, Math.round(b.x - game.img.bubble.width / 2), Math.round(b.y - game.img.bubble.height / 2)); + } +} + +function draw_hearts(ctx) { + for (let h of hearts) { + ctx.drawImage(game.img.heart, Math.round(h.x - game.img.heart.width / 2), Math.round(h.y - game.img.heart.height / 2)); + } +} + +/* ---------- event handlers ------------ */ + +function handle_gamestart(game) { + console.log("Game start!"); + + //game.music.bgm_menu.play(); +} + +function handle_mousedown(game, e, x, y) { + mouse_x = x; + mouse_y = y; + mouse_is_down = true; + + if (game.mode === 'menu') { + if (zb.buttons.click(game.buttons.menu, x, y)) return; + } else if (game.mode === 'game') { + if (zb.buttons.click(game.buttons.ingame, x, y)) return; + + let tile_x = Math.floor(mouse_x / game.tile_size); + let tile_y = Math.floor(mouse_y / game.tile_size); + + if (game.mode === 'game') { + //if (zb.buttons.click(buttons.ingame, x, y)) return; + + if (game.state === State.WIN) { + return; + } + + if (game.state === State.STAND) { + if (hovered_block) { + hovered_block.state = 2; + game.state = State.BLOCK; + start_drag_x = tile_x; + start_drag_y = tile_y; + } + } + } + } +} + +function handle_mouseup(game, e, x, y) { + mouse_is_down = false; + just_clicked_block = true; + + if (game.mode === 'youwon') { + game.long_transition(zb.transition.type.fade, 1000, function() { + continue_level = 1; + game.buttons.menu.buttons[1].disabled = true; + game.mode = 'menu'; + }); + } else if (game.mode === 'menu') { + zb.buttons.update(game.buttons.menu, x, y); + } else if (game.mode === 'game') { + zb.buttons.update(game.buttons.ingame, x, y); + + if (game.state === State.WIN) { + advance_level(); + } else if (game.state === State.BLOCK) { + game.state = State.STAND; + handle_mousemove(game, e, x, y); + } else if (game.state === State.FISH) { + if (potential_fish_target === null) { + game.state = State.STAND; + handle_mousemove(game, e, x, y); + } else { + create_undo_point(); + game.state = State.SWIM; + start_swimming(hovered_fish); + } + } else if (game.state === State.STAND) { + if (hovered_fish) { + game.state = State.FISH; + } + } + + if (won_everything) { + return; + } + } +} + +function handle_mouseleave(game, e) { + mouse_is_down = false; + if (game.mode === 'game') { + if (game.state === State.STAND || game.state === State.BLOCK) { + handle_mouseup(game, e, -1, -1); + } + } +} + +function handle_mousemove(game, e, x, y) { + mouse_x = x; + mouse_y = y; + + let tile_x = Math.floor(mouse_x / game.tile_size); + let tile_y = Math.floor(mouse_y / game.tile_size); + + if (game.mode === 'menu') { + zb.buttons.update(game.buttons.menu, mouse_x, mouse_y); + } else if (game.mode === 'game') { + zb.buttons.update(game.buttons.ingame, mouse_x, mouse_y); + + if (game.state === State.STAND) { + hovered_block = null; + for (let b of blocks) { + b.state = 0; + for (let t of b.tiles) { + if (t[0] === tile_x && t[1] === tile_y) { + b.state = 1; + hovered_block = b; + break; + } + } + } + + hovered_fish = fish_at(tile_x, tile_y); + } else if (game.state === State.FISH) { + potential_fish_target = null; + fish_path = path_between(hovered_fish.x, hovered_fish.y, tile_x, tile_y, true); + if (fish_path) { + potential_fish_target = [tile_x, tile_y]; + } + } else if (game.state === State.BLOCK) { + if (tile_x !== start_drag_x || tile_y !== start_drag_y) { + /* Move block */ + let dx = 0; + let dy = 0; + hovered_block.target_dx = 0; + hovered_block.target_dy = 0; + if (tile_x > start_drag_x) { + dx = 1; + } else if (tile_x < start_drag_x) { + dx = -1; + } else if (tile_y > start_drag_y) { + dy = 1; + } else if (tile_y < start_drag_y) { + dy = -1; + } + + let free_to_move = true; + let mouse_inside_block = false; + for (let t of hovered_block.tiles) { + if (anything_at(t[0] + dx, t[1] + dy, hovered_block)) { + free_to_move = false; + } + + if (t[0] === tile_x && t[1] === tile_y) { + mouse_inside_block = true; + } + } + + if (free_to_move) { + if (just_clicked_block) { + create_undo_point(); + just_clicked_block = false; + } + hovered_block.move_fraction = 0; + hovered_block.target_dx = dx; + hovered_block.target_dy = dy; + game.state = State.DRAG; + game.sfx.drag.play(); + } else if (mouse_inside_block) { + start_drag_x = tile_x; + start_drag_y = tile_y; + } + } + } + } +} + +function toggle_mute_button() { + if (game.buttons.ingame.buttons[2].id == 2) { + game.buttons.ingame.buttons[2].id = 3; + } else { + game.buttons.ingame.buttons[2].id = 2; + } + game.toggle_mute(); +} + +function handle_keyup(game, e) { + if (game.transition.is_transitioning) return; + + // key up + switch (e.key) { + case 'm': + toggle_mute_button(); + e.preventDefault(); + break; + case 'r': + reset(); + e.preventDefault(); + break; + case 'z': + undo(); + e.preventDefault(); + break; + } +} + +function new_game_button() { + game.music.bgm.play(); + game.start_transition(zb.transition.type.fade, 1000, function() { + game.mode = 'game'; + level_number = 1; + load_level(); + }); +} + +function continue_button() { + game.music.bgm.play(); + game.start_transition(zb.transition.type.fade, 1000, function() { + game.mode = 'game'; + level_number = continue_level; + load_level(); + }); +} diff --git a/img/bubble.png b/img/bubble.png new file mode 100644 index 0000000..d601231 Binary files /dev/null and b/img/bubble.png differ diff --git a/img/buttons.png b/img/buttons.png new file mode 100644 index 0000000..86d6565 Binary files /dev/null and b/img/buttons.png differ diff --git a/img/decoration.png b/img/decoration.png new file mode 100644 index 0000000..89582fd Binary files /dev/null and b/img/decoration.png differ diff --git a/img/failure.png b/img/failure.png new file mode 100644 index 0000000..967da35 Binary files /dev/null and b/img/failure.png differ diff --git a/img/fish.png b/img/fish.png new file mode 100644 index 0000000..ad1235a Binary files /dev/null and b/img/fish.png differ diff --git a/img/heart.png b/img/heart.png new file mode 100644 index 0000000..143ece0 Binary files /dev/null and b/img/heart.png differ diff --git a/img/level/1.png b/img/level/1.png new file mode 100644 index 0000000..bffdafb Binary files /dev/null and b/img/level/1.png differ diff --git a/img/level/2.png b/img/level/2.png new file mode 100644 index 0000000..01c3882 Binary files /dev/null and b/img/level/2.png differ diff --git a/img/level/4.png b/img/level/4.png new file mode 100644 index 0000000..9e70f32 Binary files /dev/null and b/img/level/4.png differ diff --git a/img/levelnums.png b/img/levelnums.png new file mode 100644 index 0000000..7107a49 Binary files /dev/null and b/img/levelnums.png differ diff --git a/img/menubuttons.png b/img/menubuttons.png new file mode 100644 index 0000000..9165a92 Binary files /dev/null and b/img/menubuttons.png differ diff --git a/img/menuimage.png b/img/menuimage.png new file mode 100644 index 0000000..5623295 Binary files /dev/null and b/img/menuimage.png differ diff --git a/img/move_tile.png b/img/move_tile.png new file mode 100644 index 0000000..0cc83ae Binary files /dev/null and b/img/move_tile.png differ diff --git a/img/selector.png b/img/selector.png new file mode 100644 index 0000000..6b74208 Binary files /dev/null and b/img/selector.png differ diff --git a/img/success.png b/img/success.png new file mode 100644 index 0000000..025419c Binary files /dev/null and b/img/success.png differ diff --git a/img/wall_tile.png b/img/wall_tile.png new file mode 100644 index 0000000..8ac813d Binary files /dev/null and b/img/wall_tile.png differ diff --git a/img/x.png b/img/x.png new file mode 100644 index 0000000..07171aa Binary files /dev/null and b/img/x.png differ diff --git a/img/youwon.png b/img/youwon.png new file mode 100644 index 0000000..7471f14 Binary files /dev/null and b/img/youwon.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..1ae5796 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + fish shark game + + + + + + + + + + + diff --git a/levels.js b/levels.js new file mode 100644 index 0000000..7796217 --- /dev/null +++ b/levels.js @@ -0,0 +1,13 @@ +var levels={ + 10: { map: [0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[0,3], ],[[2,3], [2,4], [1,3], [1,4], ],[[3,3], [3,4], [4,3], ],[[4,4], ],[[1,6], [1,7], [2,6], [2,7], ],[[3,7], [4,6], [4,7], [3,6], ],], fish: [[3,1], [1,9], ], sharks: [[5,5], ], decorations: [[6,0,0], [4,1,2], [5,1,2], [7,1,0], [6,2,0], [5,7,3], [6,7,3], [4,9,1], [6,9,1], [1,10,2], [2,10,2], [3,10,2], [5,10,1], [7,10,1], ] }, + 1: { map: [0,0,0,0,0,0,0,1,1,1,1,1,0,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0,1,1,1,1,0,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,], blocks: [[[4,3], [5,3], [4,2], [5,2], ],[[4,4], [4,5], ],[[4,6], [3,5], [3,6], ],[[5,5], [6,5], ],[[5,7], ],], fish: [[2,0], [1,7], [6,8], ], sharks: [], decorations: [[0,0,2], [1,0,2], [7,2,1], [0,8,2], [1,8,2], [4,8,1], ] }, + 2: { map: [0,0,1,0,0,0,0,1,0,1,1,0,0,0,0,1,0,1,0,0,0,0,1,1,0,1,0,0,0,0,1,1,0,0,1,1,0,0,1,1,0,1,1,1,0,0,0,1,0,1,1,1,0,0,1,1,0,0,0,0,0,0,1,1,0,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[4,0], [5,0], [5,1], [4,1], ],[[0,1], [0,2], [0,3], [0,4], ],[[4,3], [5,3], [4,2], [5,2], ],[[1,4], ],[[4,4], [5,4], [4,5], ],[[5,5], [6,5], ],], fish: [[1,0], [6,1], ], sharks: [], decorations: [[2,3,2], [3,3,2], [0,8,2], [6,9,0], [5,10,0], [7,10,0], ] }, + 3: { map: [1,1,0,0,0,0,1,1,0,1,0,0,0,1,1,1,0,0,0,0,0,1,0,1,0,0,0,0,0,1,0,1,0,0,0,1,1,1,0,1,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[3,1], [4,1], [2,1], ],[[4,2], [4,3], ],[[0,7], [1,5], [1,4], [0,6], [0,5], [1,6], ],[[6,4], [6,5], ],[[6,6], [5,5], [5,6], ],], fish: [[5,0], [6,3], [0,9], ], sharks: [], decorations: [[7,0,1], [2,4,2], [5,8,2], [4,9,2], [7,9,1], [6,10,1], ] }, + 4: { map: [0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,], blocks: [[[4,0], [6,1], [5,1], [5,0], [6,0], [4,1], ],[[4,3], [5,3], [4,2], [5,2], ],[[4,4], [3,4], [5,4], [0,3], [2,3], [3,3], [1,3], ],], fish: [[3,1], [2,6], ], sharks: [[6,3], ], decorations: [[1,0,0], [3,7,3], [4,7,3], [5,7,3], ] }, + 5: { map: [0,0,0,0,1,1,1,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[3,5], ],[[4,5], [4,6], [3,6], ],[[3,7], [3,8], [3,9], ],], fish: [[5,1], [6,1], [5,2], [6,2], [5,3], [6,3], [5,4], [6,4], [6,5], [4,8], ], sharks: [[1,2], ], decorations: [[4,0,1], [7,6,1], [6,8,1], [4,9,3], [0,10,3], [7,10,1], ] }, + 6: { map: [0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[2,3], [0,3], [1,3], [3,3], ],[[2,7], [3,7], [2,6], [3,6], [2,5], ],[[5,5], [5,6], [6,5], ],[[4,8], [4,7], [5,7], [5,8], ],], fish: [[2,0], [2,2], ], sharks: [[6,7], [0,10], ], decorations: [[0,0,2], [1,0,2], [7,1,0], [2,10,3], [3,10,3], [5,10,1], ] }, + 7: { map: [1,0,0,1,0,0,1,1,0,0,0,1,0,0,0,1,0,1,1,1,1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,1,1,1,0,0,0,0,1,1,0,0,0,1,0,0,0,1,0,1,1,1,1,1,0,1,0,0,0,1,0,0,0,1,1,0,0,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[1,3], [1,4], ],[[2,4], [2,5], ],[[4,4], [4,5], ],[[5,4], [5,5], ],], fish: [[2,1], [4,1], [2,8], [4,8], ], sharks: [[3,4], [3,5], ], decorations: [[1,1,2], [5,1,2], [3,3,0], [3,6,0], [1,9,3], [2,9,3], [4,9,3], [5,9,3], ] }, + 8: { map: [1,1,0,0,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[6,1], [6,2], [5,0], [5,1], ],[[0,2], [1,2], [0,3], ],[[2,3], [1,3], [1,4], ],[[2,4], [2,5], [3,5], ],[[3,7], [4,6], [5,6], [3,6], ],[[3,8], [1,8], [2,9], [3,9], [2,8], ],[[0,9], [0,10], ],], fish: [[6,0], [5,7], [1,9], ], sharks: [[2,2], [5,4], [1,5], ], decorations: [[6,3,2], [4,8,3], [6,8,0], [5,9,0], [6,9,1], [5,10,1], [6,10,0], ] }, + 9: { map: [0,0,0,0,0,0,1,1,0,0,0,0,1,0,1,1,1,1,1,1,1,0,1,1,1,0,0,0,1,0,1,1,1,0,1,0,1,0,1,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[5,3], [5,4], [5,5], ],[[2,5], [2,6], [1,5], ],[[4,6], [5,6], [3,5], [3,6], ],[[1,10], ],], fish: [[0,0], [1,0], [0,1], [1,1], [0,5], ], sharks: [[1,6], [1,7], [2,7], [3,7], [4,7], [5,7], [1,8], [2,8], [5,8], [1,9], [2,9], [3,9], [4,9], [5,9], ], decorations: [[6,0,1], [7,0,0], [2,1,2], [3,1,2], [6,3,0], [2,4,0], [6,4,1], [6,5,0], [7,6,0], [7,7,1], [6,8,1], [6,9,0], [7,9,1], [6,11,0], [7,11,1], ] }, + test: { map: [1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,0,0,1,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,], blocks: [[[5,3], [5,4], [4,4], [4,3], ],], fish: [[3,2], [3,6], [3,9], ], sharks: [], decorations: [] }, +} diff --git a/levels/1.tmx b/levels/1.tmx new file mode 100644 index 0000000..b1f7efc --- /dev/null +++ b/levels/1.tmx @@ -0,0 +1,20 @@ + + + + + +12,12,3,1,1,1,1,2, +2,2,2,2,1,2,1,2, +1,1,1,2,7,7,1,11, +1,1,1,2,7,7,1,2, +2,2,2,2,8,1,2,2, +2,1,1,6,8,5,5,2, +1,1,1,6,6,2,1,2, +1,3,2,1,1,6,1,2, +12,12,2,2,11,2,3,2, +2,2,1,1,1,1,2,2, +2,2,1,1,1,1,2,2, +2,2,2,2,2,2,2,2 + + + diff --git a/levels/10.tmx b/levels/10.tmx new file mode 100644 index 0000000..b450c62 --- /dev/null +++ b/levels/10.tmx @@ -0,0 +1,20 @@ + + + + + +1,1,1,1,1,1,10,2, +1,1,1,3,12,12,2,10, +1,1,1,2,2,2,10,2, +8,6,6,5,5,1,1,2, +2,6,6,5,7,1,1,2, +2,2,2,2,2,4,1,2, +2,6,6,5,5,1,1,2, +2,6,6,5,5,13,13,2, +2,1,1,2,2,2,2,2, +1,3,1,1,11,2,11,2, +2,12,12,12,2,11,2,11, +2,2,2,2,2,2,2,2 + + + diff --git a/levels/2.tmx b/levels/2.tmx new file mode 100644 index 0000000..888160a --- /dev/null +++ b/levels/2.tmx @@ -0,0 +1,20 @@ + + + + + +1,3,2,1,6,6,1,2, +8,2,2,1,6,6,3,2, +8,2,1,1,7,7,2,2, +8,2,12,12,7,7,2,2, +8,5,2,2,5,5,2,2, +1,2,2,2,5,9,9,2, +1,2,2,2,1,1,2,2, +1,1,1,1,1,1,2,2, +12,2,2,2,2,2,2,2, +2,1,1,1,1,2,10,2, +2,1,1,1,1,10,2,10, +2,2,2,2,2,2,2,2 + + + diff --git a/levels/3.tmx b/levels/3.tmx new file mode 100644 index 0000000..31b4151 --- /dev/null +++ b/levels/3.tmx @@ -0,0 +1,20 @@ + + + + + +2,2,1,1,1,3,2,11, +1,2,7,7,7,2,2,2, +1,1,1,1,9,2,1,2, +1,1,1,1,9,2,3,2, +1,8,12,2,2,2,5,2, +8,8,2,1,1,6,5,2, +8,8,1,2,1,6,6,2, +8,1,1,2,1,1,2,2, +1,1,1,1,1,12,2,2, +3,1,1,1,12,2,2,11, +2,2,2,2,2,2,11,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..11bc8b3 --- /dev/null +++ b/levels/4.tmx @@ -0,0 +1,20 @@ + + + + + +1,10,1,1,6,6,6,2, +1,1,1,3,6,6,6,2, +1,1,1,1,5,5,1,2, +8,8,8,8,5,5,4,2, +1,1,1,8,8,8,2,2, +1,1,1,1,1,1,2,2, +1,1,3,1,1,1,2,2, +1,1,1,13,13,13,2,2, +2,2,2,2,2,2,2,2, +2,1,1,1,1,1,1,2, +2,1,1,1,1,1,1,2, +2,2,2,2,2,2,2,2 + + + diff --git a/levels/5.tmx b/levels/5.tmx new file mode 100644 index 0000000..d76e7f7 --- /dev/null +++ b/levels/5.tmx @@ -0,0 +1,20 @@ + + + + + +1,1,1,1,11,2,2,2, +1,1,1,1,2,3,3,2, +1,4,1,1,2,3,3,2, +1,1,1,1,2,3,3,2, +1,1,1,1,2,3,3,2, +1,1,1,6,8,1,3,2, +1,1,1,8,8,1,2,11, +1,1,1,5,2,2,2,2, +1,1,1,5,3,2,11,2, +1,1,1,5,13,2,2,2, +13,1,1,2,2,2,2,11, +2,2,2,2,2,2,2,2 + + + diff --git a/levels/6.tmx b/levels/6.tmx new file mode 100644 index 0000000..4c29414 --- /dev/null +++ b/levels/6.tmx @@ -0,0 +1,20 @@ + + + + + +12,12,3,1,1,1,1,2, +2,2,2,2,2,1,1,10, +1,1,3,1,2,1,1,2, +6,6,6,6,2,1,1,2, +1,1,1,1,2,1,1,2, +1,1,6,1,2,6,6,2, +1,1,6,6,2,6,1,2, +1,1,6,6,5,5,4,1, +1,1,1,1,5,5,2,2, +1,1,1,1,1,2,2,2, +4,1,13,13,2,11,2,2, +2,2,2,2,2,2,2,2 + + + diff --git a/levels/7.tmx b/levels/7.tmx new file mode 100644 index 0000000..e17291f --- /dev/null +++ b/levels/7.tmx @@ -0,0 +1,20 @@ + + + + + +2,1,1,2,1,1,2,2, +1,12,3,2,3,12,1,2, +1,2,2,2,2,2,1,2, +1,6,1,10,1,1,1,2, +1,6,5,4,5,6,2,2, +2,2,5,4,5,6,2,2, +1,1,1,10,1,1,1,2, +1,2,2,2,2,2,1,2, +1,1,3,2,3,1,1,2, +2,13,13,2,13,13,2,2, +2,2,2,2,2,2,2,2, +2,2,2,2,2,2,2,2 + + + diff --git a/levels/8.tmx b/levels/8.tmx new file mode 100644 index 0000000..b1689fe --- /dev/null +++ b/levels/8.tmx @@ -0,0 +1,20 @@ + + + + + +2,2,1,1,1,6,3,2, +1,1,1,1,2,6,6,2, +9,9,4,1,2,1,6,2, +9,7,7,2,1,1,12,2, +1,7,8,1,1,4,2,2, +2,4,8,8,1,1,2,2, +1,1,1,5,5,5,2,2, +1,1,1,5,1,3,2,2, +1,8,8,8,13,2,10,2, +7,3,8,8,2,10,11,2, +7,2,2,2,2,11,10,2, +2,2,2,2,2,2,2,2 + + + diff --git a/levels/9.tmx b/levels/9.tmx new file mode 100644 index 0000000..2bb7307 --- /dev/null +++ b/levels/9.tmx @@ -0,0 +1,20 @@ + + + + + +3,3,1,1,1,1,11,10, +3,3,12,12,2,1,2,2, +2,2,2,2,2,1,2,2, +2,1,1,1,2,7,10,2, +2,1,10,1,2,7,11,2, +3,5,5,9,1,7,10,2, +2,4,5,9,9,9,2,10, +2,4,4,4,4,4,2,11, +2,4,4,1,1,4,11,2, +2,4,4,4,4,4,10,11, +2,6,2,2,2,2,2,2, +2,2,2,2,2,2,10,11 + + + diff --git a/levels/mapcompile.py b/levels/mapcompile.py new file mode 100755 index 0000000..820231c --- /dev/null +++ b/levels/mapcompile.py @@ -0,0 +1,94 @@ +#!/usr/bin/python3 + +import ulvl +import sys + +if len(sys.argv) < 2: + print("usage:", sys.argv[0], "") + sys.exit(1) + +screenwidth, screenheight = 8, 12 + +tilemapping = { 9: 1, 10: 1, 11: 0, 12: 0 } + +decomapping = { 9: 0, 10: 1, 11: 2, 12: 3 } + +movable_types = [4,5,6,7,8] + +fish_code = 2 +shark_code = 3 + +print("var levels={") +for filename in sys.argv[1:]: + m = ulvl.TMX.load(filename) + + w = m.meta['width'] + h = m.meta['height'] + + movable_blocks = [] + fish = [] + sharks = [] + decorations = [] + + used_block_coords = set() + + print('\t', filename.replace('.tmx', '').replace('levels/', ''), end=': { ') + + print('map: [', end='') + for y in range(screenheight): + for x in range(screenwidth): + thing = m.layers[0].tiles[y * w + x] - 1 + + if (x, y) in used_block_coords: + print('0,', end='') + elif thing == fish_code: + fish.append((x, y)) + print('0,', end='') + elif thing == shark_code: + sharks.append((x, y)) + print('0,', end='') + elif thing in movable_types: + block_type = thing + block_locs = set() + block_locs.add((x, y)) + used_block_coords.add((x, y)) + queue = [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)] + while len(queue) > 0: + check_loc = queue.pop(0) + if check_loc in block_locs: + # we already looked at this. skip! + continue + if m.layers[0].tiles[check_loc[1] * w + check_loc[0]] - 1 != thing: + # this isn't in our block. skip! + continue + # ok so this is part of our block. add it and add its neighbors to the queue + block_locs.add(check_loc) + used_block_coords.add(check_loc) + queue += [(check_loc[0] - 1, check_loc[1]), (check_loc[0] + 1, check_loc[1]), + (check_loc[0], check_loc[1] - 1), (check_loc[0], check_loc[1] + 1)] + + # Save our block + movable_blocks.append(list(block_locs)) + print('0,', end='') + else: + print(tilemapping.get(thing, thing), end='') + if thing in decomapping: + decorations.append((x, y, decomapping[thing])) + print(',', end='') + print('], blocks: [', end='') + for b in movable_blocks: + print('[', end='') + for coords in b: + print('[' + str(coords[0]) + ',' + str(coords[1]) + '], ', end='') + print('],', end='') + print('], fish: [', end='') + for f in fish: + print('[' + str(f[0]) + ',' + str(f[1]) + '], ', end='') + print('], sharks: [', end='') + for s in sharks: + print('[' + str(s[0]) + ',' + str(s[1]) + '], ', end='') + print('], decorations: [', end='') + for d in decorations: + print('[' + str(d[0]) + ',' + str(d[1]) + ',' + str(d[2]) + '], ', end='') + print('] },'); +print("}") diff --git a/levels/test.tmx b/levels/test.tmx new file mode 100644 index 0000000..b86cf3e --- /dev/null +++ b/levels/test.tmx @@ -0,0 +1,20 @@ + + + + + +2,2,2,2,2,2,2,2, +2,1,1,1,1,1,1,2, +2,1,1,3,1,1,1,2, +2,2,2,2,8,8,2,2, +2,2,2,2,8,8,2,2, +2,1,1,1,1,1,1,2, +2,1,1,3,1,1,1,2, +2,1,1,1,1,1,1,2, +2,1,1,1,1,1,1,2, +2,1,1,3,1,1,1,2, +2,1,1,1,1,1,1,2, +2,2,2,2,2,2,2,2 + + + diff --git a/levels/tmptile.png b/levels/tmptile.png new file mode 100644 index 0000000..f4b4b2f 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..4ae6535 --- /dev/null +++ b/levels/tmptile.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/loading.png b/loading.png new file mode 100644 index 0000000..9552ba1 Binary files /dev/null and b/loading.png differ diff --git a/music/sharkgamebeachmusic.mp3 b/music/sharkgamebeachmusic.mp3 new file mode 100755 index 0000000..6390123 Binary files /dev/null and b/music/sharkgamebeachmusic.mp3 differ diff --git a/packaging.zip b/packaging.zip new file mode 100644 index 0000000..32eab08 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..9454614 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..dfa9f2b --- /dev/null +++ b/packaging/game.js @@ -0,0 +1,1514 @@ +"use strict"; + +let game; + +let game_started = false; + +let just_started = true; + +let level_number = 1; + +let map; + +let buttons = {}; + +let max_unlocked_level; +let continue_level; + +let how_to_play = false; + +let disable_transitions = false; +let disable_colors = false; + +let won_everything = false; + +let KINDS_OF_FISH = 5; + +/* --------- definitions ---------- */ + +/* state: + * STAND: waiting for input + * BLOCK: moving block + * DRAG: actually dragging block + * FISH: moving fish or shark + * CONGREGATE: swim together to win + * WIN: level complete + * LOSESWIM: shark swimming to eat us + * LOSE: got eaten by shark + */ +let State = { STAND: 0, BLOCK: 1, DRAG: 2, FISH: 3, SWIM: 4, CONGREGATE: 5, WIN: 6, LOSESWIM: 7, LOSE: 8 }; + +let UIState = { MENU: 0, LEVELSELECT: 1, OPTIONS: 2, GAME: 3, REALLYQUIT: 4, REALLYDELETE: 5 }; + +let can_continue = false; + +let save_data = 1; +const SAVE_KEY = "casso.continentalshelf.save" + +zb.ready(function() { + game = zb.create_game({ + canvas: 'canvas', + canvas_w: 576, + canvas_h: 864, + draw_scale: 3, + tile_size: 24, + level_w: 8, + level_h: 12, + background_color: '#192c4a', + run_in_background: true, + load_with_progress_bar: false, + save_key: SAVE_KEY, + state: State.STAND, + uistate: UIState.MENU, + events: { + keyup: handle_keyup, + mouseup: handle_mouseup, + mouseleave: handle_mouseleave, + mousedown: handle_mousedown, + mousemove: handle_mousemove, + gamestart: handle_gamestart, + }, + modes: { + menu: { + draw: do_draw_menu, + }, + youwon: { + draw: do_draw_youwon, + }, + game: { + draw: do_draw, + update: do_update, + }, + }, + mode: 'menu', + buttons: {}, + }); + + game.register_images({ + fish: 'img/fish.png', + selector: 'img/selector.png', + x: 'img/x.png', + heart: 'img/heart.png', + bubble: 'img/bubble.png', + move_tile: 'img/move_tile.png', + wall_tile: 'img/wall_tile.png', + decorations: 'img/decoration.png', + levelnums: 'img/levelnums.png', + success: 'img/success.png', + failure: 'img/failure.png', + buttons: 'img/buttons.png', + menubuttons: 'img/menubuttons.png', + menuimage: 'img/menuimage.png', + youwon: 'img/youwon.png', + levels: { + 1: 'img/level/1.png', + 2: 'img/level/2.png', + 4: 'img/level/4.png', + } + }); + + game.register_sfx({ + victory: { + path: 'sfx/victory.wav', + volume: 0.3, + }, + fail: { + path: 'sfx/fail.wav', + volume: 0.8, + }, + drip: { + path: 'sfx/drip.wav', + volume: 1, + }, + drag: { + path: 'sfx/drag.wav', + volume: 0.19, + }, + bubble: { + path: 'sfx/bubble.wav', + volume: 0.5, + }, + bubble2: { + path: 'sfx/bubble2.wav', + volume: 0.5, + }, + bubble3: { + path: 'sfx/bubble3.wav', + volume: 0.5, + }, + bubble4: { + path: 'sfx/bubble4.wav', + volume: 0.5, + }, + }); + + game.register_music({ + bgm: { + path: 'music/sharkgamebeachmusic', + volume: 0.1, + }, + }); + + game.resources_ready(); + + continue_level = parseInt(game.load("continue_level") || "1"); + + game.buttons.ingame = zb.buttons.create({ + img: game.img.buttons, + button_w: 24, + button_h: 24, + buttons: [ + { + /* Undo */ + x: game.screen_w - 72, + y: game.screen_h - 24, + id: 0, + callback: undo, + }, + { + /* Reset */ + x: game.screen_w - 48, + y: game.screen_h - 24, + id: 1, + callback: reset, + }, + { + /* Toggle mute */ + x: game.screen_w - 24, + y: game.screen_h - 24, + id: 2, + callback: toggle_mute_button, + }, + ], + }); + + game.buttons.menu = zb.buttons.create({ + img: game.img.menubuttons, + button_w: 72, + button_h: 16, + buttons: [ + { + /* New game */ + x: game.screen_w / 2 - 36, + y: game.screen_h / 2 + 36, + id: 0, + callback: new_game_button, + }, + { + /* Continue */ + x: game.screen_w / 2 - 36, + y: game.screen_h / 2 + 60, + id: 1, + callback: continue_button, + disabled: continue_level === 1, + }, + ], + }); +}); + +let tileID = { + blank: 0, + wall: 1, +}; + +let creatureID = { + fish: 0, + shark: 1, +}; + +let creature_type_mapping = { + 0: creatureID.fish, + 1: creatureID.fish, + 2: creatureID.fish, + 3: creatureID.fish, + 4: creatureID.fish, + 5: creatureID.shark, +}; + +/* ------ timers & static timer values --------- */ + +const STAND_FRAME_LENGTH = 500; +const SWIM_FRAME_LENGTH = 200; + +const SELECTOR_FRAME_LENGTH = 300; + +const BLOCK_MOVE_SPEED = 12; + +const SWIM_SPEED = 6; +const LOSE_SWIM_SPEED = 8; +const WIN_SWIM_SPEED = 5; + +const X_FRAME_LENGTH = 100; +const MAX_X_FRAME = 12; + +const BUBBLE_FRAME_LENGTH = 300; +const BUBBLE_UP_SPEED = 15; + +const SCREEN_MESSAGE_DELAY = 600; +const SCREEN_MESSAGE_FADEIN_LENGTH = 1000; + +const DRIFT_SPEED = 3000; + +/* ------- game global state -------- */ + +let mouse_x = -1, mouse_y = -1, mouse_is_down = false; + +let fish = []; + +let blocks = []; + +let hearts = []; + +let bubbles = []; + +let decorations = []; + +let hovered_block = null; + +let hovered_fish = null; + +let eaten_fish = null; + +let start_drag_x = 0, start_drag_y = 0; + +let selector_timer = 0; +let selector_frame = 0; + +let potential_fish_target = null; +let fish_path = null; + +let x_timer = 0; +let x_frame = 0; + +let just_clicked_block = true; + +let screen_message_delay = 0; +let screen_message_alpha = 0; + +/* ------- game behavior functions -------- */ + +function tile_at(x, y) { + if (x < 0 || x >= game.level_w) return tileID.wall; + if (y < 0 || y >= game.level_h) return tileID.wall; + + let result = map[y * game.level_w + x]; + return result; +} + +function fish_at(x, y) { + let filtered_fish = fish.filter(o => o.x === x && o.y === y); + if (filtered_fish.length > 0) { + return filtered_fish[0]; + } + return null; +} + +function block_at(x, y) { + for (let b of blocks) { + for (let t of b.tiles) { + if (t[0] === x && t[1] === y) { + return true; + } + } + } + return false; +} + +function tile_or_block_at(x, y) { + return tile_at(x, y) === tileID.wall || block_at(x, y); +} + +function anything_at(x, y, ignore_block) { + if (tile_at(x, y) === tileID.wall) { + return true; + } + + for (let f of fish) { + if (f.x === x && f.y === y) { + return true; + } + } + + for (let b of blocks) { + if (ignore_block && ignore_block === b) { + continue; + } + for (let t of b.tiles) { + if (t[0] === x && t[1] === y) { + return true; + } + } + } + + return false; +} + +function anything_except_fish_at(x, y, ignore_block) { + if (tile_at(x, y) === tileID.wall) { + return true; + } + + for (let f of fish) { + if (f.x === x && f.y === y && f.species !== creatureID.fish) { + return true; + } + } + + for (let b of blocks) { + if (ignore_block && ignore_block === b) { + continue; + } + for (let t of b.tiles) { + if (t[0] === x && t[1] === y) { + return true; + } + } + } + + return false; +} + +function sofc(coords) { + return "" + coords[0] + "," + coords[1]; +} + +function cofs(string) { + return string.split(",").map(a => parseInt(a, 10)); +} + +function path_between(start_x, start_y, end_x, end_y, end_can_be_blocked, ignore_fish) { + /* Pathfinding, for fish */ + + if (anything_at(end_x, end_y) && end_can_be_blocked) { + console.log("very no"); + return null; + } + + let queue = [[start_x, start_y, null]]; + let checked = {}; + + while (queue.length > 0) { + let next_pos = queue.shift(); + + if (checked.hasOwnProperty(sofc(next_pos))) { + continue; + } + + checked[sofc(next_pos)] = next_pos[2]; + + if (next_pos[0] === end_x && next_pos[1] === end_y) { + console.log("yes"); + let path = []; + while (next_pos !== null) { + path.unshift([next_pos[0], next_pos[1]]); + next_pos = checked[sofc(next_pos)]; + } + return path; + } + + if ((next_pos[0] !== start_x || next_pos[1] !== start_y) + && (ignore_fish || anything_at(next_pos[0], next_pos[1])) + && (!ignore_fish || anything_except_fish_at(next_pos[0], next_pos[1]))) { + continue; + } + + queue.push([next_pos[0] + 1, next_pos[1], [next_pos[0], next_pos[1]]]); + queue.push([next_pos[0] - 1, next_pos[1], [next_pos[0], next_pos[1]]]); + queue.push([next_pos[0], next_pos[1] + 1, [next_pos[0], next_pos[1]]]); + queue.push([next_pos[0], next_pos[1] - 1, [next_pos[0], next_pos[1]]]); + } + + console.log("no"); + return null; +} + +let undo_stack = []; + +function create_undo_point() { + console.log("cup"); + + let blocks_copy = []; + for (let b of blocks) { + let new_block = { + state: 0, + tiles: [], + target_dx: 0, + target_dy: 0, + move_fraction: 0, + }; + for (let coord of b.tiles) { + new_block.tiles.push( [ coord[0], coord[1] ] ); + } + blocks_copy.push(new_block); + } + + undo_stack.push({ + fish: zb.copy_flat_objlist(fish), + blocks: blocks_copy, + }); +} + +function make_normal_state() { + game.state = State.STAND; + x_frame = 0; + hearts = []; + bubbles = []; + potential_fish_target = null; + hovered_fish = null; + screen_message_alpha = 0; + screen_message_delay = SCREEN_MESSAGE_DELAY; +} + +function undo() { + console.log("undo") + if (undo_stack.length > 0) { + let undo_point = undo_stack.pop(); + fish = undo_point.fish; + blocks = undo_point.blocks; + make_normal_state(); + } +} + +function reset() { + console.log("reset"); + create_undo_point(); + game.start_transition(zb.transition.type.fade, 300, function() { + load_level(); + }); +} + +function advance_level() { + if (!can_continue) return; + + undo_stack = []; + + if (level_number + 1 > Math.max(...Object.keys(levels).map(a => parseInt(a, 10) || 0))) { + game.long_transition(zb.transition.type.fade, 1000, function() { + reset_save(); + game.mode = 'youwon'; + }); + } else { + game.start_transition(zb.transition.type.fade, 1000, function() { + level_number ++; + load_level(); + make_normal_state(); + }); + } +} + +function skip_to(num) { + level_number = num; + load_level(); +} + +function win_everything() { + console.log("WIN!!"); +} + +function load_level() { + load_level_data(levels[level_number]); +} + +function load_level_data(lvl) { + map = lvl.map; + + fish = []; + blocks = []; + + make_normal_state(); + + decorations = []; + + let fish_type = zb.mod(level_number || 0, KINDS_OF_FISH); + if (isNaN(level_number)) { + fish_type = 0; + } + for (let f of lvl.fish) { + fish.push({ + x: f[0], + y: f[1], + type: fish_type, + species: creatureID.fish, + frame: 0, + timer: Math.random() * STAND_FRAME_LENGTH, + drift: Math.random(), + }); + fish_type ++; + fish_type %= KINDS_OF_FISH; + } + + for (let s of lvl.sharks) { + fish.push({ + x: s[0], + y: s[1], + type: KINDS_OF_FISH, + species: creatureID.shark, + frame: 0, + timer: Math.random() * STAND_FRAME_LENGTH, + drift: Math.random(), + }); + } + + for (let b of lvl.blocks) { + let new_block = { + state: 0, + tiles: [], + target_dx: 0, + target_dy: 0, + move_fraction: 0, + }; + for (let coord of b) { + new_block.tiles.push( [ coord[0], coord[1] ] ); + } + blocks.push(new_block); + } + + for (let d of lvl.decorations) { + decorations.push({ + x: d[0], + y: d[1], + type: d[2], + }); + } + + game.state = State.STAND; +} + +function save() { + console.log("Saving"); + let save_data = level_number; + if (game.state === State.WIN) { + save_data = level_number + 1; + } + game.save('continue_level', save_data); +} + +function reset_save() { + game.save('continue_level', 1); +} + +function win() { + //game.sfx.victory.play(); + console.log("You won!"); + game.state = State.WIN; + game.sfx.victory.play(); + + let heart_x = 0; + let heart_y = 0; + for (let f of fish) { + if (f.species === creatureID.fish) { + f.hidden = true; + heart_x = f.x; + heart_y = f.y; + } + } + + for (let i = 0; i < 20; i++) { + create_heart(heart_x * game.tile_size + game.tile_size / 2 + Math.random() * 10 - 5, + heart_y * game.tile_size + game.tile_size / 2 + Math.random() * 10 - 5); + } + + save(); + + window.setTimeout(function() { + can_continue = true; + }, 350); +} + +function lose() { + game.state = State.LOSE; + game.sfx.fail.play(); + x_timer = 0; + x_frame = 0; + eaten_fish.hidden = true; +} + +function check_status() { + /* Check lose: a shark and a fish in the same area */ + for (let f of fish) { + if (f.species === creatureID.shark) { + for (let g of fish) { + if (g.species === creatureID.fish) { + let path = path_between(f.x, f.y, g.x, g.y, false); + if (path) { + game.state = State.LOSESWIM; + hovered_fish = f; + eaten_fish = g; + fish_path = path; + start_swimming(hovered_fish); + break; + } + } + } + if (game.state === State.LOSESWIM) break; + } + } + + /* Check win: all fish in same area */ + /* find first fish and see if it paths to all others */ + let first_fish = null; + for (let f of fish) { + if (f.species === creatureID.fish) { + first_fish = f; + break; + } + } + + let won = true; + for (let f of fish) { + if (f === first_fish) continue; + if (f.species !== creatureID.fish) continue; + let path = path_between(first_fish.x, first_fish.y, f.x, f.y, false, true); + if (path === null) { + won = false; + break; + } + } + + if (won) { + console.log("omg win"); + game.state = State.CONGREGATE; + /* DFS to find spot closest to all fish */ + for (let f of fish) { + if (f.species !== creatureID.fish) continue; + f.explored_territory = new Set(); + f.explored_territory.add(sofc([f.x, f.y])); + } + + let found_thing = false; + let common_square = null; + let bailout = 0; + while (!found_thing) { + for (let f of fish) { + if (f.species !== creatureID.fish) continue; + let new_explored_territory = new Set(); + for (let e of f.explored_territory) { + let coords = cofs(e); + let neighbors = [ + [coords[0] + 1, coords[1]], + [coords[0] - 1, coords[1]], + [coords[0], coords[1] + 1], + [coords[0], coords[1] - 1], + ]; + for (let n of neighbors) { + if (n[0] < 0 || n[0] >= game.level_w || n[1] < 0 || n[1] >= game.level_h) continue; + if (f.explored_territory.has(sofc(n))) continue; + if (anything_except_fish_at(n[0], n[1])) continue; + new_explored_territory.add(sofc(n)); + } + } + f.explored_territory = f.explored_territory.union(new_explored_territory); + } + + let common_squares = fish[0].explored_territory; + for (let f of fish) { + if (f.species !== creatureID.fish) continue; + common_squares = common_squares.intersection(f.explored_territory); + } + + bailout ++; + if (bailout > 1000) found_thing = true; + + if (common_squares.size > 0) { + common_square = cofs(Array.from(common_squares)[0]); + found_thing = true; + } + } + + if (common_square === null) { + console.error("Assert this should not happen!"); + } + + /* ok, now we found where all the fish will swim to. and we know they all have a path. so figure out the path for each fish */ + let max_win_path_length = 0; + for (let f of fish) { + if (f.species !== creatureID.fish) continue; + f.win_path = path_between(f.x, f.y, common_square[0], common_square[1], false, true); + if (f.win_path.length > max_win_path_length) { + max_win_path_length = f.win_path.length; + } + } + + /* now we want them to reach it at the same time. so scale each one's speed proportionately to their path length */ + for (let f of fish) { + if (f.species !== creatureID.fish) continue; + f.win_swim_speed = WIN_SWIM_SPEED * (f.win_path.length - 1) / (max_win_path_length - 1); + start_swimming(f); + } + } +} + +function start_swimming(fish) { + fish.target_x = fish.x; + fish.target_y = fish.y; + fish.swimming = true; + fish.move_fraction = 1; +} + +function create_heart(x, y) { + hearts.push({ + x: Math.round(x), + y: Math.round(y), + xspeed: Math.random() * 60 - 30, + yspeed: Math.random() * -30 - 30, + gravity: 50, + }); +} + +/* ---------- update functions ------------ */ + +/* MAIN UPDATE FUNCTION */ +function do_update(delta) { + update_bubbles(delta); + + if (game.state === State.WIN || game.state === State.LOSE) { + if (screen_message_delay > 0) { + screen_message_delay -= delta; + } else { + if (screen_message_alpha < 1) { + screen_message_alpha += delta / SCREEN_MESSAGE_FADEIN_LENGTH; + } else { + screen_message_alpha = 1; + } + } + } + + for (let f of fish) { + f.drift += delta / DRIFT_SPEED; + + f.timer += delta; + + let max_time = STAND_FRAME_LENGTH; + if (f.swimming) { + max_time = SWIM_FRAME_LENGTH; + } + while (f.timer > max_time) { + f.timer -= max_time; + f.frame ++; + f.frame %= 2; + } + } + + selector_timer += delta; + while (selector_timer > SELECTOR_FRAME_LENGTH) { + selector_timer -= SELECTOR_FRAME_LENGTH; + selector_frame ++; + selector_frame %= 2; + } + + if (game.state === State.DRAG) { + hovered_block.move_fraction += BLOCK_MOVE_SPEED * delta / 1000; + if (hovered_block.move_fraction > 1) { + for (let t of hovered_block.tiles) { + t[0] += hovered_block.target_dx; + t[1] += hovered_block.target_dy; + } + hovered_block.move_fraction = 0; + start_drag_x += hovered_block.target_dx; + start_drag_y += hovered_block.target_dy; + hovered_block.target_dx = 0; + hovered_block.target_dy = 0; + if (mouse_is_down) { + game.state = State.BLOCK; + } else { + game.state = State.STAND; + } + + /* Check if we won or lost */ + check_status(); + + if (game.state !== State.CONGREGATE && game.state !== State.LOSESWIM) { + /* Check if we need to move more */ + if (mouse_is_down) { + handle_mousemove(game, {}, mouse_x, mouse_y); + } + } else { + hovered_block = null; + for (let b of blocks) { + b.state = 0; + } + } + } + } else if (game.state === State.SWIM || game.state === State.LOSESWIM) { + let speed = SWIM_SPEED; + + if (game.state === State.LOSESWIM) { + speed = LOSE_SWIM_SPEED; + } + + let done = update_fish_swim(delta, hovered_fish, fish_path, speed); + + if (done) { + if (game.state === State.SWIM) { + game.state = State.STAND; + } else if (game.state === State.LOSESWIM) { + lose(); + } + if (game.state === State.SWIM) { + fish = null; + } + potential_fish_target = null; + handle_mousemove(game, {}, mouse_x, mouse_y); + } + } else if (game.state === State.CONGREGATE) { + let all_done = true; + for (let f of fish) { + if (f.species === creatureID.fish) { + let done = update_fish_swim(delta, f, f.win_path, f.win_swim_speed); + if (f.win_path.length > 0) { + all_done = false; + } + } + } + + if (all_done) { + win(); + } + } else if (game.state === State.LOSE) { + if (x_frame < MAX_X_FRAME) { + x_timer += delta; + while (x_timer > X_FRAME_LENGTH) { + x_timer -= X_FRAME_LENGTH; + x_frame ++; + } + } + } else if (game.state === State.WIN) { + update_hearts(delta); + } +} + +function update_fish_swim(delta, fish, path, speed) { + fish.move_fraction += speed * delta / 1000; + if (fish.move_fraction > 1) { + fish.x = fish.target_x; + fish.y = fish.target_y; + fish.move_fraction = 0; + if (path.length > 0 && path[0][0] === fish.x && path[0][1] === fish.y) { + /* Pop off start coordinate from path... */ + path.shift(); + } + + if (path.length == 0) { + console.log("Swim done"); + if (game.state === State.SWIM) { + fish.rotate = 0; + } + fish.swimming = false; + return true; + } else { + fish.target_x = path[0][0]; + fish.target_y = path[0][1]; + + let old_rotate = fish.rotate; + let old_hflip = fish.hflip; + + if (fish.target_x > fish.x) { + fish.hflip = false; + } else if (fish.target_x < fish.x) { + fish.hflip = true; + } + + if (fish.target_y > fish.y) { + fish.rotate = 90; + } else if (fish.target_y < fish.y) { + fish.rotate = -90; + } else { + fish.rotate = 0; + } + + if (fish.rotate !== old_rotate || fish.hflip !== old_hflip) { + game.sfx.drip.play(); + } + } + } + return false; +} + +function update_hearts(delta) { + for (let h of hearts) { + h.x += h.xspeed * delta / 1000; + h.y += h.yspeed * delta / 1000; + h.yspeed += h.gravity * delta / 1000; + } +} + +let bubble_sfx_timeout = 0; +function create_bubble(x, y) { + bubbles.push({ + x: x, + y: y, + frame: Math.floor(Math.random() * 2), + timer: Math.random() * BUBBLE_FRAME_LENGTH, + }); + + if (Math.random() < 0.8 && bubble_sfx_timeout <= 0) { + let decider = Math.random(); + if (decider < 0.25) { + game.sfx.bubble.play(); + } else if (decider < 0.5) { + game.sfx.bubble2.play(); + } else if (decider < 0.75) { + game.sfx.bubble3.play(); + } else { + game.sfx.bubble4.play(); + } + bubble_sfx_timeout = 800; + } +} + +function update_bubbles(delta) { + bubble_sfx_timeout -= delta; + + for (let f of fish) { + if (!f.hasOwnProperty("bubble_timeout")) { + f.bubble_timeout = 0; + } + f.bubble_timeout -= delta; + + if (f.swimming) continue; + if (f.hidden) continue; + if (f.bubble_timeout > 0) continue; + + if (Math.random() < 0.002) { + let x_offset = 0; + + if (f.species === creatureID.fish) { + if (f.hflip) { + x_offset = 6; + } else { + x_offset = 18; + } + } else if (f.species === creatureID.shark) { + if (f.hflip) { + x_offset = 3; + } else { + x_offset = 21; + } + } + + create_bubble(f.x * game.tile_size + x_offset, f.y * game.tile_size + game.tile_size / 2); + + f.bubble_timeout = 1500; + } + } + + for (let d of decorations) { + if (!d.hasOwnProperty("bubble_timeout")) { + d.bubble_timeout = 0; + } + d.bubble_timeout -= delta; + + if (d.bubble_timeout > 0) continue; + if (tile_at(d.x, d.y) === tileID.wall) continue; + + if (Math.random() < 0.001) { + let x_offset = Math.random() * game.tile_size * 2 / 3 - game.tile_size / 3; + + create_bubble(d.x * game.tile_size + game.tile_size / 2 + x_offset, d.y * game.tile_size + game.tile_size / 2); + + d.bubble_timeout = 3000; + } + } + + for (let b of bubbles) { + b.timer += delta; + while (b.timer > BUBBLE_FRAME_LENGTH) { + b.timer -= BUBBLE_FRAME_LENGTH; + b.frame ++; + b.frame %= 2; + } + b.y -= BUBBLE_UP_SPEED * delta / 1000; + if (zb.mod(b.y, game.tile_size) < game.tile_size / 2) { + if (tile_or_block_at(Math.floor(b.x / game.tile_size), Math.floor(b.y / game.tile_size))) { + b.deleteme = true; + } + } + } + + bubbles = bubbles.filter(b => !b.deleteme); +} + +/* ---------- draw functions ----------- */ + +/* DRAW */ +function do_draw(ctx) { + draw_bubbles(ctx); + + draw_map(ctx); + + if (!isNaN(parseInt(level_number))) { + zb.sprite_draw(ctx, game.img.levelnums, game.tile_size, game.tile_size, 0, level_number - 1, 0, (game.level_h - 1) * game.tile_size); + } + + draw_decorations(ctx); + + zb.buttons.draw(ctx, game.buttons.ingame); + + if (game.img.levels.hasOwnProperty(level_number)) { + zb.screen_draw(ctx, game.img.levels[level_number]); + } + + draw_blocks(ctx); + + draw_creatures(ctx); + + draw_hearts(ctx); + + if (potential_fish_target) { + zb.sprite_draw(ctx, game.img.selector, 28, 28, 0, zb.mod(selector_frame + 1, 2), + potential_fish_target[0] * game.tile_size - 2, potential_fish_target[1] * game.tile_size - 2); + } + + if (game.state === State.LOSE) { + zb.sprite_draw(ctx, game.img.x, 48, 48, 0, x_frame, hovered_fish.x * game.tile_size - 12, hovered_fish.y * game.tile_size - 12); + } + + if (game.state === State.WIN || game.state === State.LOSE) { + ctx.save(); + ctx.globalAlpha = screen_message_alpha; + if (game.state === State.WIN) { + zb.screen_draw(ctx, game.img.success); + } else { + zb.screen_draw(ctx, game.img.failure); + } + ctx.restore(); + } +} + +function do_draw_menu(ctx) { + zb.screen_draw(ctx, game.img.menuimage); + zb.buttons.draw(ctx, game.buttons.menu); +} + +function do_draw_youwon(ctx) { + zb.screen_draw(ctx, game.img.youwon); +} + +function rpgmaker_tile_format_thing(x, y, tile_check_func) { + let u = tile_check_func(x, y - 1); + let d = tile_check_func(x, y + 1); + let l = tile_check_func(x - 1, y); + let r = tile_check_func(x + 1, y); + let ul = tile_check_func(x - 1, y - 1); + let ur = tile_check_func(x + 1, y - 1); + let dl = tile_check_func(x - 1, y + 1); + let dr = tile_check_func(x + 1, y + 1); + let ulx, uly, urx, ury, dlx, dly, drx, dry; + + /* Draw top left */ + if (u) { + if (l) { + if (ul) { + /* Open water */ + ulx = 2; uly = 4; + } else { + /* Upper left corner */ + ulx = 2; uly = 0; + } + } else { + /* Left border */ + ulx = 0; uly = 4; + } + } else { + if (l) { + /* Top border */ + ulx = 2; uly = 2; + } else { + /* Very corner */ + ulx = 0; uly = 2; + } + } + + /* Draw top right */ + if (u) { + if (r) { + if (ur) { + /* Open water */ + urx = 1; ury = 4; + } else { + /* Upper right corner */ + urx = 3; ury = 0; + } + } else { + /* Right border */ + urx = 3; ury = 4; + } + } else { + if (r) { + /* Top border */ + urx = 1; ury = 2; + } else { + /* Very corner */ + urx = 3; ury = 2; + } + } + + /* Draw bottom left */ + if (d) { + if (l) { + if (dl) { + /* Open water */ + dlx = 2; dly = 3; + } else { + /* Bottom left corner */ + dlx = 2; dly = 1; + } + } else { + /* Left border */ + dlx = 0; dly = 3; + } + } else { + if (l) { + /* Bottom border */ + dlx = 2; dly = 5; + } else { + /* Very corner */ + dlx = 0; dly = 5; + } + } + + /* Draw bottom right */ + if (d) { + if (r) { + if (dr) { + /* Open water */ + drx = 1; dry = 3; + } else { + /* Bottom right corner */ + drx = 3; dry = 1; + } + } else { + /* Right border */ + drx = 3; dry = 3; + } + } else { + if (r) { + /* Bottom border */ + drx = 1; dry = 5; + } else { + /* Very corner */ + drx = 3; dry = 5; + } + } + + return { ulx: ulx, uly: uly, urx: urx, ury: ury, dlx: dlx, dly: dly, drx: drx, dry: dry }; +} + +function draw_rpgmaker_tile(ctx, img, x, y, rpgmaker_tft, state) { + let ts = game.tile_size; + let { ulx, uly, urx, ury, dlx, dly, drx, dry } = rpgmaker_tft; + zb.sprite_draw(ctx, img, ts / 2, ts / 2, ulx, uly + state * 6, x, y); + zb.sprite_draw(ctx, img, ts / 2, ts / 2, urx, ury + state * 6, x + ts / 2, y); + zb.sprite_draw(ctx, img, ts / 2, ts / 2, dlx, dly + state * 6, x, y + ts / 2); + zb.sprite_draw(ctx, img, ts / 2, ts / 2, drx, dry + state * 6, x + ts / 2, y + ts / 2); +} + +function draw_map(ctx) { + for (let y = 0; y < game.level_h; y++) { + for (let x = 0; x < game.level_w; x++) { + if (tile_at(x, y) === tileID.wall) { + let tileinfo = rpgmaker_tile_format_thing(x, y, (x, y) => tile_at(x, y) === tileID.wall); + draw_rpgmaker_tile(ctx, game.img.wall_tile, x * game.tile_size, y * game.tile_size, tileinfo, 0); + } + } + } +} + +function draw_blocks(ctx) { + for (let block of blocks) { + for (let tile of block.tiles) { + let tileinfo = rpgmaker_tile_format_thing(tile[0], tile[1], (x, y) => { + for (let tile of block.tiles) { + if (tile[0] === x && tile[1] === y) { + return true; + } + } + return false; + }); + draw_rpgmaker_tile(ctx, game.img.move_tile, + tile[0] * game.tile_size + Math.round(block.target_dx * game.tile_size * block.move_fraction), + tile[1] * game.tile_size + Math.round(block.target_dy * game.tile_size * block.move_fraction), + tileinfo, block.state); + } + } +} + +function draw_decorations(ctx) { + for (let d of decorations) { + ctx.save(); + if (d.type === 2) { + ctx.translate(0, 1); + } + zb.sprite_draw(ctx, game.img.decorations, game.tile_size, game.tile_size, d.type, 0, d.x * game.tile_size, d.y * game.tile_size); + ctx.restore(); + } +} + +function draw_creatures(ctx) { + for (let f of fish) { + if (f.hidden) continue; + + ctx.save(); + + let mf = f.move_fraction || 0; + let tx = f.target_x !== undefined ? f.target_x : f.x; + let ty = f.target_y !== undefined ? f.target_y : f.y; + + let drift_y_offset = Math.round(1.9 * Math.sin(f.drift * 2 * Math.PI)); + + ctx.translate(f.x * game.tile_size * (1 - mf) + tx * game.tile_size * mf, + f.y * game.tile_size * (1 - mf) + ty * game.tile_size * mf); + + if (f.hflip) { + ctx.translate(game.tile_size, 0); + ctx.scale(-1, 1); + } + + if (f.vflip) { + ctx.translate(0, game.tile_size); + ctx.scale(1, -1); + } + + if (f.rotate) { + ctx.translate(game.tile_size / 2, game.tile_size / 2); + ctx.rotate(f.rotate * Math.PI / 180); + ctx.translate(-game.tile_size / 2, -game.tile_size / 2); + } + + zb.sprite_draw(ctx, game.img.fish, game.tile_size, game.tile_size, f.type, f.frame, 0, drift_y_offset); + if (f === hovered_fish && (game.state === State.STAND || game.state === State.FISH)) { + zb.sprite_draw(ctx, game.img.selector, 28, 28, 0, selector_frame, -2, -2); + } + + ctx.restore(); + } +} + +function draw_bubbles(ctx) { + for (let b of bubbles) { + zb.sprite_draw(ctx, game.img.bubble, 5, 11, 0, b.frame, Math.round(b.x - game.img.bubble.width / 2), Math.round(b.y - game.img.bubble.height / 2)); + } +} + +function draw_hearts(ctx) { + for (let h of hearts) { + ctx.drawImage(game.img.heart, Math.round(h.x - game.img.heart.width / 2), Math.round(h.y - game.img.heart.height / 2)); + } +} + +/* ---------- event handlers ------------ */ + +function handle_gamestart(game) { + console.log("Game start!"); + + //game.music.bgm_menu.play(); +} + +function handle_mousedown(game, e, x, y) { + mouse_x = x; + mouse_y = y; + mouse_is_down = true; + + if (game.mode === 'menu') { + if (zb.buttons.click(game.buttons.menu, x, y)) return; + } else if (game.mode === 'game') { + if (zb.buttons.click(game.buttons.ingame, x, y)) return; + + let tile_x = Math.floor(mouse_x / game.tile_size); + let tile_y = Math.floor(mouse_y / game.tile_size); + + if (game.mode === 'game') { + //if (zb.buttons.click(buttons.ingame, x, y)) return; + + if (game.state === State.WIN) { + return; + } + + if (game.state === State.STAND) { + if (hovered_block) { + hovered_block.state = 2; + game.state = State.BLOCK; + start_drag_x = tile_x; + start_drag_y = tile_y; + } + } + } + } +} + +function handle_mouseup(game, e, x, y) { + mouse_is_down = false; + just_clicked_block = true; + + if (game.mode === 'youwon') { + game.long_transition(zb.transition.type.fade, 1000, function() { + continue_level = 1; + game.buttons.menu.buttons[1].disabled = true; + game.mode = 'menu'; + }); + } else if (game.mode === 'menu') { + zb.buttons.update(game.buttons.menu, x, y); + } else if (game.mode === 'game') { + zb.buttons.update(game.buttons.ingame, x, y); + + if (game.state === State.WIN) { + advance_level(); + } else if (game.state === State.BLOCK) { + game.state = State.STAND; + handle_mousemove(game, e, x, y); + } else if (game.state === State.FISH) { + if (potential_fish_target === null) { + game.state = State.STAND; + handle_mousemove(game, e, x, y); + } else { + create_undo_point(); + game.state = State.SWIM; + start_swimming(hovered_fish); + } + } else if (game.state === State.STAND) { + if (hovered_fish) { + game.state = State.FISH; + } + } + + if (won_everything) { + return; + } + } +} + +function handle_mouseleave(game, e) { + mouse_is_down = false; + if (game.mode === 'game') { + if (game.state === State.STAND || game.state === State.BLOCK) { + handle_mouseup(game, e, -1, -1); + } + } +} + +function handle_mousemove(game, e, x, y) { + mouse_x = x; + mouse_y = y; + + let tile_x = Math.floor(mouse_x / game.tile_size); + let tile_y = Math.floor(mouse_y / game.tile_size); + + if (game.mode === 'menu') { + zb.buttons.update(game.buttons.menu, mouse_x, mouse_y); + } else if (game.mode === 'game') { + zb.buttons.update(game.buttons.ingame, mouse_x, mouse_y); + + if (game.state === State.STAND) { + hovered_block = null; + for (let b of blocks) { + b.state = 0; + for (let t of b.tiles) { + if (t[0] === tile_x && t[1] === tile_y) { + b.state = 1; + hovered_block = b; + break; + } + } + } + + hovered_fish = fish_at(tile_x, tile_y); + } else if (game.state === State.FISH) { + potential_fish_target = null; + fish_path = path_between(hovered_fish.x, hovered_fish.y, tile_x, tile_y, true); + if (fish_path) { + potential_fish_target = [tile_x, tile_y]; + } + } else if (game.state === State.BLOCK) { + if (tile_x !== start_drag_x || tile_y !== start_drag_y) { + /* Move block */ + let dx = 0; + let dy = 0; + hovered_block.target_dx = 0; + hovered_block.target_dy = 0; + if (tile_x > start_drag_x) { + dx = 1; + } else if (tile_x < start_drag_x) { + dx = -1; + } else if (tile_y > start_drag_y) { + dy = 1; + } else if (tile_y < start_drag_y) { + dy = -1; + } + + let free_to_move = true; + let mouse_inside_block = false; + for (let t of hovered_block.tiles) { + if (anything_at(t[0] + dx, t[1] + dy, hovered_block)) { + free_to_move = false; + } + + if (t[0] === tile_x && t[1] === tile_y) { + mouse_inside_block = true; + } + } + + if (free_to_move) { + if (just_clicked_block) { + create_undo_point(); + just_clicked_block = false; + } + hovered_block.move_fraction = 0; + hovered_block.target_dx = dx; + hovered_block.target_dy = dy; + game.state = State.DRAG; + game.sfx.drag.play(); + } else if (mouse_inside_block) { + start_drag_x = tile_x; + start_drag_y = tile_y; + } + } + } + } +} + +function toggle_mute_button() { + if (game.buttons.ingame.buttons[2].id == 2) { + game.buttons.ingame.buttons[2].id = 3; + } else { + game.buttons.ingame.buttons[2].id = 2; + } + game.toggle_mute(); +} + +function handle_keyup(game, e) { + if (game.transition.is_transitioning) return; + + // key up + switch (e.key) { + case 'm': + toggle_mute_button(); + e.preventDefault(); + break; + case 'r': + reset(); + e.preventDefault(); + break; + case 'z': + undo(); + e.preventDefault(); + break; + } +} + +function new_game_button() { + game.music.bgm.play(); + game.start_transition(zb.transition.type.fade, 1000, function() { + game.mode = 'game'; + level_number = 1; + load_level(); + }); +} + +function continue_button() { + game.music.bgm.play(); + game.start_transition(zb.transition.type.fade, 1000, function() { + game.mode = 'game'; + level_number = continue_level; + load_level(); + }); +} diff --git a/packaging/img/bubble.png b/packaging/img/bubble.png new file mode 100644 index 0000000..d601231 Binary files /dev/null and b/packaging/img/bubble.png differ diff --git a/packaging/img/buttons.png b/packaging/img/buttons.png new file mode 100644 index 0000000..86d6565 Binary files /dev/null and b/packaging/img/buttons.png differ diff --git a/packaging/img/decoration.png b/packaging/img/decoration.png new file mode 100644 index 0000000..89582fd Binary files /dev/null and b/packaging/img/decoration.png differ diff --git a/packaging/img/failure.png b/packaging/img/failure.png new file mode 100644 index 0000000..967da35 Binary files /dev/null and b/packaging/img/failure.png differ diff --git a/packaging/img/fish.png b/packaging/img/fish.png new file mode 100644 index 0000000..ad1235a Binary files /dev/null and b/packaging/img/fish.png differ diff --git a/packaging/img/heart.png b/packaging/img/heart.png new file mode 100644 index 0000000..143ece0 Binary files /dev/null and b/packaging/img/heart.png differ diff --git a/packaging/img/level/1.png b/packaging/img/level/1.png new file mode 100644 index 0000000..bffdafb Binary files /dev/null and b/packaging/img/level/1.png differ diff --git a/packaging/img/level/2.png b/packaging/img/level/2.png new file mode 100644 index 0000000..01c3882 Binary files /dev/null and b/packaging/img/level/2.png differ diff --git a/packaging/img/level/4.png b/packaging/img/level/4.png new file mode 100644 index 0000000..9e70f32 Binary files /dev/null and b/packaging/img/level/4.png differ diff --git a/packaging/img/levelnums.png b/packaging/img/levelnums.png new file mode 100644 index 0000000..7107a49 Binary files /dev/null and b/packaging/img/levelnums.png differ diff --git a/packaging/img/menubuttons.png b/packaging/img/menubuttons.png new file mode 100644 index 0000000..9165a92 Binary files /dev/null and b/packaging/img/menubuttons.png differ diff --git a/packaging/img/menuimage.png b/packaging/img/menuimage.png new file mode 100644 index 0000000..5623295 Binary files /dev/null and b/packaging/img/menuimage.png differ diff --git a/packaging/img/move_tile.png b/packaging/img/move_tile.png new file mode 100644 index 0000000..0cc83ae Binary files /dev/null and b/packaging/img/move_tile.png differ diff --git a/packaging/img/selector.png b/packaging/img/selector.png new file mode 100644 index 0000000..6b74208 Binary files /dev/null and b/packaging/img/selector.png differ diff --git a/packaging/img/success.png b/packaging/img/success.png new file mode 100644 index 0000000..025419c Binary files /dev/null and b/packaging/img/success.png differ diff --git a/packaging/img/wall_tile.png b/packaging/img/wall_tile.png new file mode 100644 index 0000000..8ac813d Binary files /dev/null and b/packaging/img/wall_tile.png differ diff --git a/packaging/img/x.png b/packaging/img/x.png new file mode 100644 index 0000000..07171aa Binary files /dev/null and b/packaging/img/x.png differ diff --git a/packaging/img/youwon.png b/packaging/img/youwon.png new file mode 100644 index 0000000..7471f14 Binary files /dev/null and b/packaging/img/youwon.png differ diff --git a/packaging/index.html b/packaging/index.html new file mode 100644 index 0000000..1ae5796 --- /dev/null +++ b/packaging/index.html @@ -0,0 +1,19 @@ + + + + fish shark game + + + + + + + + + + + diff --git a/packaging/levels.js b/packaging/levels.js new file mode 100644 index 0000000..7796217 --- /dev/null +++ b/packaging/levels.js @@ -0,0 +1,13 @@ +var levels={ + 10: { map: [0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,1,1,1,1,1,0,0,0,0,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[0,3], ],[[2,3], [2,4], [1,3], [1,4], ],[[3,3], [3,4], [4,3], ],[[4,4], ],[[1,6], [1,7], [2,6], [2,7], ],[[3,7], [4,6], [4,7], [3,6], ],], fish: [[3,1], [1,9], ], sharks: [[5,5], ], decorations: [[6,0,0], [4,1,2], [5,1,2], [7,1,0], [6,2,0], [5,7,3], [6,7,3], [4,9,1], [6,9,1], [1,10,2], [2,10,2], [3,10,2], [5,10,1], [7,10,1], ] }, + 1: { map: [0,0,0,0,0,0,0,1,1,1,1,1,0,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0,1,1,1,1,0,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,], blocks: [[[4,3], [5,3], [4,2], [5,2], ],[[4,4], [4,5], ],[[4,6], [3,5], [3,6], ],[[5,5], [6,5], ],[[5,7], ],], fish: [[2,0], [1,7], [6,8], ], sharks: [], decorations: [[0,0,2], [1,0,2], [7,2,1], [0,8,2], [1,8,2], [4,8,1], ] }, + 2: { map: [0,0,1,0,0,0,0,1,0,1,1,0,0,0,0,1,0,1,0,0,0,0,1,1,0,1,0,0,0,0,1,1,0,0,1,1,0,0,1,1,0,1,1,1,0,0,0,1,0,1,1,1,0,0,1,1,0,0,0,0,0,0,1,1,0,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[4,0], [5,0], [5,1], [4,1], ],[[0,1], [0,2], [0,3], [0,4], ],[[4,3], [5,3], [4,2], [5,2], ],[[1,4], ],[[4,4], [5,4], [4,5], ],[[5,5], [6,5], ],], fish: [[1,0], [6,1], ], sharks: [], decorations: [[2,3,2], [3,3,2], [0,8,2], [6,9,0], [5,10,0], [7,10,0], ] }, + 3: { map: [1,1,0,0,0,0,1,1,0,1,0,0,0,1,1,1,0,0,0,0,0,1,0,1,0,0,0,0,0,1,0,1,0,0,0,1,1,1,0,1,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[3,1], [4,1], [2,1], ],[[4,2], [4,3], ],[[0,7], [1,5], [1,4], [0,6], [0,5], [1,6], ],[[6,4], [6,5], ],[[6,6], [5,5], [5,6], ],], fish: [[5,0], [6,3], [0,9], ], sharks: [], decorations: [[7,0,1], [2,4,2], [5,8,2], [4,9,2], [7,9,1], [6,10,1], ] }, + 4: { map: [0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,], blocks: [[[4,0], [6,1], [5,1], [5,0], [6,0], [4,1], ],[[4,3], [5,3], [4,2], [5,2], ],[[4,4], [3,4], [5,4], [0,3], [2,3], [3,3], [1,3], ],], fish: [[3,1], [2,6], ], sharks: [[6,3], ], decorations: [[1,0,0], [3,7,3], [4,7,3], [5,7,3], ] }, + 5: { map: [0,0,0,0,1,1,1,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,1,0,0,0,0,1,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[3,5], ],[[4,5], [4,6], [3,6], ],[[3,7], [3,8], [3,9], ],], fish: [[5,1], [6,1], [5,2], [6,2], [5,3], [6,3], [5,4], [6,4], [6,5], [4,8], ], sharks: [[1,2], ], decorations: [[4,0,1], [7,6,1], [6,8,1], [4,9,3], [0,10,3], [7,10,1], ] }, + 6: { map: [0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[2,3], [0,3], [1,3], [3,3], ],[[2,7], [3,7], [2,6], [3,6], [2,5], ],[[5,5], [5,6], [6,5], ],[[4,8], [4,7], [5,7], [5,8], ],], fish: [[2,0], [2,2], ], sharks: [[6,7], [0,10], ], decorations: [[0,0,2], [1,0,2], [7,1,0], [2,10,3], [3,10,3], [5,10,1], ] }, + 7: { map: [1,0,0,1,0,0,1,1,0,0,0,1,0,0,0,1,0,1,1,1,1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,1,1,1,0,0,0,0,1,1,0,0,0,1,0,0,0,1,0,1,1,1,1,1,0,1,0,0,0,1,0,0,0,1,1,0,0,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[1,3], [1,4], ],[[2,4], [2,5], ],[[4,4], [4,5], ],[[5,4], [5,5], ],], fish: [[2,1], [4,1], [2,8], [4,8], ], sharks: [[3,4], [3,5], ], decorations: [[1,1,2], [5,1,2], [3,3,0], [3,6,0], [1,9,3], [2,9,3], [4,9,3], [5,9,3], ] }, + 8: { map: [1,1,0,0,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[6,1], [6,2], [5,0], [5,1], ],[[0,2], [1,2], [0,3], ],[[2,3], [1,3], [1,4], ],[[2,4], [2,5], [3,5], ],[[3,7], [4,6], [5,6], [3,6], ],[[3,8], [1,8], [2,9], [3,9], [2,8], ],[[0,9], [0,10], ],], fish: [[6,0], [5,7], [1,9], ], sharks: [[2,2], [5,4], [1,5], ], decorations: [[6,3,2], [4,8,3], [6,8,0], [5,9,0], [6,9,1], [5,10,1], [6,10,0], ] }, + 9: { map: [0,0,0,0,0,0,1,1,0,0,0,0,1,0,1,1,1,1,1,1,1,0,1,1,1,0,0,0,1,0,1,1,1,0,1,0,1,0,1,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,], blocks: [[[5,3], [5,4], [5,5], ],[[2,5], [2,6], [1,5], ],[[4,6], [5,6], [3,5], [3,6], ],[[1,10], ],], fish: [[0,0], [1,0], [0,1], [1,1], [0,5], ], sharks: [[1,6], [1,7], [2,7], [3,7], [4,7], [5,7], [1,8], [2,8], [5,8], [1,9], [2,9], [3,9], [4,9], [5,9], ], decorations: [[6,0,1], [7,0,0], [2,1,2], [3,1,2], [6,3,0], [2,4,0], [6,4,1], [6,5,0], [7,6,0], [7,7,1], [6,8,1], [6,9,0], [7,9,1], [6,11,0], [7,11,1], ] }, + test: { map: [1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,0,0,1,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,], blocks: [[[5,3], [5,4], [4,4], [4,3], ],], fish: [[3,2], [3,6], [3,9], ], sharks: [], decorations: [] }, +} diff --git a/packaging/loading.png b/packaging/loading.png new file mode 100644 index 0000000..9552ba1 Binary files /dev/null and b/packaging/loading.png differ diff --git a/packaging/music/sharkgamebeachmusic.mp3 b/packaging/music/sharkgamebeachmusic.mp3 new file mode 100755 index 0000000..6390123 Binary files /dev/null and b/packaging/music/sharkgamebeachmusic.mp3 differ diff --git a/packaging/saveerror.png b/packaging/saveerror.png new file mode 100644 index 0000000..ab259b9 Binary files /dev/null and b/packaging/saveerror.png differ diff --git a/packaging/sfx/bubble.wav b/packaging/sfx/bubble.wav new file mode 100644 index 0000000..c8a37fa Binary files /dev/null and b/packaging/sfx/bubble.wav differ diff --git a/packaging/sfx/bubble2.wav b/packaging/sfx/bubble2.wav new file mode 100644 index 0000000..7b173be Binary files /dev/null and b/packaging/sfx/bubble2.wav differ diff --git a/packaging/sfx/bubble3.wav b/packaging/sfx/bubble3.wav new file mode 100644 index 0000000..d640ee8 Binary files /dev/null and b/packaging/sfx/bubble3.wav differ diff --git a/packaging/sfx/bubble4.wav b/packaging/sfx/bubble4.wav new file mode 100644 index 0000000..e137be1 Binary files /dev/null and b/packaging/sfx/bubble4.wav differ diff --git a/packaging/sfx/drag.wav b/packaging/sfx/drag.wav new file mode 100644 index 0000000..89fb719 Binary files /dev/null and b/packaging/sfx/drag.wav differ diff --git a/packaging/sfx/drip.wav b/packaging/sfx/drip.wav new file mode 100644 index 0000000..c17ef2a Binary files /dev/null and b/packaging/sfx/drip.wav differ diff --git a/packaging/sfx/fail.wav b/packaging/sfx/fail.wav new file mode 100644 index 0000000..028864b Binary files /dev/null and b/packaging/sfx/fail.wav differ diff --git a/packaging/sfx/victory.wav b/packaging/sfx/victory.wav new file mode 100644 index 0000000..679eaaf Binary files /dev/null and b/packaging/sfx/victory.wav differ diff --git a/packaging/zucchinibread.js b/packaging/zucchinibread.js new file mode 100644 index 0000000..eed4706 --- /dev/null +++ b/packaging/zucchinibread.js @@ -0,0 +1,1321 @@ +"use strict"; + +/* Please forgive my strictly procedural/functional style here. + * Thanks for reading though! */ + +let zb = (function() { + /* ---- Util ---- */ + + function mod(x, n) { + return ((x%n)+n)%n; + } + + function sgn(x) { + if (x === 0) { + return 0; + } else { + return x / Math.abs(x); + } + } + + function as_hex(num, length) { + let result = Math.round(num).toString(16); + + if (length) { + while (result.length < length) { + result = "0" + result; + } + } + + return result; + } + + function copy_list(list) { + let newlist = []; + for (let x of list) { + newlist.push(x); + } + return newlist; + } + + function copy_flat_objlist(list) { + let newlist = []; + for (let x of list) { + newlist.push({ ...x }); + } + return newlist; + } + + /* ---- Resource loading / loading screen ---- */ + + let _audiocheck = document.createElement('audio'); + + let _SFX_ARRAY_SIZE = 10; + + /* Returns a callback that should be called when a resource finishes loading. */ + function _register_resource(name, game, callback) { + game._total_things_to_load ++; + console.log("Loading", name + ". Things to load:", game._total_things_to_load); + return function() { + if (!game.ready_to_go) { + game._things_loaded ++; + console.log("Loaded", name + ". Things loaded:", game._things_loaded, "/", game._total_things_to_load); + if (game.load_with_progress_bar) { + if (game._progressbar_img_loaded) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage( + game.img._progressbar, + 0, + 0, + Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width), + game.img._progressbar.height, + Math.round(game.canvas_w / 2 / game.draw_scale - game.img._progressbar.width / 2), + Math.round(game.canvas_h / 2 / game.draw_scale - game.img._progressbar.height / 2), + Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width), + game.img._progressbar.height, + ); + game.ctx.global.restore(); + } + } + _check_if_loaded(game); + if (callback) { + callback(); + } + } + } + } + + /* Check if audio failed to load bc file doesn't exist and print error message. */ + /* TODO add the same thing for an image */ + function _sound_resource_error(name, game, audio) { + return function(e) { + if (audio === undefined) { + console.error("Error loading resource, skipping:", name); + game._things_loaded ++; + _check_if_loaded(game); + } + } + } + + /* Check if the game has loaded everything and if so show the 'click to start' image */ + function _check_if_loaded(game) { + if (game.ready_to_go) return; + + if (game._things_loaded >= game._total_things_to_load) { + console.log("Ready"); + game.ready_to_go = true; + game._on_ready(); + } + } + + function _register_sfx(sfxdata, game) { + for (let key in sfxdata) { + let sfx_size = sfxdata[key].copies || _SFX_ARRAY_SIZE; + let sfx_array = new Array(sfx_size); + for (let i = 0; i < sfx_size; i++) { + sfx_array[i] = new Audio(sfxdata[key].path); + sfx_array[i].addEventListener('canplaythrough', + _register_resource(sfxdata[key].path + '#' + (i+1), game), false); + sfx_array[i].addEventListener('error', + _sound_resource_error(sfxdata[key].path + '#' + (i+1), game), false, sfx_array[i]); + if (sfxdata[key].hasOwnProperty('volume')) { + sfx_array[i].volume = sfxdata[key].volume; + } + } + game.sfx[key] = { + _array: sfx_array, + _index: 0, + play: function() { + if (!game.muted) { + this._array[this._index].currentTime = 0; + this._array[this._index].play(); + this._index ++; + this._index = mod(this._index, _SFX_ARRAY_SIZE); + } + } + } + } + } + + function _register_music(musicdata, game) { + for (let key in musicdata) { + let musicpath; + if (musicdata[key].path.match(/\.[a-z]{3}$/)) { + musicpath = musicdata[key].path; + } else if (_audiocheck.canPlayType('audio/mpeg')) { + musicpath = musicdata[key].path + '.mp3'; + } else if (_audiocheck.canPlayType('audio/ogg')) { + musicpath = musicdata[key].path + '.ogg'; + } else { + console.log("No supported music type known :("); + return; + } + let music = new Audio(musicpath); + if (musicdata[key].hasOwnProperty('volume')) { + music.volume = musicdata[key].volume; + } + + if (!musicdata[key].hasOwnProperty('loop') || musicdata[key].loop) { + /* We loop by default unless 'loop: false' is specified. */ + music.loop = true; + } + music.addEventListener('canplaythrough', _register_resource(musicpath, game), false); + music.addEventListener('error', _sound_resource_error(musicpath, game, music), false); + + /* Handle music changes when muted, so when we unmute, + * the correct music will be playing. */ + music._og_play = music.play; + music.play = function() { + if (game.muted) { + music.was_playing = true; + } else { + music._og_play(); + } + } + + music._og_pause = music.pause; + music.pause = function() { + if (game.muted) { + music.was_playing = false; + } else { + music._og_pause(); + } + } + + game.music[key] = music; + } + } + + function _register_images(imgdata, game) { + function _recursive_load_images(pathmap) { + let result = {}; + for (let key in pathmap) { + if (typeof pathmap[key] === 'object') { + result[key] = _recursive_load_images(pathmap[key]); + } else { + result[key] = new Image(); + result[key].onload = _register_resource(pathmap[key], game); + result[key].src = pathmap[key]; + } + } + return result; + } + + let loaded_imgs = _recursive_load_images(imgdata); + for (let k in loaded_imgs) { + game.img[k] = loaded_imgs[k]; + } + } + + function _create_canvas_context(canvas) { + let new_canvas = document.createElement('canvas'); + new_canvas.width = canvas.width; + new_canvas.height = canvas.height; + + let new_ctx = new_canvas.getContext('2d'); + new_ctx.imageSmoothingEnabled = false; + new_ctx.webkitImageSmoothingEnabled = false; + new_ctx.mozImageSmoothingEnabled = false; + + return new_ctx; + } + + function create_game(params) { + let game_props = {...params}; + + game_props.frame_rate = params.frame_rate || 60; + + game_props.canvas_w = params.canvas_w || 640; + game_props.canvas_h = params.canvas_h || 480; + game_props.draw_scale = params.draw_scale || 4; + game_props.top_border = params.top_border || 0; + game_props.bottom_border = params.bottom_border || 8; + game_props.left_border = params.left_border || 0; + game_props.right_border = params.right_border || 0; + + game_props.tile_size = params.tile_size || 16; + game_props.level_w = params.level_w || 10; + game_props.level_h = params.level_h || 7; + + game_props.screen_w = game_props.canvas_w / game_props.draw_scale; + game_props.screen_h = game_props.canvas_h / game_props.draw_scale; + + game_props.background_color = params.background_color || '#000000'; + + let canvas = document.getElementById(game_props.canvas); + + let global_ctx = canvas.getContext('2d'); + global_ctx.imageSmoothingEnabled = false; + global_ctx.webkitImageSmoothingEnabled = false; + global_ctx.mozImageSmoothingEnabled = false; + + let mask_canvas = document.createElement('canvas'); + mask_canvas.width = canvas.width; + mask_canvas.height = canvas.height; + let mask_ctx = mask_canvas.getContext('2d'); + mask_ctx.imageSmoothingEnabled = false; + mask_ctx.webkitImageSmoothingEnabled = false; + mask_ctx.mozImageSmoothingEnabled = false; + + let copy_canvas = document.createElement('canvas'); + copy_canvas.width = canvas.width; + copy_canvas.height = canvas.height; + let copy_ctx = copy_canvas.getContext('2d'); + copy_ctx.imageSmoothingEnabled = false; + copy_ctx.webkitImageSmoothingEnabled = false; + copy_ctx.mozImageSmoothingEnabled = false; + + let draw_canvas = document.createElement('canvas'); + draw_canvas.width = canvas.width; + draw_canvas.height = canvas.height; + let draw_ctx = draw_canvas.getContext('2d'); + draw_ctx.imageSmoothingEnabled = false; + draw_ctx.webkitImageSmoothingEnabled = false; + draw_ctx.mozImageSmoothingEnabled = false; + + let game = { + /* General properties */ + ...game_props, + + canvas: canvas, + + /* Loading */ + ready_to_go: false, + _total_things_to_load: 1, + _things_loaded: 0, + resources_ready: function() { + this._things_loaded ++; + console.log("Finished enumerating resources to load. Things loaded:", + this._things_loaded, "/", this._total_things_to_load); + _check_if_loaded(this); + }, + _on_ready: function() { + this.ready_to_go = true; + if (game.img._clicktostart && game.img._clicktostart.complete) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(game.img._clicktostart, 0, 0); + game.ctx.global.restore(); + } + }, + playing: false, + play: function() { + this.playing = true; + if (this.events.gamestart) { + this.events.gamestart(this); + } + _loop(this); + }, + _norun: false, + + /* Audio */ + sfx: {}, + music: {}, + muted: false, + mute: function() { + _mute(this); + }, + unmute: function() { + _unmute(this); + }, + toggle_mute: function() { + if (this.muted) { + _unmute(this); + } else { + _mute(this); + } + }, + register_sfx: function(sfxdata) { + _register_sfx(sfxdata, this); + }, + register_music: function(musicdata) { + _register_music(musicdata, this); + }, + + /* Drawing */ + ctx: { + global: global_ctx, /* context for the actual real canvas */ + mask: mask_ctx, /* context for drawing the transition mask, gets scaled up */ + copy: copy_ctx, /* context for copying the old screen on transition */ + draw: draw_ctx, /* context for drawing the real level */ + }, + create_canvas_context: function() { + /* Function to return a fresh screen-sized canvas context, if we need it. */ + return _create_canvas_context(this.canvas); + }, + img: {}, + register_images: function(imgdata) { + _register_images(imgdata, this); + }, + + /* Save */ + save: function(key, data) { + _save(game, key, data); + }, + load: function(key) { + return _load(game, key); + }, + + /* Transition system */ + transition: _transition, + start_transition: function(type, length, callback, on_done) { + _start_transition(this, type, length, callback, on_done); + }, + long_transition: function(type, length, callback, on_done) { + _long_transition(this, type, length, callback, on_done); + }, + + /* Input */ + touchmode: false, + + /* Mode */ + change_mode: function(newmode) { + _change_mode(game, newmode); + } + }; + + window.requestAnimFrame = (function() { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback, element) { + window.setTimeout(callback, 1000/game.frame_rate); + }; + })(); + + /* Register event listeners */ + for (let ev in params.events) { + /* Register any other events I guess */ + canvas['on' + ev] = function(e) { + if (game.playing && !game._norun) { + params.events[ev](game, e); + } + } + } + + /* Override with special events */ + canvas.onmousedown = function(e) { + if (!game._norun) { + _handle_mousedown(game, e); + } + } + + canvas.onmousemove = function(e) { + if (!game._norun) { + _handle_mousemove(game, e); + } + } + + canvas.onmouseup = function(e) { + if (!game._norun) { + _handle_mouseup(game, e); + } + } + + canvas.ontouchstart = function(e) { + if (!game._norun) { + game.touchmode = true; + _handle_touchstart(game, e); + } + e.preventDefault(); + } + + canvas.ontouchmove = function(e) { + if (!game._norun) { + game.touchmode = true; + _handle_touchmove(game, e); + } + e.preventDefault(); + } + + canvas.ontouchend = function(e) { + if (!game._norun) { + game.touchmode = true; + _handle_touchend(game, e); + } + e.preventDefault(); + } + + canvas.onkeydown = function(e) { + if (!game._norun && game.events.keydown) { + game.events.keydown(game, e); + e.preventDefault(); + } + } + + canvas.onkeyup = function(e) { + if (!game._norun && game.events.keyup) { + game.events.keyup(game, e); + e.preventDefault(); + } + } + + canvas.onblur = function(e) { + if (game.playing && !game.run_in_background) { + game._norun = true; + _stop_music(game); + } + } + + canvas.onfocus = function(e) { + if (game.playing && !game.run_in_background) { + game._norun = false; + if (!game.muted) { + _start_music(game); + } + _loop(game); + } + } + + /* Set loading stuff */ + let loading_img = new Image(); + loading_img.onload = _register_resource('loading.png', game, function() { + if (!game.ready_to_go) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(loading_img, 0, 0); + game.ctx.global.restore(); + } + }); + loading_img.src = 'loading.png'; + + if (game.load_with_progress_bar) { + game._progressbar_img_loaded = false; + game._progressbar_width = 0; + game._progressbar_height = 0; + + let progressbar_img = new Image(); + progressbar_img.onload = _register_resource('progressbar.png', game, function() { + game._progressbar_img_loaded = true; + game._progressbar_width = progressbar_img.width; + game._progressbar_height = progressbar_img.height; + if (!game.ready_to_go) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(loading_img, 0, 0); + game.ctx.global.restore(); + } + }); + progressbar_img.src = 'progressbar.png'; + game.img._progressbar = progressbar_img; + } + + if (!game.run_in_background) { + let pause_img = new Image(); + pause_img.onload = _register_resource('pause.png', game); + pause_img.src = 'pause.png'; + game.img._pause = pause_img; + } + + if (game.hasOwnProperty('save_key')) { + let saveerror_img = new Image(); + saveerror_img.onload = _register_resource('saveerror.png', game); + saveerror_img.src = 'saveerror.png'; + game.img._saveerror = saveerror_img; + } + + let clicktostart_img = new Image(); + clicktostart_img.onload = _register_resource('clicktostart.png', game, function() { + if (game.ready_to_go) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(game.img._clicktostart, 0, 0); + game.ctx.global.restore(); + } + }); + clicktostart_img.src = 'clicktostart.png'; + game.img._clicktostart = clicktostart_img; + + return game; + } + + /* ---- Audio ---- */ + + function _stop_music(game) { + if (game.muted) return; + + for (let m in game.music) { + if (!game.music[m].paused) { + game.music[m].was_playing = true; + game.music[m].pause(); + } else { + game.music[m].was_playing = false; + } + } + } + + function _start_music(game, unmuting) { + if (game.muted && !unmuting) return; + + for (let m in game.music) { + if (game.music[m].was_playing) { + game.music[m].play(); + } + } + } + + function _mute(game) { + _stop_music(game); + game.muted = true; + } + + function _unmute(game) { + game.muted = false; + _start_music(game); + } + + /* ---- Game update stuff ---- */ + + let _last_frame_time; + let _timedelta = 0; + function _loop(game, timestamp) { + if (timestamp == undefined) { + timestamp = 0; + _last_frame_time = timestamp; + } + _timedelta += timestamp - _last_frame_time; + _last_frame_time = timestamp; + + while (_timedelta >= 1000 / game.frame_rate) { + _update(game, 1000 / game.frame_rate); + _timedelta -= 1000 / game.frame_rate; + } + _draw(game); + + if (!game._norun) { + requestAnimFrame(function(timestamp) { + _loop(game, timestamp); + }); + } + } + + function _update(game, delta) { + if (game.update_func) { + game.update_func(delta); + } + + if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].update) { + game.modes[game.mode].update(delta); + } + + if (game.transition.is_transitioning) { + game.transition.timer += delta; + if (game.transition.timer > game.transition.end_time) { + _finish_transition(game); + } + } + } + + function _change_mode(game, newmode) { + game.mode = newmode; + } + + /* ---- UI ---- */ + + function create_buttons(button_data) { + for (let b of button_data.buttons) { + b.state = 0; + } + + return button_data; + } + + /* Call with a button set and given x / y mouse coordinates. + * Will update the button set in place. + * Should generally be called in mouse move and mouse up event handlers. */ + function update_buttons(button_data, x, y) { + for (let button of button_data.buttons) { + if (button.state === 2) continue; + if (button.disabled) continue; + + if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) { + if (!game.touchmode) { + button.state = 1; + } + } else if (button.state !== 2) { + button.state = 0; + } + } + } + + /* Call with a button set and given x / y mouse coordinates. + * If a button is clicked, triggers its callback and returns true. + * Otherwise, returns false. + * Should be called in mouse down event handler. */ + function click_button(button_data, x, y) { + for (let button of button_data.buttons) { + if (button.disabled) continue; + + if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) { + button.state = 2; + game._clicked_button = button; + return true; + } + } + return false; + } + + function _handle_clicked_button(game) { + if (game._clicked_button) { + game._clicked_button.state = 0; + _draw(game); + if (game._clicked_button.callback) { + game._clicked_button.callback(); + } + game._clicked_button = null; + return; + } + } + + /* ---- Mouse ---- */ + + function _handle_mousedown(game, e) { + if (!game.playing) return; + + game.touchmode = false; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1; + if (e.button === 0 && game.events.mousedown) { + game.events.mousedown(game, e, x, y); + } + } + + function _handle_mouseup(game, e) { + if (!game.playing && game.ready_to_go) { + /* Click to start */ + if (game.hasOwnProperty('save_key') && !game._show_save_error) { + /* Test if save/load is working */ + try { + console.log("Test saving"); + localStorage.setItem(game.save_key + ".test", "savetest"); + let savetest = localStorage.getItem(game.save_key + ".test"); + if (savetest !== "savetest") { + throw new Error("oops"); + } + } catch (e) { + /* If error saving, show save error message first */ + /* We set _show_save_error so that the second time the user clicks, + * the game will actually start */ + console.log("- SAVE FAILED -"); + game._show_save_error = true; + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(game.img._saveerror, 0, 0); + game.ctx.global.restore(); + return; + } + } + game.play(); + return; + } + + game.touchmode = false; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1; + + _handle_clicked_button(game); + + if (e.button === 0 && game.events.mouseup) { + game.events.mouseup(game, e, x, y); + } + } + + function _handle_mousemove(game, e) { + game.touchmode = false; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1; + if (game.events.mousemove) { + game.events.mousemove(game, e, x, y); + } + } + + /* ---- Touch (defaults to same as mouse) ---- */ + + let _last_touch_event; + + function _handle_touchstart(game, e) { + if (!game.playing) return; + + game.touchmode = true; + + if (e.touches) e = e.touches[0]; + + _last_touch_event = e; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1; + if (game.events.touchstart) { + game.events.touchstart(game, e, x, y); + } else if (game.events.mousedown) { + if (game.events.mousemove) { + game.events.mousemove(game, e, x, y); + } + game.events.mousedown(game, e, x, y); + } + } + + function _handle_touchend(game, e) { + if (!game.playing && game.ready_to_go) { + /* Click to start */ + game.play(); + return; + } + + game.touchmode = true; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((_last_touch_event.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((_last_touch_event.clientY - rect.top) / rect.height * game.screen_h) - 1; + + _handle_clicked_button(game); + + if (game.events.touchend) { + game.events.touchend(game, e, x, y); + } else if (game.events.mouseup) { + if (game.events.mousemove) { + game.events.mousemove(game, e, x, y); + } + game.events.mouseup(game, e, x, y); + } + } + + function _handle_touchmove(game, e) { + if (!game.playing) return; + + game.touchmode = true; + + if (e.touches) e = e.touches[0]; + + _last_touch_event = e; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1; + if (game.events.touchmove) { + game.events.touchmove(game, e, x, y); + } else if (game.events.mousemove) { + game.events.mousemove(game, e, x, y); + } + } + + /* ---- Drawing stuff ---- */ + + function _draw(game) { + game.ctx.draw.save(); + + game.ctx.draw.fillStyle = game.background_color; + + game.ctx.draw.beginPath(); + game.ctx.draw.rect(0, 0, game.screen_w, game.screen_h); + game.ctx.draw.fill(); + + if (game.draw_func) { + game.draw_func(game.ctx.draw); + } + + if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw) { + game.modes[game.mode].draw(game.ctx.draw); + } + + if (game.transition.mid_long) { + game.ctx.draw.fillStyle = game.transition.color; + game.ctx.draw.fillRect(-1, -1, game.canvas_w + 5, game.canvas_h + 5); + } + + game.ctx.draw.restore(); + + if (game.transition.is_transitioning) { + _draw_transition(game, game.ctx.mask); + } else { + game.ctx.mask.drawImage(game.ctx.draw.canvas, 0, 0); + } + + if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw_after_transition) { + game.modes[game.mode].draw_after_transition(game.ctx.mask); + } + + + game.ctx.global.fillStyle = 'rgb(0, 0, 0)'; + game.ctx.global.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(game.ctx.mask.canvas, 0, 0); + + if (game._norun) { + game.ctx.global.drawImage(game.img._pause, 0, 0); + } + + game.ctx.global.restore(); + } + + /* Draw a particular section of an image/spritesheet, + * without having to do as much math or type the destination size */ + function sprite_draw(ctx, img, section_w, section_h, section_x, section_y, dest_x, dest_y) { + ctx.drawImage(img, + section_w * section_x, section_h * section_y, section_w, section_h, + 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); + } + + /* Draw a set of buttons. */ + function button_draw(ctx, button_data) { + for (let button of button_data.buttons) { + if (!button.state) button.state = 0; + let button_state = button.state; + if (button.disabled) button_state = 3; + sprite_draw(ctx, button_data.img, button_data.button_w, button_data.button_h, button.id, button_state, button.x, button.y); + } + } + + /* ---- Save ---- */ + + function _save(game, key, data) { + let save_data; + + if (!game.hasOwnProperty('save_key')) { + throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object"); + } + + try { + save_data = localStorage.getItem(game.save_key) || "{}"; + } catch (e) { + console.error("Saving is disabled; cannot save " + key, e); + return; + } + + try { + save_data = JSON.parse(save_data); + } catch (e) { + console.error("Cannot parse stored save data (when trying to save " + key + ")", e); + return; + } + + save_data[key] = JSON.stringify(data); + + try { + save_data = JSON.stringify(save_data); + } catch (e) { + console.error("Failed to stringify save data (while trying to save " + key + ")", e); + return; + } + + try { + localStorage.setItem(game.save_key, save_data); + } catch (e) { + console.error("Failed to save to localStorage (while saving key " + key + ")", e); + return; + } + } + + function _load(game, key) { + let save_data; + + if (!game.hasOwnProperty('save_key')) { + throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object"); + } + + try { + save_data = localStorage.getItem(game.save_key); + } catch (e) { + console.error("Loading is disabled; cannot load " + key, e); + return; + } + + try { + save_data = JSON.parse(save_data); + } catch (e) { + console.error("Cannot parse stored save data (when trying to load " + key + ")", e); + return; + } + + if (!save_data) { + return null; + } + + return save_data[key]; + } + + /* ---- Transition stuff ---- */ + + let _transition = { + is_transitioning: false, + timer: 0, + color: '#000000', + w: 20, + h: 14, + dir_invert_v: false, + dir_invert_h: false, + invert_shape: true, + mid_long: false, + done_func: null, + type: _draw_fade_transition, + nodraw: false, + end_time: 100, + } + + function _long_transition(game, type, length, callback) { + if (game.transition.is_transitioning) return; + + _draw(game); + + game.transition.invert_shape = false; + _internal_start_transition(game, type, length, function() { + game.transition.mid_long = true; + }, function() { + game.transition.invert_shape = true; + game.transition.is_transitioning = true; + let tdiv = game.transition.dir_invert_v; + let tdih = game.transition.dir_invert_h; + _internal_start_transition(game, type, length, function() { + game.transition.mid_long = false; + callback(); + game.transition.dir_invert_v = tdiv; + game.transition.dir_invert_h = tdih; + }); + }); + } + + function _start_transition(game, type, length, callback, on_done) { + if (game.transition.is_transitioning) return; + if (!game.transition.nodraw) _draw(game); + + _internal_start_transition(game, type, length, callback, on_done); + } + + function _internal_start_transition(game, type, length, callback, on_done) { + if (on_done) { + game.transition.done_func = on_done; + } + + game.transition.timer = 0; + game.transition.type = type; + game.transition.end_time = length; + + game.ctx.copy.drawImage(game.ctx.draw.canvas, 0, 0); + + callback(); + + game.transition.is_transitioning = true; + } + + function _finish_transition(game) { + game.transition.is_transitioning = false; + game.transition.timer = 0; + + if (game.transition.done_func) { + setTimeout(function() { + game.transition.done_func(); + game.transition.done_func = null; + }, 400); + } + } + + function _draw_transition(game, ctx) { + let frac = game.transition.timer / game.transition.end_time; + + ctx.save(); + ctx.clearRect(0, 0, game.screen_w, game.screen_h); + game.transition.type(game, ctx, game.ctx.copy, game.ctx.draw, frac, game.transition.invert_shape); + ctx.restore(); + } + + function _draw_slide_down_transition(game, ctx, oldc, newc, frac, reverse) { + let offset = frac * game.screen_h; + + ctx.drawImage(oldc.canvas, 0, -offset); + ctx.drawImage(newc.canvas, 0, game.screen_h - offset); + } + + function _draw_slide_up_transition(game, ctx, oldc, newc, frac, reverse) { + let offset = frac * game.screen_h; + + ctx.drawImage(oldc.canvas, 0, offset); + ctx.drawImage(newc.canvas, 0, - game.screen_h + offset); + } + + function _draw_fade_transition(game, ctx, oldc, newc, frac, reverse) { + ctx.globalAlpha = 1; + ctx.drawImage(oldc.canvas, 0, 0); + ctx.globalAlpha = frac; + ctx.drawImage(newc.canvas, 0, 0); + } + + /* ---- former contents of ready.js ---- */ + + // Due to Timo Huovinen + // https://stackoverflow.com/questions/799981/document-ready-equivalent-without-jquery + var ready = (function(){ + + var readyList, + DOMContentLoaded, + class2type = {}; + class2type["[object Boolean]"] = "boolean"; + class2type["[object Number]"] = "number"; + class2type["[object String]"] = "string"; + class2type["[object Function]"] = "function"; + class2type["[object Array]"] = "array"; + class2type["[object Date]"] = "date"; + class2type["[object RegExp]"] = "regexp"; + class2type["[object Object]"] = "object"; + + var ReadyObj = { + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + ReadyObj.readyWait++; + } else { + ReadyObj.ready( true ); + } + }, + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--ReadyObj.readyWait) || (wait !== true && !ReadyObj.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( ReadyObj.ready, 1 ); + } + + // Remember that the DOM is ready + ReadyObj.isReady = true; + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --ReadyObj.readyWait > 0 ) { + return; + } + // If there are functions bound, to execute + readyList.resolveWith( document, [ ReadyObj ] ); + + // Trigger any bound ready events + //if ( ReadyObj.fn.trigger ) { + // ReadyObj( document ).trigger( "ready" ).unbind( "ready" ); + //} + } + }, + bindReady: function() { + if ( readyList ) { + return; + } + readyList = ReadyObj._Deferred(); + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( ReadyObj.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + // A fallback to window.onload, that will always work + window.addEventListener( "load", ReadyObj.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", ReadyObj.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + _Deferred: function() { + var // callbacks list + callbacks = [], + // stored [ context , args ] + fired, + // to avoid firing when already doing so + firing, + // flag to know if the deferred has been cancelled + cancelled, + // the deferred itself + deferred = { + + // done( f1, f2, ...) + done: function() { + if ( !cancelled ) { + var args = arguments, + i, + length, + elem, + type, + _fired; + if ( fired ) { + _fired = fired; + fired = 0; + } + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = ReadyObj.type( elem ); + if ( type === "array" ) { + deferred.done.apply( deferred, elem ); + } else if ( type === "function" ) { + callbacks.push( elem ); + } + } + if ( _fired ) { + deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] ); + } + } + return this; + }, + + // resolve with given context and args + resolveWith: function( context, args ) { + if ( !cancelled && !fired && !firing ) { + // make sure args are available (#8421) + args = args || []; + firing = 1; + try { + while( callbacks[ 0 ] ) { + callbacks.shift().apply( context, args );//shifts a callback, and applies it to document + } + } + finally { + fired = [ context, args ]; + firing = 0; + } + } + return this; + }, + + // resolve with this as context and given arguments + resolve: function() { + deferred.resolveWith( this, arguments ); + return this; + }, + + // Has this deferred been resolved? + isResolved: function() { + return !!( firing || fired ); + }, + + // Cancel + cancel: function() { + cancelled = 1; + callbacks = []; + return this; + } + }; + + return deferred; + }, + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ Object.prototype.toString.call(obj) ] || "object"; + } + } + // The DOM ready check for Internet Explorer + function doScrollCheck() { + if ( ReadyObj.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + ReadyObj.ready(); + } + // Cleanup functions for the document ready method + if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + ReadyObj.ready(); + }; + + } else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + ReadyObj.ready(); + } + }; + } + function ready( fn ) { + // Attach the listeners + ReadyObj.bindReady(); + + var type = ReadyObj.type( fn ); + + // Add the callback + readyList.done( fn );//readyList is result of _Deferred() + } + return ready; + })(); + + return { + create_game: create_game, + + ready: ready, + mod: mod, + sgn: sgn, + as_hex: as_hex, + copy_list: copy_list, + copy_flat_objlist: copy_flat_objlist, + + sprite_draw: sprite_draw, + screen_draw: screen_draw, + + buttons: { + create: create_buttons, + update: update_buttons, + click: click_button, + draw: button_draw, + }, + + transition: { + type: { + fade: _draw_fade_transition, + slide_up: _draw_slide_up_transition, + slide_down: _draw_slide_down_transition, + }, + }, + } +})(); diff --git a/pause.png b/pause.png new file mode 100644 index 0000000..7479e89 Binary files /dev/null and b/pause.png differ diff --git a/progressbar.png b/progressbar.png new file mode 100644 index 0000000..7479e89 Binary files /dev/null and b/progressbar.png differ diff --git a/saveerror.png b/saveerror.png new file mode 100644 index 0000000..ab259b9 Binary files /dev/null and b/saveerror.png differ diff --git a/sfx/bubble.wav b/sfx/bubble.wav new file mode 100644 index 0000000..c8a37fa Binary files /dev/null and b/sfx/bubble.wav differ diff --git a/sfx/bubble2.wav b/sfx/bubble2.wav new file mode 100644 index 0000000..7b173be Binary files /dev/null and b/sfx/bubble2.wav differ diff --git a/sfx/bubble3.wav b/sfx/bubble3.wav new file mode 100644 index 0000000..d640ee8 Binary files /dev/null and b/sfx/bubble3.wav differ diff --git a/sfx/bubble4.wav b/sfx/bubble4.wav new file mode 100644 index 0000000..e137be1 Binary files /dev/null and b/sfx/bubble4.wav differ diff --git a/sfx/drag.wav b/sfx/drag.wav new file mode 100644 index 0000000..89fb719 Binary files /dev/null and b/sfx/drag.wav differ diff --git a/sfx/drip.wav b/sfx/drip.wav new file mode 100644 index 0000000..c17ef2a Binary files /dev/null and b/sfx/drip.wav differ diff --git a/sfx/fail.wav b/sfx/fail.wav new file mode 100644 index 0000000..028864b Binary files /dev/null and b/sfx/fail.wav differ diff --git a/sfx/victory.wav b/sfx/victory.wav new file mode 100644 index 0000000..679eaaf Binary files /dev/null and b/sfx/victory.wav differ diff --git a/sfx_raw/2025_04_06_08_58_05_1.mp3 b/sfx_raw/2025_04_06_08_58_05_1.mp3 new file mode 100644 index 0000000..3136fd2 Binary files /dev/null and b/sfx_raw/2025_04_06_08_58_05_1.mp3 differ diff --git a/sfx_raw/2025_04_06_08_58_35_1.mp3 b/sfx_raw/2025_04_06_08_58_35_1.mp3 new file mode 100644 index 0000000..757b0d7 Binary files /dev/null and b/sfx_raw/2025_04_06_08_58_35_1.mp3 differ diff --git a/sfx_raw/2025_04_06_09_41_49_1.mp3 b/sfx_raw/2025_04_06_09_41_49_1.mp3 new file mode 100644 index 0000000..e280633 Binary files /dev/null and b/sfx_raw/2025_04_06_09_41_49_1.mp3 differ diff --git a/sfx_raw/fail.wav b/sfx_raw/fail.wav new file mode 100644 index 0000000..b9cd3dc Binary files /dev/null and b/sfx_raw/fail.wav differ diff --git a/sfx_raw/fanfare.wav b/sfx_raw/fanfare.wav new file mode 100644 index 0000000..f763f9d Binary files /dev/null and b/sfx_raw/fanfare.wav differ diff --git a/sharkfont.zip b/sharkfont.zip new file mode 100644 index 0000000..9ef9ea1 Binary files /dev/null and b/sharkfont.zip differ diff --git a/sharkfont/license.txt b/sharkfont/license.txt new file mode 100644 index 0000000..6fe4768 --- /dev/null +++ b/sharkfont/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 “sharkfont” (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 (“sharkfont”); and +to the extent reasonably practicable, the URI +(https://fontstruct.com/fontstructions/show/2657823) 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/sharkfont/readme.txt b/sharkfont/readme.txt new file mode 100644 index 0000000..4667a2f --- /dev/null +++ b/sharkfont/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/2657823 +[ancestry] +Try Fontstruct at https://fontstruct.com +It’s easy and it’s fun. + +Fontstruct is copyright ©2025 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/sharkfont/sharkfont.ttf b/sharkfont/sharkfont.ttf new file mode 100644 index 0000000..bf1d333 Binary files /dev/null and b/sharkfont/sharkfont.ttf differ diff --git a/zucchinibread.js b/zucchinibread.js new file mode 100644 index 0000000..eed4706 --- /dev/null +++ b/zucchinibread.js @@ -0,0 +1,1321 @@ +"use strict"; + +/* Please forgive my strictly procedural/functional style here. + * Thanks for reading though! */ + +let zb = (function() { + /* ---- Util ---- */ + + function mod(x, n) { + return ((x%n)+n)%n; + } + + function sgn(x) { + if (x === 0) { + return 0; + } else { + return x / Math.abs(x); + } + } + + function as_hex(num, length) { + let result = Math.round(num).toString(16); + + if (length) { + while (result.length < length) { + result = "0" + result; + } + } + + return result; + } + + function copy_list(list) { + let newlist = []; + for (let x of list) { + newlist.push(x); + } + return newlist; + } + + function copy_flat_objlist(list) { + let newlist = []; + for (let x of list) { + newlist.push({ ...x }); + } + return newlist; + } + + /* ---- Resource loading / loading screen ---- */ + + let _audiocheck = document.createElement('audio'); + + let _SFX_ARRAY_SIZE = 10; + + /* Returns a callback that should be called when a resource finishes loading. */ + function _register_resource(name, game, callback) { + game._total_things_to_load ++; + console.log("Loading", name + ". Things to load:", game._total_things_to_load); + return function() { + if (!game.ready_to_go) { + game._things_loaded ++; + console.log("Loaded", name + ". Things loaded:", game._things_loaded, "/", game._total_things_to_load); + if (game.load_with_progress_bar) { + if (game._progressbar_img_loaded) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage( + game.img._progressbar, + 0, + 0, + Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width), + game.img._progressbar.height, + Math.round(game.canvas_w / 2 / game.draw_scale - game.img._progressbar.width / 2), + Math.round(game.canvas_h / 2 / game.draw_scale - game.img._progressbar.height / 2), + Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width), + game.img._progressbar.height, + ); + game.ctx.global.restore(); + } + } + _check_if_loaded(game); + if (callback) { + callback(); + } + } + } + } + + /* Check if audio failed to load bc file doesn't exist and print error message. */ + /* TODO add the same thing for an image */ + function _sound_resource_error(name, game, audio) { + return function(e) { + if (audio === undefined) { + console.error("Error loading resource, skipping:", name); + game._things_loaded ++; + _check_if_loaded(game); + } + } + } + + /* Check if the game has loaded everything and if so show the 'click to start' image */ + function _check_if_loaded(game) { + if (game.ready_to_go) return; + + if (game._things_loaded >= game._total_things_to_load) { + console.log("Ready"); + game.ready_to_go = true; + game._on_ready(); + } + } + + function _register_sfx(sfxdata, game) { + for (let key in sfxdata) { + let sfx_size = sfxdata[key].copies || _SFX_ARRAY_SIZE; + let sfx_array = new Array(sfx_size); + for (let i = 0; i < sfx_size; i++) { + sfx_array[i] = new Audio(sfxdata[key].path); + sfx_array[i].addEventListener('canplaythrough', + _register_resource(sfxdata[key].path + '#' + (i+1), game), false); + sfx_array[i].addEventListener('error', + _sound_resource_error(sfxdata[key].path + '#' + (i+1), game), false, sfx_array[i]); + if (sfxdata[key].hasOwnProperty('volume')) { + sfx_array[i].volume = sfxdata[key].volume; + } + } + game.sfx[key] = { + _array: sfx_array, + _index: 0, + play: function() { + if (!game.muted) { + this._array[this._index].currentTime = 0; + this._array[this._index].play(); + this._index ++; + this._index = mod(this._index, _SFX_ARRAY_SIZE); + } + } + } + } + } + + function _register_music(musicdata, game) { + for (let key in musicdata) { + let musicpath; + if (musicdata[key].path.match(/\.[a-z]{3}$/)) { + musicpath = musicdata[key].path; + } else if (_audiocheck.canPlayType('audio/mpeg')) { + musicpath = musicdata[key].path + '.mp3'; + } else if (_audiocheck.canPlayType('audio/ogg')) { + musicpath = musicdata[key].path + '.ogg'; + } else { + console.log("No supported music type known :("); + return; + } + let music = new Audio(musicpath); + if (musicdata[key].hasOwnProperty('volume')) { + music.volume = musicdata[key].volume; + } + + if (!musicdata[key].hasOwnProperty('loop') || musicdata[key].loop) { + /* We loop by default unless 'loop: false' is specified. */ + music.loop = true; + } + music.addEventListener('canplaythrough', _register_resource(musicpath, game), false); + music.addEventListener('error', _sound_resource_error(musicpath, game, music), false); + + /* Handle music changes when muted, so when we unmute, + * the correct music will be playing. */ + music._og_play = music.play; + music.play = function() { + if (game.muted) { + music.was_playing = true; + } else { + music._og_play(); + } + } + + music._og_pause = music.pause; + music.pause = function() { + if (game.muted) { + music.was_playing = false; + } else { + music._og_pause(); + } + } + + game.music[key] = music; + } + } + + function _register_images(imgdata, game) { + function _recursive_load_images(pathmap) { + let result = {}; + for (let key in pathmap) { + if (typeof pathmap[key] === 'object') { + result[key] = _recursive_load_images(pathmap[key]); + } else { + result[key] = new Image(); + result[key].onload = _register_resource(pathmap[key], game); + result[key].src = pathmap[key]; + } + } + return result; + } + + let loaded_imgs = _recursive_load_images(imgdata); + for (let k in loaded_imgs) { + game.img[k] = loaded_imgs[k]; + } + } + + function _create_canvas_context(canvas) { + let new_canvas = document.createElement('canvas'); + new_canvas.width = canvas.width; + new_canvas.height = canvas.height; + + let new_ctx = new_canvas.getContext('2d'); + new_ctx.imageSmoothingEnabled = false; + new_ctx.webkitImageSmoothingEnabled = false; + new_ctx.mozImageSmoothingEnabled = false; + + return new_ctx; + } + + function create_game(params) { + let game_props = {...params}; + + game_props.frame_rate = params.frame_rate || 60; + + game_props.canvas_w = params.canvas_w || 640; + game_props.canvas_h = params.canvas_h || 480; + game_props.draw_scale = params.draw_scale || 4; + game_props.top_border = params.top_border || 0; + game_props.bottom_border = params.bottom_border || 8; + game_props.left_border = params.left_border || 0; + game_props.right_border = params.right_border || 0; + + game_props.tile_size = params.tile_size || 16; + game_props.level_w = params.level_w || 10; + game_props.level_h = params.level_h || 7; + + game_props.screen_w = game_props.canvas_w / game_props.draw_scale; + game_props.screen_h = game_props.canvas_h / game_props.draw_scale; + + game_props.background_color = params.background_color || '#000000'; + + let canvas = document.getElementById(game_props.canvas); + + let global_ctx = canvas.getContext('2d'); + global_ctx.imageSmoothingEnabled = false; + global_ctx.webkitImageSmoothingEnabled = false; + global_ctx.mozImageSmoothingEnabled = false; + + let mask_canvas = document.createElement('canvas'); + mask_canvas.width = canvas.width; + mask_canvas.height = canvas.height; + let mask_ctx = mask_canvas.getContext('2d'); + mask_ctx.imageSmoothingEnabled = false; + mask_ctx.webkitImageSmoothingEnabled = false; + mask_ctx.mozImageSmoothingEnabled = false; + + let copy_canvas = document.createElement('canvas'); + copy_canvas.width = canvas.width; + copy_canvas.height = canvas.height; + let copy_ctx = copy_canvas.getContext('2d'); + copy_ctx.imageSmoothingEnabled = false; + copy_ctx.webkitImageSmoothingEnabled = false; + copy_ctx.mozImageSmoothingEnabled = false; + + let draw_canvas = document.createElement('canvas'); + draw_canvas.width = canvas.width; + draw_canvas.height = canvas.height; + let draw_ctx = draw_canvas.getContext('2d'); + draw_ctx.imageSmoothingEnabled = false; + draw_ctx.webkitImageSmoothingEnabled = false; + draw_ctx.mozImageSmoothingEnabled = false; + + let game = { + /* General properties */ + ...game_props, + + canvas: canvas, + + /* Loading */ + ready_to_go: false, + _total_things_to_load: 1, + _things_loaded: 0, + resources_ready: function() { + this._things_loaded ++; + console.log("Finished enumerating resources to load. Things loaded:", + this._things_loaded, "/", this._total_things_to_load); + _check_if_loaded(this); + }, + _on_ready: function() { + this.ready_to_go = true; + if (game.img._clicktostart && game.img._clicktostart.complete) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(game.img._clicktostart, 0, 0); + game.ctx.global.restore(); + } + }, + playing: false, + play: function() { + this.playing = true; + if (this.events.gamestart) { + this.events.gamestart(this); + } + _loop(this); + }, + _norun: false, + + /* Audio */ + sfx: {}, + music: {}, + muted: false, + mute: function() { + _mute(this); + }, + unmute: function() { + _unmute(this); + }, + toggle_mute: function() { + if (this.muted) { + _unmute(this); + } else { + _mute(this); + } + }, + register_sfx: function(sfxdata) { + _register_sfx(sfxdata, this); + }, + register_music: function(musicdata) { + _register_music(musicdata, this); + }, + + /* Drawing */ + ctx: { + global: global_ctx, /* context for the actual real canvas */ + mask: mask_ctx, /* context for drawing the transition mask, gets scaled up */ + copy: copy_ctx, /* context for copying the old screen on transition */ + draw: draw_ctx, /* context for drawing the real level */ + }, + create_canvas_context: function() { + /* Function to return a fresh screen-sized canvas context, if we need it. */ + return _create_canvas_context(this.canvas); + }, + img: {}, + register_images: function(imgdata) { + _register_images(imgdata, this); + }, + + /* Save */ + save: function(key, data) { + _save(game, key, data); + }, + load: function(key) { + return _load(game, key); + }, + + /* Transition system */ + transition: _transition, + start_transition: function(type, length, callback, on_done) { + _start_transition(this, type, length, callback, on_done); + }, + long_transition: function(type, length, callback, on_done) { + _long_transition(this, type, length, callback, on_done); + }, + + /* Input */ + touchmode: false, + + /* Mode */ + change_mode: function(newmode) { + _change_mode(game, newmode); + } + }; + + window.requestAnimFrame = (function() { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback, element) { + window.setTimeout(callback, 1000/game.frame_rate); + }; + })(); + + /* Register event listeners */ + for (let ev in params.events) { + /* Register any other events I guess */ + canvas['on' + ev] = function(e) { + if (game.playing && !game._norun) { + params.events[ev](game, e); + } + } + } + + /* Override with special events */ + canvas.onmousedown = function(e) { + if (!game._norun) { + _handle_mousedown(game, e); + } + } + + canvas.onmousemove = function(e) { + if (!game._norun) { + _handle_mousemove(game, e); + } + } + + canvas.onmouseup = function(e) { + if (!game._norun) { + _handle_mouseup(game, e); + } + } + + canvas.ontouchstart = function(e) { + if (!game._norun) { + game.touchmode = true; + _handle_touchstart(game, e); + } + e.preventDefault(); + } + + canvas.ontouchmove = function(e) { + if (!game._norun) { + game.touchmode = true; + _handle_touchmove(game, e); + } + e.preventDefault(); + } + + canvas.ontouchend = function(e) { + if (!game._norun) { + game.touchmode = true; + _handle_touchend(game, e); + } + e.preventDefault(); + } + + canvas.onkeydown = function(e) { + if (!game._norun && game.events.keydown) { + game.events.keydown(game, e); + e.preventDefault(); + } + } + + canvas.onkeyup = function(e) { + if (!game._norun && game.events.keyup) { + game.events.keyup(game, e); + e.preventDefault(); + } + } + + canvas.onblur = function(e) { + if (game.playing && !game.run_in_background) { + game._norun = true; + _stop_music(game); + } + } + + canvas.onfocus = function(e) { + if (game.playing && !game.run_in_background) { + game._norun = false; + if (!game.muted) { + _start_music(game); + } + _loop(game); + } + } + + /* Set loading stuff */ + let loading_img = new Image(); + loading_img.onload = _register_resource('loading.png', game, function() { + if (!game.ready_to_go) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(loading_img, 0, 0); + game.ctx.global.restore(); + } + }); + loading_img.src = 'loading.png'; + + if (game.load_with_progress_bar) { + game._progressbar_img_loaded = false; + game._progressbar_width = 0; + game._progressbar_height = 0; + + let progressbar_img = new Image(); + progressbar_img.onload = _register_resource('progressbar.png', game, function() { + game._progressbar_img_loaded = true; + game._progressbar_width = progressbar_img.width; + game._progressbar_height = progressbar_img.height; + if (!game.ready_to_go) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(loading_img, 0, 0); + game.ctx.global.restore(); + } + }); + progressbar_img.src = 'progressbar.png'; + game.img._progressbar = progressbar_img; + } + + if (!game.run_in_background) { + let pause_img = new Image(); + pause_img.onload = _register_resource('pause.png', game); + pause_img.src = 'pause.png'; + game.img._pause = pause_img; + } + + if (game.hasOwnProperty('save_key')) { + let saveerror_img = new Image(); + saveerror_img.onload = _register_resource('saveerror.png', game); + saveerror_img.src = 'saveerror.png'; + game.img._saveerror = saveerror_img; + } + + let clicktostart_img = new Image(); + clicktostart_img.onload = _register_resource('clicktostart.png', game, function() { + if (game.ready_to_go) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(game.img._clicktostart, 0, 0); + game.ctx.global.restore(); + } + }); + clicktostart_img.src = 'clicktostart.png'; + game.img._clicktostart = clicktostart_img; + + return game; + } + + /* ---- Audio ---- */ + + function _stop_music(game) { + if (game.muted) return; + + for (let m in game.music) { + if (!game.music[m].paused) { + game.music[m].was_playing = true; + game.music[m].pause(); + } else { + game.music[m].was_playing = false; + } + } + } + + function _start_music(game, unmuting) { + if (game.muted && !unmuting) return; + + for (let m in game.music) { + if (game.music[m].was_playing) { + game.music[m].play(); + } + } + } + + function _mute(game) { + _stop_music(game); + game.muted = true; + } + + function _unmute(game) { + game.muted = false; + _start_music(game); + } + + /* ---- Game update stuff ---- */ + + let _last_frame_time; + let _timedelta = 0; + function _loop(game, timestamp) { + if (timestamp == undefined) { + timestamp = 0; + _last_frame_time = timestamp; + } + _timedelta += timestamp - _last_frame_time; + _last_frame_time = timestamp; + + while (_timedelta >= 1000 / game.frame_rate) { + _update(game, 1000 / game.frame_rate); + _timedelta -= 1000 / game.frame_rate; + } + _draw(game); + + if (!game._norun) { + requestAnimFrame(function(timestamp) { + _loop(game, timestamp); + }); + } + } + + function _update(game, delta) { + if (game.update_func) { + game.update_func(delta); + } + + if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].update) { + game.modes[game.mode].update(delta); + } + + if (game.transition.is_transitioning) { + game.transition.timer += delta; + if (game.transition.timer > game.transition.end_time) { + _finish_transition(game); + } + } + } + + function _change_mode(game, newmode) { + game.mode = newmode; + } + + /* ---- UI ---- */ + + function create_buttons(button_data) { + for (let b of button_data.buttons) { + b.state = 0; + } + + return button_data; + } + + /* Call with a button set and given x / y mouse coordinates. + * Will update the button set in place. + * Should generally be called in mouse move and mouse up event handlers. */ + function update_buttons(button_data, x, y) { + for (let button of button_data.buttons) { + if (button.state === 2) continue; + if (button.disabled) continue; + + if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) { + if (!game.touchmode) { + button.state = 1; + } + } else if (button.state !== 2) { + button.state = 0; + } + } + } + + /* Call with a button set and given x / y mouse coordinates. + * If a button is clicked, triggers its callback and returns true. + * Otherwise, returns false. + * Should be called in mouse down event handler. */ + function click_button(button_data, x, y) { + for (let button of button_data.buttons) { + if (button.disabled) continue; + + if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) { + button.state = 2; + game._clicked_button = button; + return true; + } + } + return false; + } + + function _handle_clicked_button(game) { + if (game._clicked_button) { + game._clicked_button.state = 0; + _draw(game); + if (game._clicked_button.callback) { + game._clicked_button.callback(); + } + game._clicked_button = null; + return; + } + } + + /* ---- Mouse ---- */ + + function _handle_mousedown(game, e) { + if (!game.playing) return; + + game.touchmode = false; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1; + if (e.button === 0 && game.events.mousedown) { + game.events.mousedown(game, e, x, y); + } + } + + function _handle_mouseup(game, e) { + if (!game.playing && game.ready_to_go) { + /* Click to start */ + if (game.hasOwnProperty('save_key') && !game._show_save_error) { + /* Test if save/load is working */ + try { + console.log("Test saving"); + localStorage.setItem(game.save_key + ".test", "savetest"); + let savetest = localStorage.getItem(game.save_key + ".test"); + if (savetest !== "savetest") { + throw new Error("oops"); + } + } catch (e) { + /* If error saving, show save error message first */ + /* We set _show_save_error so that the second time the user clicks, + * the game will actually start */ + console.log("- SAVE FAILED -"); + game._show_save_error = true; + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(game.img._saveerror, 0, 0); + game.ctx.global.restore(); + return; + } + } + game.play(); + return; + } + + game.touchmode = false; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1; + + _handle_clicked_button(game); + + if (e.button === 0 && game.events.mouseup) { + game.events.mouseup(game, e, x, y); + } + } + + function _handle_mousemove(game, e) { + game.touchmode = false; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1; + if (game.events.mousemove) { + game.events.mousemove(game, e, x, y); + } + } + + /* ---- Touch (defaults to same as mouse) ---- */ + + let _last_touch_event; + + function _handle_touchstart(game, e) { + if (!game.playing) return; + + game.touchmode = true; + + if (e.touches) e = e.touches[0]; + + _last_touch_event = e; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1; + if (game.events.touchstart) { + game.events.touchstart(game, e, x, y); + } else if (game.events.mousedown) { + if (game.events.mousemove) { + game.events.mousemove(game, e, x, y); + } + game.events.mousedown(game, e, x, y); + } + } + + function _handle_touchend(game, e) { + if (!game.playing && game.ready_to_go) { + /* Click to start */ + game.play(); + return; + } + + game.touchmode = true; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((_last_touch_event.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((_last_touch_event.clientY - rect.top) / rect.height * game.screen_h) - 1; + + _handle_clicked_button(game); + + if (game.events.touchend) { + game.events.touchend(game, e, x, y); + } else if (game.events.mouseup) { + if (game.events.mousemove) { + game.events.mousemove(game, e, x, y); + } + game.events.mouseup(game, e, x, y); + } + } + + function _handle_touchmove(game, e) { + if (!game.playing) return; + + game.touchmode = true; + + if (e.touches) e = e.touches[0]; + + _last_touch_event = e; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1; + let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1; + if (game.events.touchmove) { + game.events.touchmove(game, e, x, y); + } else if (game.events.mousemove) { + game.events.mousemove(game, e, x, y); + } + } + + /* ---- Drawing stuff ---- */ + + function _draw(game) { + game.ctx.draw.save(); + + game.ctx.draw.fillStyle = game.background_color; + + game.ctx.draw.beginPath(); + game.ctx.draw.rect(0, 0, game.screen_w, game.screen_h); + game.ctx.draw.fill(); + + if (game.draw_func) { + game.draw_func(game.ctx.draw); + } + + if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw) { + game.modes[game.mode].draw(game.ctx.draw); + } + + if (game.transition.mid_long) { + game.ctx.draw.fillStyle = game.transition.color; + game.ctx.draw.fillRect(-1, -1, game.canvas_w + 5, game.canvas_h + 5); + } + + game.ctx.draw.restore(); + + if (game.transition.is_transitioning) { + _draw_transition(game, game.ctx.mask); + } else { + game.ctx.mask.drawImage(game.ctx.draw.canvas, 0, 0); + } + + if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw_after_transition) { + game.modes[game.mode].draw_after_transition(game.ctx.mask); + } + + + game.ctx.global.fillStyle = 'rgb(0, 0, 0)'; + game.ctx.global.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(game.ctx.mask.canvas, 0, 0); + + if (game._norun) { + game.ctx.global.drawImage(game.img._pause, 0, 0); + } + + game.ctx.global.restore(); + } + + /* Draw a particular section of an image/spritesheet, + * without having to do as much math or type the destination size */ + function sprite_draw(ctx, img, section_w, section_h, section_x, section_y, dest_x, dest_y) { + ctx.drawImage(img, + section_w * section_x, section_h * section_y, section_w, section_h, + 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); + } + + /* Draw a set of buttons. */ + function button_draw(ctx, button_data) { + for (let button of button_data.buttons) { + if (!button.state) button.state = 0; + let button_state = button.state; + if (button.disabled) button_state = 3; + sprite_draw(ctx, button_data.img, button_data.button_w, button_data.button_h, button.id, button_state, button.x, button.y); + } + } + + /* ---- Save ---- */ + + function _save(game, key, data) { + let save_data; + + if (!game.hasOwnProperty('save_key')) { + throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object"); + } + + try { + save_data = localStorage.getItem(game.save_key) || "{}"; + } catch (e) { + console.error("Saving is disabled; cannot save " + key, e); + return; + } + + try { + save_data = JSON.parse(save_data); + } catch (e) { + console.error("Cannot parse stored save data (when trying to save " + key + ")", e); + return; + } + + save_data[key] = JSON.stringify(data); + + try { + save_data = JSON.stringify(save_data); + } catch (e) { + console.error("Failed to stringify save data (while trying to save " + key + ")", e); + return; + } + + try { + localStorage.setItem(game.save_key, save_data); + } catch (e) { + console.error("Failed to save to localStorage (while saving key " + key + ")", e); + return; + } + } + + function _load(game, key) { + let save_data; + + if (!game.hasOwnProperty('save_key')) { + throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object"); + } + + try { + save_data = localStorage.getItem(game.save_key); + } catch (e) { + console.error("Loading is disabled; cannot load " + key, e); + return; + } + + try { + save_data = JSON.parse(save_data); + } catch (e) { + console.error("Cannot parse stored save data (when trying to load " + key + ")", e); + return; + } + + if (!save_data) { + return null; + } + + return save_data[key]; + } + + /* ---- Transition stuff ---- */ + + let _transition = { + is_transitioning: false, + timer: 0, + color: '#000000', + w: 20, + h: 14, + dir_invert_v: false, + dir_invert_h: false, + invert_shape: true, + mid_long: false, + done_func: null, + type: _draw_fade_transition, + nodraw: false, + end_time: 100, + } + + function _long_transition(game, type, length, callback) { + if (game.transition.is_transitioning) return; + + _draw(game); + + game.transition.invert_shape = false; + _internal_start_transition(game, type, length, function() { + game.transition.mid_long = true; + }, function() { + game.transition.invert_shape = true; + game.transition.is_transitioning = true; + let tdiv = game.transition.dir_invert_v; + let tdih = game.transition.dir_invert_h; + _internal_start_transition(game, type, length, function() { + game.transition.mid_long = false; + callback(); + game.transition.dir_invert_v = tdiv; + game.transition.dir_invert_h = tdih; + }); + }); + } + + function _start_transition(game, type, length, callback, on_done) { + if (game.transition.is_transitioning) return; + if (!game.transition.nodraw) _draw(game); + + _internal_start_transition(game, type, length, callback, on_done); + } + + function _internal_start_transition(game, type, length, callback, on_done) { + if (on_done) { + game.transition.done_func = on_done; + } + + game.transition.timer = 0; + game.transition.type = type; + game.transition.end_time = length; + + game.ctx.copy.drawImage(game.ctx.draw.canvas, 0, 0); + + callback(); + + game.transition.is_transitioning = true; + } + + function _finish_transition(game) { + game.transition.is_transitioning = false; + game.transition.timer = 0; + + if (game.transition.done_func) { + setTimeout(function() { + game.transition.done_func(); + game.transition.done_func = null; + }, 400); + } + } + + function _draw_transition(game, ctx) { + let frac = game.transition.timer / game.transition.end_time; + + ctx.save(); + ctx.clearRect(0, 0, game.screen_w, game.screen_h); + game.transition.type(game, ctx, game.ctx.copy, game.ctx.draw, frac, game.transition.invert_shape); + ctx.restore(); + } + + function _draw_slide_down_transition(game, ctx, oldc, newc, frac, reverse) { + let offset = frac * game.screen_h; + + ctx.drawImage(oldc.canvas, 0, -offset); + ctx.drawImage(newc.canvas, 0, game.screen_h - offset); + } + + function _draw_slide_up_transition(game, ctx, oldc, newc, frac, reverse) { + let offset = frac * game.screen_h; + + ctx.drawImage(oldc.canvas, 0, offset); + ctx.drawImage(newc.canvas, 0, - game.screen_h + offset); + } + + function _draw_fade_transition(game, ctx, oldc, newc, frac, reverse) { + ctx.globalAlpha = 1; + ctx.drawImage(oldc.canvas, 0, 0); + ctx.globalAlpha = frac; + ctx.drawImage(newc.canvas, 0, 0); + } + + /* ---- former contents of ready.js ---- */ + + // Due to Timo Huovinen + // https://stackoverflow.com/questions/799981/document-ready-equivalent-without-jquery + var ready = (function(){ + + var readyList, + DOMContentLoaded, + class2type = {}; + class2type["[object Boolean]"] = "boolean"; + class2type["[object Number]"] = "number"; + class2type["[object String]"] = "string"; + class2type["[object Function]"] = "function"; + class2type["[object Array]"] = "array"; + class2type["[object Date]"] = "date"; + class2type["[object RegExp]"] = "regexp"; + class2type["[object Object]"] = "object"; + + var ReadyObj = { + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + ReadyObj.readyWait++; + } else { + ReadyObj.ready( true ); + } + }, + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--ReadyObj.readyWait) || (wait !== true && !ReadyObj.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( ReadyObj.ready, 1 ); + } + + // Remember that the DOM is ready + ReadyObj.isReady = true; + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --ReadyObj.readyWait > 0 ) { + return; + } + // If there are functions bound, to execute + readyList.resolveWith( document, [ ReadyObj ] ); + + // Trigger any bound ready events + //if ( ReadyObj.fn.trigger ) { + // ReadyObj( document ).trigger( "ready" ).unbind( "ready" ); + //} + } + }, + bindReady: function() { + if ( readyList ) { + return; + } + readyList = ReadyObj._Deferred(); + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( ReadyObj.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + // A fallback to window.onload, that will always work + window.addEventListener( "load", ReadyObj.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", ReadyObj.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + _Deferred: function() { + var // callbacks list + callbacks = [], + // stored [ context , args ] + fired, + // to avoid firing when already doing so + firing, + // flag to know if the deferred has been cancelled + cancelled, + // the deferred itself + deferred = { + + // done( f1, f2, ...) + done: function() { + if ( !cancelled ) { + var args = arguments, + i, + length, + elem, + type, + _fired; + if ( fired ) { + _fired = fired; + fired = 0; + } + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = ReadyObj.type( elem ); + if ( type === "array" ) { + deferred.done.apply( deferred, elem ); + } else if ( type === "function" ) { + callbacks.push( elem ); + } + } + if ( _fired ) { + deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] ); + } + } + return this; + }, + + // resolve with given context and args + resolveWith: function( context, args ) { + if ( !cancelled && !fired && !firing ) { + // make sure args are available (#8421) + args = args || []; + firing = 1; + try { + while( callbacks[ 0 ] ) { + callbacks.shift().apply( context, args );//shifts a callback, and applies it to document + } + } + finally { + fired = [ context, args ]; + firing = 0; + } + } + return this; + }, + + // resolve with this as context and given arguments + resolve: function() { + deferred.resolveWith( this, arguments ); + return this; + }, + + // Has this deferred been resolved? + isResolved: function() { + return !!( firing || fired ); + }, + + // Cancel + cancel: function() { + cancelled = 1; + callbacks = []; + return this; + } + }; + + return deferred; + }, + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ Object.prototype.toString.call(obj) ] || "object"; + } + } + // The DOM ready check for Internet Explorer + function doScrollCheck() { + if ( ReadyObj.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + ReadyObj.ready(); + } + // Cleanup functions for the document ready method + if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + ReadyObj.ready(); + }; + + } else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + ReadyObj.ready(); + } + }; + } + function ready( fn ) { + // Attach the listeners + ReadyObj.bindReady(); + + var type = ReadyObj.type( fn ); + + // Add the callback + readyList.done( fn );//readyList is result of _Deferred() + } + return ready; + })(); + + return { + create_game: create_game, + + ready: ready, + mod: mod, + sgn: sgn, + as_hex: as_hex, + copy_list: copy_list, + copy_flat_objlist: copy_flat_objlist, + + sprite_draw: sprite_draw, + screen_draw: screen_draw, + + buttons: { + create: create_buttons, + update: update_buttons, + click: click_button, + draw: button_draw, + }, + + transition: { + type: { + fade: _draw_fade_transition, + slide_up: _draw_slide_up_transition, + slide_down: _draw_slide_down_transition, + }, + }, + } +})();