From: cassowarii Date: Sun, 1 Oct 2023 22:18:51 +0000 (-0700) Subject: Jam version X-Git-Url: https://www.git.cassowary.me/gitweb.cgi?a=commitdiff_plain;h=edd7f6849164a9e6a31f454ee574cee3d6052e51;p=ld54.git Jam version --- edd7f6849164a9e6a31f454ee574cee3d6052e51 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1fbeb22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.swp +packaging.zip diff --git a/clicktostart.png b/clicktostart.png new file mode 100644 index 0000000..213139e Binary files /dev/null and b/clicktostart.png differ diff --git a/cover.png b/cover.png new file mode 100644 index 0000000..1b58a2a Binary files /dev/null and b/cover.png differ diff --git a/covre.xcf b/covre.xcf new file mode 100644 index 0000000..c29c590 Binary files /dev/null and b/covre.xcf differ diff --git a/game.js b/game.js new file mode 100644 index 0000000..f1e5592 --- /dev/null +++ b/game.js @@ -0,0 +1,1102 @@ +"use strict"; + +let game; + +let game_started = false; + +let level_number = 1; +let intitle = true; +let inend = false; + +let map; + +/* state: + * STAND: waiting for input + * DRAG: dragging hare path + * HOPDIR: thinking about where to hop + * HOP: hop animation + * WIN: won level + */ +let State = { STAND: 0, DRAG: 1, HOPDIR: 2, HOP: 3, WIN: 4 }; + +let save_data = 1; +const SAVE_KEY = "casso.haregame.save"; + +zb.ready(function() { + game = zb.create_game({ + canvas: 'canvas', + canvas_w: 648, + canvas_h: 720, + draw_scale: 3, + tile_size: 24, + level_w: 9, + level_h: 9, + background_color: 'black', + draw_func: do_draw, + update_func: do_update, + run_in_background: true, + //save_key: SAVE_KEY, + state: State.STAND, + events: { + keydown: handle_keydown, + keyup: handle_keyup, + mouseup: handle_mouseup, + mousedown: handle_mousedown, + mousemove: handle_mousemove, + mouseleave: handle_mouseleave, + gamestart: handle_gamestart, + }, + }); + + game.buttons = { + undo: { + id: 0, + state: 0, + x: game.screen_w - 40, + y: -20, + w: 16, + h: 16, + callback: undo, + }, + reset: { + id: 1, + state: 0, + x: game.screen_w - 20, + y: -20, + w: 16, + h: 16, + callback: reset, + }, + }; + + game.register_sfx({ + jump: { + path: 'sfx/jump.mp3', + volume: 0.4, + }, + push: { + path: 'sfx/push.mp3', + volume: 0.1, + }, + whew: { + path: 'sfx/whew.mp3', + volume: 1, + }, + cronch: { + path: 'sfx/cronch.mp3', + volume: 1, + }, + alert_sound: { + path: 'sfx/alert.mp3', + volume: 0.8, + }, + youwon: { + path: 'sfx/haregameyouwon.mp3', + volume: 0.2, + }, + }); + + game.register_images({ + selector: 'img/selector.png', + hare_white: 'img/rabbitswhite.png', + hare_brown: 'img/rabbitsbrown.png', + ground_tile: 'img/ground_tile.png', + water_tile: 'img/water_tile.png', + snow_tile: 'img/snow_tile.png', + patchy_tile: 'img/patchy_tile.png', + sign_tile: 'img/sign_tile.png', + frontgrass: 'img/frontgrass.png', + backgrass: 'img/backgrass.png', + welldone: 'img/welldone.png', + clicktocontinue: 'img/clicktocontinue.png', + title: 'img/title.png', + end: 'img/end.png', + buttons: 'img/buttons.png', + level: { + 1: 'img/level/1.png', + 2: 'img/level/2.png', + 3: 'img/level/3.png', + 4: 'img/level/4.png', + 5: 'img/level/5.png', + 6: 'img/level/6.png', + 7: 'img/level/7.png', + 8: 'img/level/8.png', + 9: 'img/level/9.png', + 10: 'img/level/10.png', + } + }); + + game.register_music({ + song: { + path: 'music/haregamesong', + volume: 0.2, + }, + nervous: { + path: 'sfx/nervous', + volume: 0.6, + }, + }); + + game.resources_ready(); +}); + +/* ---- constants ---- */ + +let TOP_BAR_SIZE = 24; /* height of top bar */ + +let CURSOR_CHANGE_SPEED = 300; +let WATER_CHANGE_SPEED = 1000; + +let HARE_LAST_HOP_FRAME = 5; /* index of last frame before hop ends */ +let HARE_OFFSET = 3; /* hare vertical offset from tile */ +let HARE_STAND_FRAME_LENGTH = 50; /* frames when standing to anxious state */ +let HARE_QUIVER_FRAME_LENGTH = 100; /* frame length for nervous animation */ +let HARE_SIT_FRAME_LENGTH = 100; /* frames when sitting from anxious state */ + +let GRASS_OFFSET = 2; /* how much draw grass up from grid ?? */ + +let Tile = { + WATER: 0, + GROUND: 1, + SNOW: 2, + PATCHY: 3, + SIGN: 4, +} + +let HareType = { + WHITE: 0, + BROWN: 1, +} + +let Dir = { + DOWN: 0, + LEFT: 1, + RIGHT: 2, + UP: 3, +} + +/* ---- global state vars ---- */ + +let mouse_x = null, mouse_y = null; + +let selector = { + x: null, + y: null, + frame: 0, + timer: 0, +}; + +let path = []; +let path_extra = []; + +let hares = []; +let grass = []; +let backgrass = []; + +let hop_target = null; +let hop_frame_length = 100; + +let push_fraction = 0; /* for calculation of how far we've pushed our friend */ + +let water = { + frame: 0, + timer: 0, +} + +let win_title_offset; +let ctc_alpha; + +let playing_anxious_sound = false; + +/* ---- random convenience funcs ---- */ + +function delete_save() { + try { + // delete save + } catch (e) { + console.error("oops, can't save! though that uh... doesn't matter here"); + } +} + +function save() { + try { + // save + } catch (e) { + console.error("oops, can't save!", e); + } +} + +function handle_gamestart(game) { + console.log("Game start!"); + + game.music.song.play(); + + // initialize game + load_level_data(levels.title); +} + +let undo_stack = []; + +function create_undo_point() { + let undo_point = {}; + + undo_point.hares = zb.copy_flat_objlist(hares); + undo_point.grass = zb.copy_flat_objlist(grass); + + undo_stack.push(undo_point); +} + +function undo() { + // perform undo + if (undo_stack.length) { + let state = undo_stack.pop(); + + hares = state.hares; + grass = state.grass; + backgrass = []; + for (let g of state.grass) { + backgrass.push({ + is_backgrass: true, + x: g.x, + y: g.y, + eaten: g.eaten, + }); + } + + game.state = State.STAND; + path = []; + path_extra = []; + update_selector(mouse_x, mouse_y); + } +} + +function reset() { + game.start_transition(zb.transition.FADE, 500, function() { + load_level(); + game.state = State.STAND; + }); +} + +function advance_level() { + if (level_number === 10) { + win_everything(); + } else { + game.long_transition(zb.transition.FADE, 350, function() { + undo_stack = []; + level_number ++; + load_level(); + game.state = State.STAND; + }); + } +} + +function win_everything() { + game.long_transition(zb.transition.FADE, 1000, function() { + inend = true; + load_level_data(levels.end); + }); +} + +function load_level() { + if (level_number > Object.keys(levels).length) { + win_everything(); + } else { + console.log(levels); + console.log(level_number); + console.log(levels[level_number]); + load_level_data(levels[level_number]); + } +} + +function load_level_data(lvl) { + game.state = State.STAND; + + map = lvl.map; + console.log("map:", map); + + path = []; + path_extra = []; + + hares = []; + for (let h of lvl.hares) { + create_hare(h.x, h.y, h.color === 'brown' ? HareType.BROWN : HareType.WHITE); + } + + grass = []; + backgrass = []; + for (let g of lvl.grass) { + create_grass(g.x, g.y); + } +} + +function create_hare(x, y, type) { + hares.push({ + is_hare: true, + x: x, + y: y, + type: type, + dir: Dir.DOWN, + frame: 0, + timer: 0, + }); +} + +function create_grass(x, y) { + grass.push({ + is_grass: true, + x: x, + y: y, + eaten: false, + }); + + backgrass.push({ + is_backgrass: true, + x: x, + y: y, + eaten: false, + }); +} + +function hare_matches_tile(hare, tile) { + return tile === Tile.PATCHY + || hare_strictly_matches_tile(hare, tile); +} + +function hare_strictly_matches_tile(hare, tile) { + return hare.type === HareType.BROWN && tile === Tile.GROUND + || hare.type === HareType.WHITE && tile === Tile.SNOW; +} + +/* ---- buttons ---- */ + + +/* ---- thing finding funcs ---- */ + +function tile_at(x, y) { + if (x < 0 || x >= game.level_w || y < 0 || y >= game.level_h) { + return Tile.WATER; + } + return map[y * game.level_w + x]; +} + +function hare_at(x, y) { + let list = hares.filter(h => h.x === x && h.y === y); + if (list.length) { + return list[0]; + } else { + return null; + } +} + +function grass_at(x, y) { + let frontlist = grass.filter(g => g.x === x && g.y === y); + let backlist = backgrass.filter(g => g.x === x && g.y === y); + if (frontlist.length && backlist.length) { + return { front: frontlist[0], back: backlist[0] }; + } else { + return null; + } +} + +function can_push_to(x, y) { + return tile_at(x, y) !== Tile.WATER; +} + +/* ---- update ---- */ + +/* MAIN UPDATE FUNCTION */ +function do_update(delta) { + selector.timer += delta; + while (selector.timer > CURSOR_CHANGE_SPEED) { + selector.timer -= CURSOR_CHANGE_SPEED; + selector.frame ++; + selector.frame = zb.mod(selector.frame, 2); + } + + water.timer += delta; + while (water.timer > WATER_CHANGE_SPEED) { + water.timer -= WATER_CHANGE_SPEED; + water.frame ++; + water.frame = zb.mod(water.frame, 2); + } + + update_hare_anxiety(delta); + + update_hare_movement(delta); + + if (game.state === State.WIN) { + if (win_title_offset > 1) { + win_title_offset /= 1.08; + ctc_alpha = 0; + } else { + win_title_offset = 0; + if (ctc_alpha < 1) { + ctc_alpha += delta / 500; + } else { + ctc_alpha = 1; + } + } + } +} + +function update_hare_movement(delta) { + if (game.state === State.HOPDIR) { + push_fraction = 0; + for (let h of hares) { + if (h.being_pushed) { + h.x = h.target_x; + h.y = h.target_y; + delete h.target_x; + delete h.target_y; + } + h.being_pushed = false; + } + + if (path.length > 0) { + let next = path.shift(); + let dx, dy; + if (next.x === hop_target.x - 1) { + hop_target.dir = Dir.LEFT; + dx = -1; dy = 0; + } else if (next.x === hop_target.x + 1) { + hop_target.dir = Dir.RIGHT; + dx = 1; dy = 0; + } else if (next.y === hop_target.y - 1) { + hop_target.dir = Dir.UP; + dx = 0; dy = -1; + } else { + hop_target.dir = Dir.DOWN; + dx = 0; dy = 1; + } + game.state = State.HOP; + game.sfx.jump.play(); + hop_target.frame = 1; + hop_target.timer = 0; + hop_target.target_x = next.x; + hop_target.target_y = next.y; + let h = hare_at(next.x, next.y); + if (h) { + if (can_push_to(h.x + dx, h.y + dy)) { + h.target_x = h.x + dx; + h.target_y = h.y + dy; + h.being_pushed = true; + game.sfx.push.play(); + } else { + cancel_move(); + } + } + } else { + cancel_move(); + } + } else if (game.state === State.HOP) { + hop_target.timer += delta; + while (hop_target.timer > hop_frame_length) { + hop_target.timer -= hop_frame_length; + hop_target.frame ++; + push_fraction = (hop_target.frame - hop_target.timer / hop_frame_length) / (HARE_LAST_HOP_FRAME + 1); + if (hop_target.frame > HARE_LAST_HOP_FRAME) { + game.state = State.HOPDIR; + hop_target.frame = 1; + hop_target.timer = 0; + hop_target.x = hop_target.target_x; + hop_target.y = hop_target.target_y; + if (!hare_matches_tile(hop_target, tile_at(hop_target.x, hop_target.y))) { + hop_target.anxiety = true; + path = []; + path_extra = []; + } else { + /* Eat any grass on the tile */ + eat_grass(hop_target.x, hop_target.y); + } + break; + } + } + } +} + +function eat_grass(x, y) { + let g = grass_at(x, y); + if (g) { + if (!g.front.eaten) { + game.sfx.cronch.play(); + } + g.front.eaten = true; + g.back.eaten = true; + } + check_victory(); +} + +function cancel_move() { + selector.x = null; + selector.y = null; + path = []; + path_extra = []; + update_selector(mouse_x, mouse_y); + game.state = State.STAND; + hop_target.frame = 0; + hop_target.timer = 0; +} + +function update_hare_anxiety(delta) { + /* Hare Anxiety */ + let any_anxiety = false; + for (let h of hares) { + if (hare_matches_tile(h, tile_at(h.x, h.y))) { + h.anxiety = false; + } else { + h.anxiety = true; + any_anxiety = true; + } + + if (h.anxiety && h.frame < 7) { + h.frame = 7; + game.sfx.alert_sound.play(); + } else if (h.anxiety && h.frame < 10) { + h.timer += delta; + while (h.timer > HARE_STAND_FRAME_LENGTH && h.frame < 10) { + h.timer -= HARE_STAND_FRAME_LENGTH + h.frame ++; + } + if (h.frame === 10) { + h.timer = 0; + } + } else if (h.anxiety) { + /* alternate between frames 10 and 11 */ + h.timer += delta; + while (h.timer > HARE_QUIVER_FRAME_LENGTH) { + h.timer -= HARE_QUIVER_FRAME_LENGTH; + if (h.frame === 10) { + h.frame = 11; + } else { + h.frame = 10; + } + } + } else if (!h.anxiety && h.frame === 10 || h.frame === 11) { + h.frame = 12; + h.timer = 0; + game.sfx.whew.play(); + } else if (!h.anxiety && h.frame > 7) { + h.timer += delta; + while (h.timer > HARE_SIT_FRAME_LENGTH) { + h.timer -= HARE_SIT_FRAME_LENGTH + h.frame ++; + if (h.frame > 14) { + h.frame = 0; + h.timer = 0; + eat_grass(h.x, h.y); + } + } + } + } + + if (any_anxiety && !playing_anxious_sound) { + game.music.nervous.play(); + playing_anxious_sound = true; + } else if (!any_anxiety && playing_anxious_sound) { + game.music.nervous.pause(); + playing_anxious_sound = false; + } +} + +function check_victory() { + for (let g of grass) { + if (!g.eaten) { + return false; + } + } + + for (let h of hares) { + if (!hare_strictly_matches_tile(h, tile_at(h.x, h.y))) { + return false; + } + } + + win(); +} + +function win() { + for (let h of hares) { + h.frame = 0; + } + + console.log("You won!"); + win_title_offset = game.screen_h; + game.sfx.youwon.play(); + game.state = State.WIN; +} + +/* ---- draw ---- */ + +/* DRAW */ +function do_draw(ctx) { + ctx.save(); + ctx.translate(0, TOP_BAR_SIZE); + + draw_map(ctx); + + draw_objects(ctx); + + draw_selector(ctx); + + draw_ui(ctx); + + ctx.restore(); +} + +function rpgmaker_tile_format_thing(x, y, tile_type) { + let u = tile_at(x, y - 1); + let d = tile_at(x, y + 1); + let l = tile_at(x - 1, y); + let r = tile_at(x + 1, y); + let ul = tile_at(x - 1, y - 1); + let ur = tile_at(x + 1, y - 1); + let dl = tile_at(x - 1, y + 1); + let dr = tile_at(x + 1, y + 1); + let ulx, uly, urx, ury, dlx, dly, drx, dry; + + /* Draw top left */ + if (u === tile_type) { + if (l === tile_type) { + if (ul === tile_type) { + /* Open water */ + ulx = 2; uly = 4; + } else { + /* Upper left corner */ + ulx = 2; uly = 0; + } + } else { + /* Left border */ + ulx = 0; uly = 4; + } + } else { + if (l === tile_type) { + /* Top border */ + ulx = 2; uly = 2; + } else { + /* Very corner */ + ulx = 0; uly = 2; + } + } + + /* Draw top right */ + if (u === tile_type) { + if (r === tile_type) { + if (ur === tile_type) { + /* Open water */ + urx = 1; ury = 4; + } else { + /* Upper right corner */ + urx = 3; ury = 0; + } + } else { + /* Right border */ + urx = 3; ury = 4; + } + } else { + if (r === tile_type) { + /* Top border */ + urx = 1; ury = 2; + } else { + /* Very corner */ + urx = 3; ury = 2; + } + } + + /* Draw bottom left */ + if (d === tile_type) { + if (l === tile_type) { + if (dl === tile_type) { + /* Open water */ + dlx = 2; dly = 3; + } else { + /* Bottom left corner */ + dlx = 2; dly = 1; + } + } else { + /* Left border */ + dlx = 0; dly = 3; + } + } else { + if (l === tile_type) { + /* Bottom border */ + dlx = 2; dly = 5; + } else { + /* Very corner */ + dlx = 0; dly = 5; + } + } + + /* Draw bottom right */ + if (d === tile_type) { + if (r === tile_type) { + if (dr === tile_type) { + /* Open water */ + drx = 1; dry = 3; + } else { + /* Bottom right corner */ + drx = 3; dry = 1; + } + } else { + /* Right border */ + drx = 3; dry = 3; + } + } else { + if (r === tile_type) { + /* 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_map(ctx) { + for (let y = 0; y < game.level_h; y++) { + for (let x = 0; x < game.level_w; x++) { + if ((x + y) % 2 == 1) { + ctx.fillStyle = 'cornflowerblue'; + ctx.fillRect(x * game.tile_size, y * game.tile_size, game.tile_size, game.tile_size); + } + } + } + + let ts = game.tile_size; + for (let y = 0; y < game.level_h; y++) { + for (let x = 0; x < game.level_w; x++) { + if (tile_at(x, y) === Tile.GROUND) { + zb.sprite_draw(ctx, game.img.ground_tile, ts, ts, 0, (x + y) % 2, x * ts, y * ts); + } else if (tile_at(x, y) === Tile.WATER) { + let { ulx, uly, urx, ury, dlx, dly, drx, dry } = rpgmaker_tile_format_thing(x, y, Tile.WATER); + /* Correct for frames with water animation. I really need to put tile/sprite animations + * into the actual engine, this stuff always turns into spaghetti code */ + uly += 6 * water.frame; + ury += 6 * water.frame; + dly += 6 * water.frame; + dry += 6 * water.frame; + zb.sprite_draw(ctx, game.img.water_tile, ts / 2, ts / 2, ulx, uly, x * ts, y * ts); + zb.sprite_draw(ctx, game.img.water_tile, ts / 2, ts / 2, urx, ury, x * ts + ts / 2, y * ts); + zb.sprite_draw(ctx, game.img.water_tile, ts / 2, ts / 2, dlx, dly, x * ts, y * ts + ts / 2); + zb.sprite_draw(ctx, game.img.water_tile, ts / 2, ts / 2, drx, dry, x * ts + ts / 2, y * ts + ts / 2); + } else if (tile_at(x, y) === Tile.SNOW) { + let { ulx, uly, urx, ury, dlx, dly, drx, dry } = rpgmaker_tile_format_thing(x, y, Tile.SNOW); + zb.sprite_draw(ctx, game.img.snow_tile, ts / 2, ts / 2, ulx, uly, x * ts, y * ts); + zb.sprite_draw(ctx, game.img.snow_tile, ts / 2, ts / 2, urx, ury, x * ts + ts / 2, y * ts); + zb.sprite_draw(ctx, game.img.snow_tile, ts / 2, ts / 2, dlx, dly, x * ts, y * ts + ts / 2); + zb.sprite_draw(ctx, game.img.snow_tile, ts / 2, ts / 2, drx, dry, x * ts + ts / 2, y * ts + ts / 2); + } else if (tile_at(x, y) === Tile.PATCHY) { + let { ulx, uly, urx, ury, dlx, dly, drx, dry } = rpgmaker_tile_format_thing(x, y, Tile.PATCHY); + zb.sprite_draw(ctx, game.img.patchy_tile, ts / 2, ts / 2, ulx, uly, x * ts, y * ts); + zb.sprite_draw(ctx, game.img.patchy_tile, ts / 2, ts / 2, urx, ury, x * ts + ts / 2, y * ts); + zb.sprite_draw(ctx, game.img.patchy_tile, ts / 2, ts / 2, dlx, dly, x * ts, y * ts + ts / 2); + zb.sprite_draw(ctx, game.img.patchy_tile, ts / 2, ts / 2, drx, dry, x * ts + ts / 2, y * ts + ts / 2); + } else if (tile_at(x, y) === Tile.SIGN) { + let { ulx, uly, urx, ury, dlx, dly, drx, dry } = rpgmaker_tile_format_thing(x, y, Tile.SIGN); + zb.sprite_draw(ctx, game.img.sign_tile, ts / 2, ts / 2, ulx, uly, x * ts, y * ts); + zb.sprite_draw(ctx, game.img.sign_tile, ts / 2, ts / 2, urx, ury, x * ts + ts / 2, y * ts); + zb.sprite_draw(ctx, game.img.sign_tile, ts / 2, ts / 2, dlx, dly, x * ts, y * ts + ts / 2); + zb.sprite_draw(ctx, game.img.sign_tile, ts / 2, ts / 2, drx, dry, x * ts + ts / 2, y * ts + ts / 2); + } + } + } + + if (game.img.level[level_number] && !intitle && !inend) { + ctx.save(); + ctx.translate(0, -TOP_BAR_SIZE); + zb.screen_draw(ctx, game.img.level[level_number]); + ctx.restore(); + } +} + +function draw_objects(ctx) { + /* dumb way to do this */ + let objs = [ ...backgrass, ...hares, ...grass ]; + objs.sort((a, b) => { + let ay = a.y; + if (a.target_y && a.target_y > a.y) { + ay = a.target_y; + } + let by = b.y; + if (b.target_y && b.target_y > b.y) { + by = b.target_y; + } + return ay - by; + }); + + for (let o of objs) { + if (o.is_hare) { + draw_hare(ctx, o); + } else if (o.is_backgrass) { + draw_backgrass(ctx, o); + } else if (o.is_grass) { + draw_grass(ctx, o); + } + } +} + +function draw_hare(ctx, h) { + let img; + if (h.type === HareType.WHITE) { + img = game.img.hare_white; + } else if (h.type === HareType.BROWN) { + img = game.img.hare_brown; + } + + let ts = game.tile_size; + let draw_x = h.x * ts - ts; + let draw_y = h.y * ts - ts - HARE_OFFSET; + if (h.being_pushed) { + let pf = push_fraction; + /* Slide them along in accordance with push_fraction if they're being pushed */ + draw_x = (h.x * (1 - pf) + h.target_x * pf) * ts - ts; + draw_y = (h.y * (1 - pf) + h.target_y * pf) * ts - ts - HARE_OFFSET; + } + + zb.sprite_draw(ctx, img, 72, 72, h.dir, h.frame, draw_x, draw_y); +} + +function draw_backgrass(ctx, g) { + let ts = game.tile_size; + zb.sprite_draw(ctx, game.img.backgrass, 72, 72, 0, g.eaten ? 1 : 0, g.x * ts - ts, g.y * ts - ts - GRASS_OFFSET); +} + +function draw_grass(ctx, g) { + let ts = game.tile_size; + zb.sprite_draw(ctx, game.img.frontgrass, 72, 72, 0, g.eaten ? 1 : 0, g.x * ts - ts, g.y * ts - ts - GRASS_OFFSET); +} + +function draw_selector(ctx) { + let ts = game.tile_size; + + if (game.state === State.STAND || game.state === State.DRAG) { + if (selector.x !== null && selector.y !== null) { + zb.sprite_draw(ctx, game.img.selector, 72, 72, 0, selector.frame, selector.x * ts - ts, selector.y * ts - ts); + } + } + + if (game.state === State.DRAG) { + for (let component of path) { + zb.sprite_draw(ctx, game.img.selector, 72, 72, 0, 2, component.x * ts - ts, component.y * ts - ts); + } + + for (let component of path_extra) { + zb.sprite_draw(ctx, game.img.selector, 72, 72, 0, 3 + component.dir, component.x * ts - ts, component.y * ts - ts); + } + } +} + +function draw_ui(ctx) { + if (game.state === State.WIN) { + ctx.save(); + ctx.translate(0, Math.round(-win_title_offset)); + zb.screen_draw(ctx, game.img.welldone); + if (win_title_offset === 0) { + ctx.save(); + ctx.globalAlpha = ctc_alpha; + zb.screen_draw(ctx, game.img.clicktocontinue); + ctx.restore(); + } + ctx.restore(); + } + + if (intitle) { + ctx.save(); + ctx.translate(0, -TOP_BAR_SIZE); + zb.screen_draw(ctx, game.img.title); + ctx.restore(); + } else if (inend) { + ctx.save(); + ctx.translate(0, -TOP_BAR_SIZE); + zb.screen_draw(ctx, game.img.end); + ctx.restore(); + } else { + /* Reset button */ + zb.sprite_draw(ctx, game.img.buttons, 16, 16, 1, game.buttons.reset.state, game.screen_w - 20, -20); + /* Undo button */ + zb.sprite_draw(ctx, game.img.buttons, 16, 16, 0, game.buttons.undo.state, game.screen_w - 40, -20); + } +} + + +/* ---- event handlers ---- */ + +function handle_keydown(game, e) { + // key down +} + +let x_pressed = false; +function handle_keyup(game, e) { + // key up + switch (e.key) { + case 'm': + game.toggle_mute(); + e.preventDefault(); + break; + case 'r': + reset(); + e.preventDefault(); + break; + case 'z': + undo(); + e.preventDefault(); + break; + case 'x': + x_pressed = true; + break; + case 'w': + if (x_pressed) { + delete_save(); + e.preventDefault(); + } + break; + } + + if (e.keyCode !== 88) { + /* non-X key */ + x_pressed = false; + } +} + +function handle_mousedown(game) { + if (intitle) { + game.long_transition(zb.transition.FADE, 1000, function() { + intitle = false; + load_level(); + }); + } + + if (inend) { + game.long_transition(zb.transition.FADE, 1000, function() { + inend = false; + intitle = true; + level_number = 1; + load_level_data(levels.title); + }); + } + + for (let b of Object.values(game.buttons)) { + if (b.state === 1) { + b.state = 2; + return; + } + } + + if (game.state === State.STAND) { + if (selector.x !== null && selector.y !== null) { + let hare = hare_at(selector.x, selector.y); + if (hare && hare_matches_tile(hare, tile_at(hare.x, hare.y))) { + game.state = State.DRAG; + hop_target = hare; + } + } + } + + if (game.state === State.WIN && win_title_offset === 0) { + advance_level(); + } +} + +function handle_mouseup(game) { + for (let b of Object.values(game.buttons)) { + if (b.state === 2) { + b.callback(); + b.state = 1; + return; + } + } + + if (game.state === State.DRAG) { + if (path.length > 0) { + create_undo_point(); + game.state = State.HOPDIR; + hop_frame_length = Math.max(5, 100 - Math.log(path.length) / Math.log(1.3) * 7); + } else { + game.state = State.STAND; + } + } +} + +function update_selector(x, y) { + let tile_x = Math.floor(x / game.tile_size); + let tile_y = Math.floor(y / game.tile_size); + + selector.x = null; + selector.y = null; + + if (intitle || inend) return; + + if (0 <= tile_x && tile_x < game.level_w && 0 <= tile_y && tile_y < game.level_h) { + selector.x = tile_x; + selector.y = tile_y; + } +} + +function handle_mousemove(game, e, x, canvy) { + let y = canvy - TOP_BAR_SIZE; + + mouse_x = x; + mouse_y = y; + + for (let b of Object.values(game.buttons)) { + if (mouse_x >= b.x && mouse_x < b.x + b.w && mouse_y >= b.y && mouse_y < b.y + b.h) { + b.state = 1; + } else { + b.state = 0; + } + } + + if (game.state === State.STAND) { + update_selector(x, y); + } else if (game.state === State.DRAG) { + let tile_x = Math.floor(x / game.tile_size); + let tile_y = Math.floor(y / game.tile_size); + + let tile_list = [ selector, ...path ]; + let last = tile_list[tile_list.length - 1]; + + let revert = { x: null, y: null }; + if (tile_list.length > 1) { + revert = tile_list[tile_list.length - 2]; + } + + if (tile_x === revert.x && tile_y === revert.y) { + path.pop(); + path_extra.pop(); + } else if (tile_x !== last.x || tile_y !== last.y) { + if (tile_x === last.x - 1 && tile_y === last.y + || tile_x === last.x + 1 && tile_y === last.y + || tile_x === last.x && tile_y === last.y + 1 + || tile_x === last.x && tile_y === last.y - 1) { + if (0 <= tile_x && tile_x < game.level_w && 0 <= tile_y && tile_y < game.level_h) { + if (tile_at(tile_x, tile_y) !== Tile.WATER) { + if (hare_matches_tile(hop_target, tile_at(last.x, last.y))) { + path.push({ x: tile_x, y: tile_y }); + if (last.x - 1 === tile_x) { + path_extra.push({ x: last.x, y: last.y, dir: Dir.LEFT }); + } else if (last.x + 1 === tile_x) { + path_extra.push({ x: last.x, y: last.y, dir: Dir.RIGHT }); + } else if (last.y - 1 === tile_y) { + path_extra.push({ x: last.x, y: last.y, dir: Dir.UP }); + } else if (last.y + 1 === tile_y) { + path_extra.push({ x: last.x, y: last.y, dir: Dir.DOWN }); + } + } + } + } + } + } + } +} + +function handle_mouseleave(game, e) { + if (game.state === State.STAND) { + selector.x = null; + selector.y = null; + } + + for (let b of Object.values(game.buttons)) { + b.state = 0; + } +} diff --git a/harefont.ttf b/harefont.ttf new file mode 100644 index 0000000..40af6aa Binary files /dev/null and b/harefont.ttf differ diff --git a/harefont.zip b/harefont.zip new file mode 100644 index 0000000..86cd2b0 Binary files /dev/null and b/harefont.zip differ diff --git a/img/backgrass.png b/img/backgrass.png new file mode 100644 index 0000000..a9b25d7 Binary files /dev/null and b/img/backgrass.png differ diff --git a/img/buttons.aseprite b/img/buttons.aseprite new file mode 100644 index 0000000..f4a4101 Binary files /dev/null and b/img/buttons.aseprite differ diff --git a/img/buttons.png b/img/buttons.png new file mode 100644 index 0000000..d693863 Binary files /dev/null and b/img/buttons.png differ diff --git a/img/clicktocontinue.aseprite b/img/clicktocontinue.aseprite new file mode 100644 index 0000000..462bb65 Binary files /dev/null and b/img/clicktocontinue.aseprite differ diff --git a/img/clicktocontinue.png b/img/clicktocontinue.png new file mode 100644 index 0000000..87a3089 Binary files /dev/null and b/img/clicktocontinue.png differ diff --git a/img/end.aseprite b/img/end.aseprite new file mode 100644 index 0000000..540db19 Binary files /dev/null and b/img/end.aseprite differ diff --git a/img/end.png b/img/end.png new file mode 100644 index 0000000..b4a37eb Binary files /dev/null and b/img/end.png differ diff --git a/img/frontgrass.png b/img/frontgrass.png new file mode 100644 index 0000000..0c9cc7c Binary files /dev/null and b/img/frontgrass.png differ diff --git a/img/grass.aseprite b/img/grass.aseprite new file mode 100644 index 0000000..363caf2 Binary files /dev/null and b/img/grass.aseprite differ diff --git a/img/grass.png b/img/grass.png new file mode 100644 index 0000000..7ca5d8d Binary files /dev/null and b/img/grass.png differ diff --git a/img/ground_tile.aseprite b/img/ground_tile.aseprite new file mode 100644 index 0000000..ff0381e Binary files /dev/null and b/img/ground_tile.aseprite differ diff --git a/img/ground_tile.png b/img/ground_tile.png new file mode 100644 index 0000000..d5ae362 Binary files /dev/null and b/img/ground_tile.png differ diff --git a/img/level/1.aseprite b/img/level/1.aseprite new file mode 100644 index 0000000..03c6aea Binary files /dev/null and b/img/level/1.aseprite differ diff --git a/img/level/1.png b/img/level/1.png new file mode 100644 index 0000000..29ee1ca Binary files /dev/null and b/img/level/1.png differ diff --git a/img/level/10.aseprite b/img/level/10.aseprite new file mode 100644 index 0000000..4839725 Binary files /dev/null and b/img/level/10.aseprite differ diff --git a/img/level/10.png b/img/level/10.png new file mode 100644 index 0000000..47fa034 Binary files /dev/null and b/img/level/10.png differ diff --git a/img/level/2.aseprite b/img/level/2.aseprite new file mode 100644 index 0000000..dd2e7dc Binary files /dev/null and b/img/level/2.aseprite differ diff --git a/img/level/2.png b/img/level/2.png new file mode 100644 index 0000000..058dd77 Binary files /dev/null and b/img/level/2.png differ diff --git a/img/level/3.aseprite b/img/level/3.aseprite new file mode 100644 index 0000000..1bbcf7c Binary files /dev/null and b/img/level/3.aseprite differ diff --git a/img/level/3.png b/img/level/3.png new file mode 100644 index 0000000..b1a0f44 Binary files /dev/null and b/img/level/3.png differ diff --git a/img/level/4.aseprite b/img/level/4.aseprite new file mode 100644 index 0000000..ceeec22 Binary files /dev/null and b/img/level/4.aseprite differ diff --git a/img/level/4.png b/img/level/4.png new file mode 100644 index 0000000..462a931 Binary files /dev/null and b/img/level/4.png differ diff --git a/img/level/5.aseprite b/img/level/5.aseprite new file mode 100644 index 0000000..7b0fd43 Binary files /dev/null and b/img/level/5.aseprite differ diff --git a/img/level/5.png b/img/level/5.png new file mode 100644 index 0000000..b213a26 Binary files /dev/null and b/img/level/5.png differ diff --git a/img/level/6.aseprite b/img/level/6.aseprite new file mode 100644 index 0000000..c6c13f3 Binary files /dev/null and b/img/level/6.aseprite differ diff --git a/img/level/6.png b/img/level/6.png new file mode 100644 index 0000000..20326ff Binary files /dev/null and b/img/level/6.png differ diff --git a/img/level/7.aseprite b/img/level/7.aseprite new file mode 100644 index 0000000..e292f14 Binary files /dev/null and b/img/level/7.aseprite differ diff --git a/img/level/7.png b/img/level/7.png new file mode 100644 index 0000000..aad01c0 Binary files /dev/null and b/img/level/7.png differ diff --git a/img/level/8.aseprite b/img/level/8.aseprite new file mode 100644 index 0000000..10e82d1 Binary files /dev/null and b/img/level/8.aseprite differ diff --git a/img/level/8.png b/img/level/8.png new file mode 100644 index 0000000..c1ee361 Binary files /dev/null and b/img/level/8.png differ diff --git a/img/level/9.aseprite b/img/level/9.aseprite new file mode 100644 index 0000000..bdcfeac Binary files /dev/null and b/img/level/9.aseprite differ diff --git a/img/level/9.png b/img/level/9.png new file mode 100644 index 0000000..840d45d Binary files /dev/null and b/img/level/9.png differ diff --git a/img/patchy_tile.aseprite b/img/patchy_tile.aseprite new file mode 100644 index 0000000..3589987 Binary files /dev/null and b/img/patchy_tile.aseprite differ diff --git a/img/patchy_tile.png b/img/patchy_tile.png new file mode 100644 index 0000000..750ad9a Binary files /dev/null and b/img/patchy_tile.png differ diff --git a/img/rabbits.aseprite b/img/rabbits.aseprite new file mode 100644 index 0000000..8402962 Binary files /dev/null and b/img/rabbits.aseprite differ diff --git a/img/rabbitsbrown.aseprite b/img/rabbitsbrown.aseprite new file mode 100644 index 0000000..5e84d63 Binary files /dev/null and b/img/rabbitsbrown.aseprite differ diff --git a/img/rabbitsbrown.png b/img/rabbitsbrown.png new file mode 100644 index 0000000..3dc6b98 Binary files /dev/null and b/img/rabbitsbrown.png differ diff --git a/img/rabbitswhite.aseprite b/img/rabbitswhite.aseprite new file mode 100644 index 0000000..158cc9c Binary files /dev/null and b/img/rabbitswhite.aseprite differ diff --git a/img/rabbitswhite.gif b/img/rabbitswhite.gif new file mode 100644 index 0000000..604072f Binary files /dev/null and b/img/rabbitswhite.gif differ diff --git a/img/rabbitswhite.png b/img/rabbitswhite.png new file mode 100644 index 0000000..07563c7 Binary files /dev/null and b/img/rabbitswhite.png differ diff --git a/img/selector.aseprite b/img/selector.aseprite new file mode 100644 index 0000000..0932670 Binary files /dev/null and b/img/selector.aseprite differ diff --git a/img/selector.gif b/img/selector.gif new file mode 100644 index 0000000..ebd2319 Binary files /dev/null and b/img/selector.gif differ diff --git a/img/selector.png b/img/selector.png new file mode 100644 index 0000000..3972db1 Binary files /dev/null and b/img/selector.png differ diff --git a/img/sign_tile.aseprite b/img/sign_tile.aseprite new file mode 100644 index 0000000..05273d8 Binary files /dev/null and b/img/sign_tile.aseprite differ diff --git a/img/sign_tile.png b/img/sign_tile.png new file mode 100644 index 0000000..d60a3f1 Binary files /dev/null and b/img/sign_tile.png differ diff --git a/img/snow_tile.aseprite b/img/snow_tile.aseprite new file mode 100644 index 0000000..45dd13e Binary files /dev/null and b/img/snow_tile.aseprite differ diff --git a/img/snow_tile.png b/img/snow_tile.png new file mode 100644 index 0000000..d6ad850 Binary files /dev/null and b/img/snow_tile.png differ diff --git a/img/title.aseprite b/img/title.aseprite new file mode 100644 index 0000000..d9e8262 Binary files /dev/null and b/img/title.aseprite differ diff --git a/img/title.png b/img/title.png new file mode 100644 index 0000000..f87377f Binary files /dev/null and b/img/title.png differ diff --git a/img/water_tile.aseprite b/img/water_tile.aseprite new file mode 100644 index 0000000..8671f35 Binary files /dev/null and b/img/water_tile.aseprite differ diff --git a/img/water_tile.png b/img/water_tile.png new file mode 100644 index 0000000..7cd6876 Binary files /dev/null and b/img/water_tile.png differ diff --git a/img/welldone.aseprite b/img/welldone.aseprite new file mode 100644 index 0000000..ba5d26f Binary files /dev/null and b/img/welldone.aseprite differ diff --git a/img/welldone.png b/img/welldone.png new file mode 100644 index 0000000..49062b4 Binary files /dev/null and b/img/welldone.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..1ea7b81 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + ld54 + + + + + + + + + + + diff --git a/levels.js b/levels.js new file mode 100644 index 0000000..62c4ef8 --- /dev/null +++ b/levels.js @@ -0,0 +1,14 @@ +var levels={ + 10: { map: [0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,2,0,0,0,0,2,2,0,1,0,0,0,0,0,0,3,0,1,0,0,0,0,0,2,1,3,2,2,0,0,0,0,0,3,2,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,],hares: [{ x:4, y:1, color: "brown" }, { x:6, y:1, color: "white" }, ],grass: [{ x:2, y:2 }, { x:6, y:4 }, { x:2, y:7 }, ], }, + 1: { map: [0,0,0,0,0,0,0,0,0,0,4,4,4,4,4,4,4,0,0,4,4,4,4,4,4,4,0,0,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,0,1,0,0,2,2,0,0,0,1,1,0,0,2,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,],hares: [{ x:2, y:5, color: "white" }, { x:6, y:7, color: "brown" }, ],grass: [{ x:5, y:5 }, { x:7, y:5 }, { x:1, y:7 }, { x:3, y:7 }, ], }, + 2: { map: [0,0,0,0,0,0,0,0,0,0,4,4,4,4,4,4,4,0,0,4,4,4,4,4,4,4,0,0,4,4,4,4,4,4,4,0,0,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,1,1,0,2,2,0,0,0,0,1,1,1,1,1,2,2,0,0,0,0,0,0,0,0,0,0,],hares: [{ x:4, y:6, color: "white" }, { x:1, y:7, color: "brown" }, ],grass: [{ x:1, y:6 }, { x:7, y:7 }, ], }, + 3: { map: [0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,1,2,2,2,1,0,0,0,0,1,2,2,2,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,],hares: [{ x:2, y:2, color: "brown" }, { x:3, y:3, color: "white" }, ],grass: [{ x:4, y:5 }, ], }, + 4: { map: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,1,1,1,0,0,0,0,2,2,2,2,1,0,0,0,0,1,2,2,2,2,0,0,0,0,2,1,1,1,2,0,0,0,0,2,1,1,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,],hares: [{ x:3, y:3, color: "white" }, { x:6, y:3, color: "brown" }, ],grass: [{ x:2, y:6 }, { x:3, y:6 }, { x:4, y:6 }, { x:5, y:6 }, { x:6, y:6 }, ], }, + 5: { map: [0,0,0,0,0,0,0,0,0,0,0,2,2,0,1,1,0,0,0,0,2,3,1,2,2,0,0,0,0,1,1,1,2,2,0,0,0,0,1,1,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,4,4,4,4,4,4,4,0,0,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,],hares: [{ x:2, y:1, color: "white" }, { x:4, y:3, color: "brown" }, ],grass: [{ x:6, y:1 }, { x:2, y:2 }, { x:2, y:3 }, { x:6, y:3 }, { x:2, y:4 }, { x:3, y:4 }, { x:5, y:4 }, { x:6, y:4 }, ], }, + 6: { map: [0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,0,0,0,0,0,2,2,2,3,3,0,0,0,0,2,1,0,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,2,1,0,0,0,1,1,2,2,3,1,1,0,0,0,0,2,2,0,2,0,0,0,0,0,0,0,0,0,0,0,],hares: [{ x:2, y:3, color: "white" }, { x:3, y:3, color: "brown" }, ],grass: [{ x:3, y:1 }, { x:1, y:6 }, { x:3, y:7 }, ], }, + 7: { map: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,2,2,1,1,0,0,0,0,0,0,2,1,0,0,0,0,0,0,0,2,2,1,1,0,0,0,0,1,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,],hares: [{ x:4, y:2, color: "white" }, { x:5, y:2, color: "brown" }, ],grass: [{ x:2, y:3 }, { x:6, y:5 }, { x:2, y:6 }, ], }, + 8: { map: [0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,3,0,0,0,0,0,2,2,2,3,0,0,0,0,0,1,2,0,1,0,0,0,2,3,1,3,0,1,0,0,0,0,0,2,0,0,1,0,0,0,0,0,1,2,1,3,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,],hares: [{ x:3, y:3, color: "brown" }, { x:3, y:5, color: "white" }, ],grass: [{ x:1, y:4 }, { x:4, y:6 }, { x:5, y:7 }, ], }, + 9: { map: [0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,1,0,0,0,0,0,2,2,3,1,1,0,0,0,2,1,1,0,1,1,0,0,0,2,1,2,0,1,0,0,0,0,0,1,0,0,1,2,0,0,1,1,2,3,1,1,2,0,0,0,0,0,0,2,2,2,0,0,0,0,0,0,0,0,0,0,],hares: [{ x:4, y:3, color: "brown" }, { x:4, y:4, color: "white" }, ],grass: [{ x:1, y:6 }, { x:5, y:6 }, { x:7, y:7 }, ], }, + end: { map: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,],hares: [],grass: [], }, + title: { map: [0,0,0,0,0,0,0,0,0,0,4,4,4,4,4,4,4,0,0,4,4,4,4,4,4,4,0,0,4,4,4,4,4,4,4,0,0,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,],hares: [],grass: [], }, +} diff --git a/levels/1.tmx b/levels/1.tmx new file mode 100644 index 0000000..53642db --- /dev/null +++ b/levels/1.tmx @@ -0,0 +1,22 @@ + + + + + + + + + + +1,1,1,1,1,1,1,1,1, +1,12,12,12,12,12,12,12,1, +1,12,12,12,12,12,12,12,1, +1,12,12,12,12,12,12,12,1, +1,1,1,1,1,1,1,1,1, +1,3,5,3,3,10,1,9,1, +1,3,3,1,1,1,2,2,1, +1,10,1,9,2,2,4,2,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/10.tmx b/levels/10.tmx new file mode 100644 index 0000000..43b647d --- /dev/null +++ b/levels/10.tmx @@ -0,0 +1,22 @@ + + + + + + + + + + +1,1,1,1,1,1,1,1,1, +1,1,1,1,4,6,5,1,1, +1,1,10,3,1,2,1,1,1, +1,1,1,6,1,2,1,1,1, +1,1,3,2,6,3,10,1,1, +1,1,1,6,3,1,1,1,1, +1,1,1,3,1,1,1,1,1, +1,1,9,2,1,1,1,1,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/2.tmx b/levels/2.tmx new file mode 100644 index 0000000..b082b33 --- /dev/null +++ b/levels/2.tmx @@ -0,0 +1,19 @@ + + + + + + + +1,1,1,1,1,1,1,1,1, +1,12,12,12,12,12,12,12,1, +1,12,12,12,12,12,12,12,1, +1,12,12,12,12,12,12,12,1, +1,12,12,12,12,12,12,12,1, +1,1,1,1,1,1,1,1,1, +1,9,2,1,5,3,1,1,1, +1,4,2,2,2,2,3,10,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/3.tmx b/levels/3.tmx new file mode 100644 index 0000000..82bf2bd --- /dev/null +++ b/levels/3.tmx @@ -0,0 +1,19 @@ + + + + + + + +1,1,1,1,1,1,1,1,1, +1,1,1,2,2,2,1,1,1, +1,1,4,3,3,3,2,1,1, +1,1,2,5,3,3,2,1,1, +1,1,1,1,2,2,1,1,1, +1,1,1,1,9,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,2,1,1,1,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/4.tmx b/levels/4.tmx new file mode 100644 index 0000000..03b05b2 --- /dev/null +++ b/levels/4.tmx @@ -0,0 +1,19 @@ + + + + + + + +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,3,3,2,2,2,1,1, +1,1,3,5,3,3,4,1,1, +1,1,2,3,3,3,3,1,1, +1,1,3,2,2,2,3,1,1, +1,1,10,9,9,9,10,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/5.tmx b/levels/5.tmx new file mode 100644 index 0000000..4c237b6 --- /dev/null +++ b/levels/5.tmx @@ -0,0 +1,19 @@ + + + + + + + +1,1,1,1,1,1,1,1,1, +1,1,5,3,1,2,9,1,1, +1,1,10,6,2,3,3,1,1, +1,1,9,2,4,3,10,1,1, +1,1,9,9,1,10,10,1,1, +1,1,1,1,1,1,1,1,1, +1,12,12,12,12,12,12,12,1, +1,12,12,12,12,12,12,12,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/6.tmx b/levels/6.tmx new file mode 100644 index 0000000..650c2a7 --- /dev/null +++ b/levels/6.tmx @@ -0,0 +1,19 @@ + + + + + + + +1,1,1,1,1,1,1,1,1, +1,1,1,10,3,3,1,1,1, +1,1,3,3,3,6,6,1,1, +1,1,5,4,1,2,2,1,1, +1,1,1,1,1,1,2,1,1, +1,1,1,1,1,3,2,1,1, +1,9,2,3,3,6,2,2,1, +1,1,1,10,3,1,3,1,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/7.tmx b/levels/7.tmx new file mode 100644 index 0000000..94d4071 --- /dev/null +++ b/levels/7.tmx @@ -0,0 +1,19 @@ + + + + + + + +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,5,4,1,1,1, +1,1,10,3,2,2,1,1,1, +1,1,1,3,2,1,1,1,1, +1,1,1,3,3,2,9,1,1, +1,1,9,2,3,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/8.tmx b/levels/8.tmx new file mode 100644 index 0000000..950a35b --- /dev/null +++ b/levels/8.tmx @@ -0,0 +1,19 @@ + + + + + + + +1,1,1,1,1,1,1,1,1, +1,1,1,1,6,1,6,1,1, +1,1,1,3,3,3,6,1,1, +1,1,1,4,3,1,2,1,1, +1,10,6,2,6,1,2,1,1, +1,1,1,5,1,1,2,1,1, +1,1,1,2,10,2,6,1,1, +1,1,1,1,1,9,1,1,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/9.tmx b/levels/9.tmx new file mode 100644 index 0000000..01aafd1 --- /dev/null +++ b/levels/9.tmx @@ -0,0 +1,19 @@ + + + + + + + +1,1,1,1,1,1,1,1,1, +1,1,1,1,6,1,2,1,1, +1,1,1,3,3,6,2,2,1, +1,1,3,2,4,1,2,2,1, +1,1,3,2,5,1,2,1,1, +1,1,2,2,1,1,2,3,1, +1,2,3,6,1,9,2,3,1, +1,9,1,1,1,3,3,10,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/end.tmx b/levels/end.tmx new file mode 100644 index 0000000..1eedab8 --- /dev/null +++ b/levels/end.tmx @@ -0,0 +1,19 @@ + + + + + + + +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/mapcompile.py b/levels/mapcompile.py new file mode 100755 index 0000000..8efd7e8 --- /dev/null +++ b/levels/mapcompile.py @@ -0,0 +1,54 @@ +#!/usr/bin/python3 + +import ulvl +import sys + +if len(sys.argv) < 2: + print("usage:", sys.argv[0], "") + sys.exit(1) + +screenwidth, screenheight = 9, 9 + +tilemapping = { 3: 1, 4: 2, 5: 3, 6: 3, 7: 3, 8: 1, 9: 2, 10: 3, 11: 4 } + +objmapping = { } + +print("var levels={") +for filename in sys.argv[1:]: + m = ulvl.TMX.load(filename) + + w = m.meta['width'] + h = m.meta['height'] + + print('\t', filename.replace('.tmx', '').replace('levels/', ''), end=': { ') + + hares = [ ] + grass = [ ] + + print('map: [', end='') + for y in range(h): + for x in range(w): + thing = m.layers[0].tiles[y * w + x] - 1 + if thing == 3 or thing == 6: + hares.append({ 'x': x, 'y': y, 'color': 'brown' }) + elif thing == 4 or thing == 7: + hares.append({ 'x': x, 'y': y, 'color': 'white' }) + elif thing == 8 or thing == 9 or thing == 10: + grass.append({ 'x': x, 'y': y }) + + print("" + str(tilemapping.get(thing, thing)) + ",", end='') + print('],', end=''); + + print('hares: [', end='') + for h in hares: + print('{ x:' + str(h['x']) + ', y:' + str(h['y']) + ', color: "' + h['color'] + '" }, ', end='') + print('],', end='') + + print('grass: [', end='') + for g in grass: + print('{ x:' + str(g['x']) + ', y:' + str(g['y']) + ' }, ', end='') + print('],', end='') + + print(' },') + +print("}") diff --git a/levels/title.tmx b/levels/title.tmx new file mode 100644 index 0000000..165c35c --- /dev/null +++ b/levels/title.tmx @@ -0,0 +1,19 @@ + + + + + + + +1,1,1,1,1,1,1,1,1, +1,12,12,12,12,12,12,12,1, +1,12,12,12,12,12,12,12,1, +1,12,12,12,12,12,12,12,1, +1,12,12,12,12,12,12,12,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1, +1,1,1,1,1,1,1,1,1 + + + diff --git a/levels/tmptile-sheet.png b/levels/tmptile-sheet.png new file mode 100644 index 0000000..f3ab3dc Binary files /dev/null and b/levels/tmptile-sheet.png differ diff --git a/levels/tmptile.png b/levels/tmptile.png new file mode 100644 index 0000000..00b1d95 Binary files /dev/null and b/levels/tmptile.png differ diff --git a/loading.png b/loading.png new file mode 100644 index 0000000..d278451 Binary files /dev/null and b/loading.png differ diff --git a/music/haregamesong.mp3 b/music/haregamesong.mp3 new file mode 100644 index 0000000..7f3443a Binary files /dev/null and b/music/haregamesong.mp3 differ diff --git a/pause.png b/pause.png new file mode 100644 index 0000000..2530de4 Binary files /dev/null and b/pause.png differ diff --git a/sfx/alert.mp3 b/sfx/alert.mp3 new file mode 100644 index 0000000..3e17161 Binary files /dev/null and b/sfx/alert.mp3 differ diff --git a/sfx/cronch.mp3 b/sfx/cronch.mp3 new file mode 100644 index 0000000..7922908 Binary files /dev/null and b/sfx/cronch.mp3 differ diff --git a/sfx/haregameyouwon.mp3 b/sfx/haregameyouwon.mp3 new file mode 100644 index 0000000..79be1b3 Binary files /dev/null and b/sfx/haregameyouwon.mp3 differ diff --git a/sfx/jump.mp3 b/sfx/jump.mp3 new file mode 100644 index 0000000..2253673 Binary files /dev/null and b/sfx/jump.mp3 differ diff --git a/sfx/nervous.mp3 b/sfx/nervous.mp3 new file mode 100644 index 0000000..2d02b84 Binary files /dev/null and b/sfx/nervous.mp3 differ diff --git a/sfx/push.mp3 b/sfx/push.mp3 new file mode 100644 index 0000000..ea96639 Binary files /dev/null and b/sfx/push.mp3 differ diff --git a/sfx/whew.mp3 b/sfx/whew.mp3 new file mode 100644 index 0000000..f3c5d22 Binary files /dev/null and b/sfx/whew.mp3 differ diff --git a/zucchinibread.js b/zucchinibread.js new file mode 100644 index 0000000..4a48d7e --- /dev/null +++ b/zucchinibread.js @@ -0,0 +1,1150 @@ +"use strict"; + +/* Please forgive my strictly procedural/functional style here. + * Thanks for reading though! */ + +let zb = (function() { + /* ---- Util ---- */ + + function mod(x, n) { + return ((x%n)+n)%n; + } + + function copy_list(list) { + let newlist = []; + for (let x of list) { + newlist.push(x); + } + return newlist; + } + + function copy_flat_objlist(list) { + let newlist = []; + for (let x of list) { + newlist.push({ ...x }); + } + return newlist; + } + + /* ---- Resource loading / loading screen ---- */ + + let _audiocheck = document.createElement('audio'); + + let _SFX_ARRAY_SIZE = 10; + + /* Returns a callback that should be called when a resource finishes loading. */ + function _register_resource(name, game, callback) { + game._total_things_to_load ++; + console.log("Loading", name + ". Things to load:", game._total_things_to_load); + return function() { + if (!game.ready_to_go) { + game._things_loaded ++; + console.log("Loaded", name + ". Things loaded:", game._things_loaded, "/", game._total_things_to_load); + _check_if_loaded(game); + if (callback) { + callback(); + } + } + } + } + + /* Check if audio failed to load bc file doesn't exist and print error message. */ + /* TODO add the same thing for an image */ + function _sound_resource_error(name, game, audio) { + return function(e) { + if (audio === undefined) { + console.error("Error loading resource, skipping:", name); + game._things_loaded ++; + _check_if_loaded(game); + } + } + } + + /* Check if the game has loaded everything and if so show the 'click to start' image */ + function _check_if_loaded(game) { + if (game.ready_to_go) return; + + if (game._things_loaded >= game._total_things_to_load) { + console.log("Ready"); + game.ready_to_go = true; + game._on_ready(); + } + } + + function _register_sfx(sfxdata, game) { + for (let key in sfxdata) { + let sfx_array = new Array(_SFX_ARRAY_SIZE); + for (let i = 0; i < _SFX_ARRAY_SIZE; i++) { + sfx_array[i] = new Audio(sfxdata[key].path); + sfx_array[i].addEventListener('canplaythrough', + _register_resource(sfxdata[key].path + '#' + (i+1), game), false); + sfx_array[i].addEventListener('error', + _sound_resource_error(sfxdata[key].path + '#' + (i+1), game), false, sfx_array[i]); + if (sfxdata[key].hasOwnProperty('volume')) { + sfx_array[i].volume = sfxdata[key].volume; + } + } + game.sfx[key] = { + _array: sfx_array, + _index: 0, + play: function() { + if (!game.muted) { + this._array[this._index].currentTime = 0; + this._array[this._index].play(); + this._index ++; + this._index = mod(this._index, _SFX_ARRAY_SIZE); + } + } + } + } + } + + function _register_music(musicdata, game) { + for (let key in musicdata) { + let musicpath; + if (musicdata[key].path.match(/\.[a-z]{3}$/)) { + musicpath = musicdata[key].path; + } else if (_audiocheck.canPlayType('audio/mpeg')) { + musicpath = musicdata[key].path + '.mp3'; + } else if (_audiocheck.canPlayType('audio/ogg')) { + musicpath = musicdata[key].path + '.ogg'; + } else { + console.log("No supported music type known :("); + return; + } + let music = new Audio(musicpath); + if (musicdata[key].hasOwnProperty('volume')) { + music.volume = musicdata[key].volume; + } + + if (!musicdata[key].hasOwnProperty('loop') || musicdata[key].loop) { + /* We loop by default unless 'loop: false' is specified. */ + music.loop = true; + } + music.addEventListener('canplaythrough', _register_resource(musicpath, game), false); + music.addEventListener('error', _sound_resource_error(musicpath, game, music), false); + + /* Handle music changes when muted, so when we unmute, + * the correct music will be playing. */ + music._og_play = music.play; + music.play = function() { + if (game.muted) { + music.was_playing = true; + } else { + music._og_play(); + } + } + + music._og_pause = music.pause; + music.pause = function() { + if (game.muted) { + music.was_playing = false; + } else { + music._og_pause(); + } + } + + game.music[key] = music; + } + } + + function _register_images(imgdata, game) { + function _recursive_load_images(pathmap) { + let result = {}; + for (let key in pathmap) { + if (typeof pathmap[key] === 'object') { + result[key] = _recursive_load_images(pathmap[key]); + } else { + result[key] = new Image(); + result[key].onload = _register_resource(pathmap[key], game); + result[key].src = pathmap[key]; + } + } + return result; + } + + let loaded_imgs = _recursive_load_images(imgdata); + for (let k in loaded_imgs) { + game.img[k] = loaded_imgs[k]; + } + } + + function create_game(params) { + let game_props = {...params}; + + game_props.frame_rate = params.frame_rate || 60; + + game_props.canvas_w = params.canvas_w || 640; + game_props.canvas_h = params.canvas_h || 480; + game_props.draw_scale = params.draw_scale || 4; + game_props.top_border = params.top_border || 0; + game_props.bottom_border = params.bottom_border || 8; + game_props.left_border = params.left_border || 0; + game_props.right_border = params.right_border || 0; + + game_props.tile_size = params.tile_size || 16; + game_props.level_w = params.level_w || 10; + game_props.level_h = params.level_h || 7; + + game_props.screen_w = game_props.canvas_w / game_props.draw_scale; + game_props.screen_h = game_props.canvas_h / game_props.draw_scale; + + game_props.background_color = params.background_color || '#000000'; + + let canvas = document.getElementById(game_props.canvas); + + let global_ctx = canvas.getContext('2d'); + global_ctx.imageSmoothingEnabled = false; + global_ctx.webkitImageSmoothingEnabled = false; + global_ctx.mozImageSmoothingEnabled = false; + + let mask_canvas = document.createElement('canvas'); + mask_canvas.width = canvas.width; + mask_canvas.height = canvas.height; + let mask_ctx = mask_canvas.getContext('2d'); + mask_ctx.imageSmoothingEnabled = false; + mask_ctx.webkitImageSmoothingEnabled = false; + mask_ctx.mozImageSmoothingEnabled = false; + + let copy_canvas = document.createElement('canvas'); + copy_canvas.width = canvas.width; + copy_canvas.height = canvas.height; + let copy_ctx = copy_canvas.getContext('2d'); + copy_ctx.imageSmoothingEnabled = false; + copy_ctx.webkitImageSmoothingEnabled = false; + copy_ctx.mozImageSmoothingEnabled = false; + + let draw_canvas = document.createElement('canvas'); + draw_canvas.width = canvas.width; + draw_canvas.height = canvas.height; + let draw_ctx = draw_canvas.getContext('2d'); + draw_ctx.imageSmoothingEnabled = false; + draw_ctx.webkitImageSmoothingEnabled = false; + draw_ctx.mozImageSmoothingEnabled = false; + + let game = { + /* General properties */ + ...game_props, + + canvas: canvas, + + /* Loading */ + ready_to_go: false, + _total_things_to_load: 1, + _things_loaded: 0, + resources_ready: function() { + this._things_loaded ++; + console.log("Finished enumerating resources to load. Things loaded:", + this._things_loaded, "/", this._total_things_to_load); + _check_if_loaded(this); + }, + _on_ready: function() { + this.ready_to_go = true; + if (game.img._clicktostart && game.img._clicktostart.complete) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(game.img._clicktostart, 0, 0); + game.ctx.global.restore(); + } + }, + playing: false, + play: function() { + this.playing = true; + if (this.events.gamestart) { + this.events.gamestart(this); + } + _loop(this); + }, + _norun: false, + + /* Audio */ + sfx: {}, + music: {}, + muted: false, + mute: function() { + _mute(this); + }, + unmute: function() { + _unmute(this); + }, + toggle_mute: function() { + if (this.muted) { + _unmute(this); + } else { + _mute(this); + } + }, + register_sfx: function(sfxdata) { + _register_sfx(sfxdata, this); + }, + register_music: function(musicdata) { + _register_music(musicdata, this); + }, + + /* Drawing */ + ctx: { + global: global_ctx, /* context for the actual real canvas */ + mask: mask_ctx, /* context for drawing the transition mask, gets scaled up */ + copy: copy_ctx, /* context for copying the old screen on transition */ + draw: draw_ctx, /* context for drawing the real level */ + }, + img: {}, + register_images: function(imgdata) { + _register_images(imgdata, this); + }, + + /* Save */ + save: function(key, data) { + _save(game, key, data); + }, + load: function(key) { + return _load(game, key); + }, + + /* Transition system */ + transition: _transition, + start_transition: function(type, length, callback, on_done) { + _start_transition(this, type, length, callback, on_done); + }, + long_transition: function(type, length, callback, on_done) { + _long_transition(this, type, length, callback, on_done); + } + }; + + window.requestAnimFrame = (function() { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback, element) { + window.setTimeout(callback, 1000/game.frame_rate); + }; + })(); + + /* Register event listeners */ + for (let ev in params.events) { + /* Register any other events I guess */ + canvas['on' + ev] = function(e) { + if (game.playing && !game._norun) { + params.events[ev](game, e); + } + } + } + + /* Override with special events */ + canvas.onmousedown = function(e) { + if (!game._norun) { + _handle_mousedown(game, e); + } + } + + canvas.onmousemove = function(e) { + if (!game._norun) { + _handle_mousemove(game, e); + } + } + + canvas.onmouseup = function(e) { + if (!game._norun) { + _handle_mouseup(game, e); + } + } + + canvas.onkeydown = function(e) { + if (!game._norun && game.events.keydown) { + game.events.keydown(game, e); + e.preventDefault(); + } + } + + canvas.onkeyup = function(e) { + if (!game._norun && game.events.keyup) { + game.events.keyup(game, e); + e.preventDefault(); + } + } + + canvas.onblur = function(e) { + if (game.playing && !game.run_in_background) { + game._norun = true; + _stop_music(game); + } + } + + canvas.onfocus = function(e) { + if (game.playing && !game.run_in_background) { + game._norun = false; + if (!game.muted) { + _start_music(game); + } + _loop(game); + } + } + + /* Set loading stuff */ + let loading_img = new Image(); + loading_img.onload = _register_resource('loading.png', game, function() { + if (!game.ready_to_go) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(loading_img, 0, 0); + game.ctx.global.restore(); + } + }); + loading_img.src = 'loading.png'; + + if (!game.run_in_background) { + let pause_img = new Image(); + pause_img.onload = _register_resource('pause.png', game); + pause_img.src = 'pause.png'; + game.img._pause = pause_img; + } + + if (game.hasOwnProperty('save_key')) { + let saveerror_img = new Image(); + saveerror_img.onload = _register_resource('saveerror.png', game); + saveerror_img.src = 'saveerror.png'; + game.img._saveerror = saveerror_img; + } + + let clicktostart_img = new Image(); + clicktostart_img.onload = _register_resource('clicktostart.png', game, function() { + if (game.ready_to_go) { + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(game.img._clicktostart, 0, 0); + game.ctx.global.restore(); + } + }); + clicktostart_img.src = 'clicktostart.png'; + game.img._clicktostart = clicktostart_img; + + return game; + } + + /* ---- Audio ---- */ + + function _stop_music(game) { + if (game.muted) return; + + for (let m in game.music) { + if (!game.music[m].paused) { + game.music[m].was_playing = true; + game.music[m].pause(); + } else { + game.music[m].was_playing = false; + } + } + } + + function _start_music(game, unmuting) { + if (game.muted && !unmuting) return; + + for (let m in game.music) { + if (game.music[m].was_playing) { + game.music[m].play(); + } + } + } + + function _mute(game) { + _stop_music(game); + game.muted = true; + } + + function _unmute(game) { + game.muted = false; + _start_music(game); + } + + /* ---- Game update stuff ---- */ + + let _last_frame_time; + let _timedelta = 0; + function _loop(game, timestamp) { + if (timestamp == undefined) { + timestamp = 0; + _last_frame_time = timestamp; + } + _timedelta += timestamp - _last_frame_time; + _last_frame_time = timestamp; + + while (_timedelta >= 1000 / game.frame_rate) { + _update(game, 1000 / game.frame_rate); + _timedelta -= 1000 / game.frame_rate; + } + _draw(game); + + if (!game._norun) { + requestAnimFrame(function(timestamp) { + _loop(game, timestamp); + }); + } + } + + function _update(game, delta) { + game.update_func(delta); + + if (game.transition.is_transitioning) { + game.transition.timer += delta; + if (game.transition.timer > game.transition.end_time) { + _finish_transition(game); + } + } + } + + /* ---- Mouse ---- */ + + function _handle_mousedown(game, e) { + if (!game.playing) return; + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / game.draw_scale) - 1; + let y = Math.round((e.clientY - rect.top) / game.draw_scale) - 1; + if (e.button === 0 && game.events.mousedown) { + game.events.mousedown(game, e, x, y); + } + } + + function _handle_mouseup(game, e) { + if (!game.playing && game.ready_to_go) { + /* Click to start */ + if (game.hasOwnProperty('save_key') && !game._show_save_error) { + /* Test if save/load is working */ + try { + console.log("Test saving"); + localStorage.setItem(game.save_key + ".test", "savetest"); + let savetest = localStorage.getItem(game.save_key + ".test"); + if (savetest !== "savetest") { + throw new Error("oops"); + } + } catch (e) { + /* If error saving, show save error message first */ + /* We set _show_save_error so that the second time the user clicks, + * the game will actually start */ + console.log("- SAVE FAILED -"); + game._show_save_error = true; + game.ctx.global.save(); + game.ctx.global.scale(game.draw_scale, game.draw_scale); + game.ctx.global.drawImage(game.img._saveerror, 0, 0); + game.ctx.global.restore(); + return; + } + } + game.play(); + return; + } + + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / game.draw_scale) - 1; + let y = Math.round((e.clientY - rect.top) / game.draw_scale) - 1; + if (e.button === 0 && game.events.mouseup) { + game.events.mouseup(game, e, x, y); + } + } + + function _handle_mousemove(game, e) { + const rect = game.canvas.getBoundingClientRect(); + let x = Math.round((e.clientX - rect.left) / game.draw_scale) - 1; + let y = Math.round((e.clientY - rect.top) / game.draw_scale) - 1; + if (game.events.mousemove) { + game.events.mousemove(game, e, x, y); + } + } + + /* ---- Drawing stuff ---- */ + + function _draw(game) { + let ctx = game.ctx.draw; + + ctx.save(); + + ctx.fillStyle = game.background_color; + + ctx.beginPath(); + ctx.rect(0, 0, game.screen_w, game.screen_h); + ctx.fill(); + + game.draw_func(game.ctx.draw); + + if (game.transition.mid_long) { + ctx.fillStyle = game.transition.color; + ctx.fillRect(-1, -1, game.canvas_w + 5, game.canvas_h + 5); + } + + ctx.restore(); + + game.ctx.global.fillStyle = 'rgb(0, 0, 0)'; + game.ctx.global.beginPath(); + game.ctx.global.rect(0, 0, game.screen_w * game.draw_scale, game.screen_h * game.draw_scale); + game.ctx.global.fill(); + + game.ctx.global.save(); + + game.ctx.global.scale(game.draw_scale, game.draw_scale); + + game.ctx.global.drawImage(ctx.canvas, 0, 0); + + if (game.transition.is_transitioning) { + _draw_transition(game); + } + + if (game._norun) { + game.ctx.global.drawImage(game.img._pause, 0, 0); + } + + game.ctx.global.restore(); + } + + /* Draw a particular section of an image/spritesheet, + * without having to do as much math or type the destination size */ + function sprite_draw(ctx, img, section_w, section_h, section_x, section_y, dest_x, dest_y) { + ctx.drawImage(img, + section_w * section_x, section_h * section_y, section_w, section_h, + dest_x, dest_y, section_w, section_h) + } + + /* Draw an image over the whole screen lol */ + function screen_draw(ctx, img) { + ctx.drawImage(img, 0, 0); + } + + /* ---- Save ---- */ + + function _save(game, key, data) { + let save_data; + + if (!game.hasOwnProperty('save_key')) { + throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object"); + } + + try { + save_data = localStorage.getItem(game.save_key) || "{}"; + } catch (e) { + console.error("Saving is disabled; cannot save " + key, e); + return; + } + + try { + save_data = JSON.parse(save_data); + } catch (e) { + console.error("Cannot parse stored save data (when trying to save " + key + ")", e); + return; + } + + save_data[key] = JSON.stringify(data); + + try { + save_data = JSON.stringify(save_data); + } catch (e) { + console.error("Failed to stringify save data (while trying to save " + key + ")", e); + return; + } + + try { + localStorage.setItem(game.save_key, save_data); + } catch (e) { + console.error("Failed to save to localStorage (while saving key " + key + ")", e); + return; + } + } + + function _load(game, key) { + let save_data; + + if (!game.hasOwnProperty('save_key')) { + throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object"); + } + + try { + save_data = localStorage.getItem(game.save_key); + } catch (e) { + console.error("Loading is disabled; cannot load " + key, e); + return; + } + + try { + save_data = JSON.parse(save_data); + } catch (e) { + console.error("Cannot parse stored save data (when trying to load " + key + ")", e); + return; + } + + if (!save_data) { + return null; + } + + return save_data[key]; + } + + /* ---- Transition stuff ---- */ + + let TransitionType = { DOTS: 1, SLIDE_DOWN: 2, SLIDE_UP: 3, FADE: 4, CIRCLE: 5 }; + + let _transition = { + is_transitioning: false, + timer: 0, + color: '#000000', + w: 20, + h: 14, + dir_invert_v: false, + dir_invert_h: false, + invert_shape: true, + mid_long: false, + done_func: null, + type: TransitionType.DOTS, + nodraw: false, + end_time: 100, + } + + function _long_transition(game, type, length, callback) { + if (game.transition.is_transitioning) return; + + _draw(game); + + game.transition.invert_shape = false; + _internal_start_transition(game, type, length, function() { + game.transition.mid_long = true; + }, function() { + game.transition.invert_shape = true; + game.transition.is_transitioning = true; + let tdiv = game.transition.dir_invert_v; + let tdih = game.transition.dir_invert_h; + _internal_start_transition(game, type, length, function() { + game.transition.mid_long = false; + callback(); + game.transition.dir_invert_v = tdiv; + game.transition.dir_invert_h = tdih; + }); + }); + } + + function _start_transition(game, type, length, callback, on_done) { + if (game.transition.is_transitioning) return; + if (!game.transition.nodraw) _draw(game); + + _internal_start_transition(game, type, length, callback, on_done); + } + + function _internal_start_transition(game, type, length, callback, on_done) { + if (on_done) { + game.transition.done_func = on_done; + } + + game.transition.type = type; + game.transition.end_time = length; + + game.ctx.copy.drawImage(game.ctx.draw.canvas, 0, 0); + + game.transition.dir_invert_v = Math.random() < 0.5; + game.transition.dir_invert_h = Math.random() < 0.5; + + callback(); + + game.transition.is_transitioning = true; + game.transition.timer = 0; + } + + function _finish_transition(game) { + game.transition.is_transitioning = false; + game.transition.timer = 0; + + if (game.transition.done_func) { + setTimeout(function() { + game.transition.done_func(); + game.transition.done_func = null; + }, 400); + } + } + + function _draw_transition(game) { + game.ctx.global.save(); + + if (game.transition.type == TransitionType.DOTS) { + game.ctx.mask.clearRect(0, 0, game.screen_w, game.screen_h); + _draw_transition_dot_mask(game, game.ctx.mask); + + // Redraw to reduce antialiasing effects + for (let i = 0; i < 5; i++) { + game.ctx.mask.drawImage(game.ctx.mask.canvas, 0, 0); + } + + game.ctx.mask.globalCompositeOperation = 'source-in'; + game.ctx.mask.drawImage(game.ctx.copy.canvas, 0, 0); + game.ctx.mask.globalCompositeOperation = 'source-over'; + + game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0); + } else if (game.transition.type == TransitionType.SLIDE_DOWN) { + let offset = game.transition.timer / game.transition.end_time * game.screen_h; + + game.ctx.global.drawImage(game.ctx.copy.canvas, 0, -offset); + game.ctx.global.drawImage(ctx.canvas, 0, game.screen_h - offset); + } else if (game.transition.type == TransitionType.SLIDE_UP) { + let offset = game.transition.timer / game.transition.end_time * game.screen_h; + + game.ctx.global.drawImage(game.ctx.copy.canvas, 0, offset); + game.ctx.global.drawImage(ctx.canvas, 0, - game.screen_h + offset); + } else if (game.transition.type == TransitionType.FADE) { + let alpha = 0; + alpha = 1 - game.transition.timer / game.transition.end_time; + + game.ctx.mask.clearRect(0, 0, game.screen_w, game.screen_h); + game.ctx.mask.fillStyle = 'rgba(255,255,255,' + alpha + ')'; + game.ctx.mask.fillRect(0, 0, game.screen_w, game.screen_h); + game.ctx.mask.globalCompositeOperation = 'source-in'; + game.ctx.mask.drawImage(game.ctx.copy.canvas, 0, 0); + game.ctx.mask.globalCompositeOperation = 'source-over'; + + game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0); + } else if (game.transition.type == TransitionType.CIRCLE) { + game.ctx.mask.clearRect(0, 0, game.screen_w, game.screen_h); + + let frac = game.transition.timer / game.transition.end_time; + if (!game.transition.invert_shape) { + frac = 1 - frac; + } + frac = Math.pow(frac, 1.5); + + let cx = character.x + 0.5; + let cy = character.y + 0.5; + let lh = game.level_h + 1; + let distances_to_corners = [ + Math.sqrt(Math.pow(cx * game.tile_size, 2) + Math.pow(cy * game.tile_size, 2)), + Math.sqrt(Math.pow((game.level_w - cx) * game.tile_size, 2) + Math.pow(cy * game.tile_size, 2)), + Math.sqrt(Math.pow(cx * game.tile_size, 2) + Math.pow((lh - cy) * game.tile_size, 2)), + Math.sqrt(Math.pow((game.level_w - cx) * game.tile_size, 2) + Math.pow((lh - cy) * game.tile_size, 2)), + ]; + let max_radius = Math.max(...distances_to_corners); + let radius = frac * max_radius; + + game.ctx.mask.globalCompositeOperation = 'source-over'; + game.ctx.mask.drawImage(game.ctx.copy.canvas, 0, 0); + + game.ctx.mask.globalCompositeOperation = 'destination-out'; + game.ctx.mask.fillStyle = 'rgba(255,255,255)'; + game.ctx.mask.beginPath(); + if (!game.transition.invert_shape) { + game.ctx.mask.rect(-5, -5, game.screen_w + 5, game.screen_h + 5); + } + game.ctx.mask.arc(character.x * game.tile_size + game.tile_size / 2, + character.y * game.tile_size + game.tile_size / 2, + radius, 0, 2 * Math.PI, + !game.transition.invert_shape); + game.ctx.mask.fill(); + + game.ctx.mask.globalCompositeOperation = 'source-over'; + + game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0); + } + + game.ctx.global.restore(); + } + + function _draw_transition_dot_mask(game, ctx) { + ctx.fillStyle = '#0000ff'; + let cell_width = game.screen_w / game.transition.w; + let cell_height = game.screen_h / game.transition.h; + let max_radius = 0.75 * Math.max(cell_width, cell_height); + + let transition_dot_length = game.transition.end_time * 3 / 8; + + for (let x = -1; x < game.transition.w + 1; x++) { + for (let y = -1; y < game.transition.h + 1; y++) { + let radius; + + let circle_start_time = (x + y) / (game.transition.w + game.transition.h) + * (game.transition.end_time - transition_dot_length); + if (game.transition.timer - circle_start_time < 0) { + if (game.transition.invert_shape) { + radius = 0; + } else { + radius = max_radius; + } + } else if (game.transition.timer - circle_start_time < transition_dot_length) { + if (game.transition.invert_shape) { + radius = (game.transition.timer - circle_start_time) / transition_dot_length * max_radius; + } else { + radius = (1 - (game.transition.timer - circle_start_time) / transition_dot_length) * max_radius; + } + } else { + if (game.transition.invert_shape) { + radius = max_radius; + } else { + radius = 0; + } + } + + let draw_x = x; + let draw_y = y; + if (game.transition.dir_invert_v) draw_x = game.transition.w - 1 - x; + if (game.transition.dir_invert_h) draw_y = game.transition.h - 1 - y; + + if (radius >= max_radius * 0.8) { + if (!game.transition.invert_shape) { + ctx.fillRect(draw_x * cell_width, draw_y * cell_width, cell_width + 1, cell_width + 1); + } + } else if (radius > 0) { + ctx.save(); + ctx.beginPath(); + if (game.transition.invert_shape) { + ctx.rect(draw_x * cell_width, draw_y * cell_width, cell_width + 3, cell_width + 3); + } + ctx.moveTo(draw_x * cell_width + cell_width / 2, draw_y * cell_width + cell_width / 2); + ctx.arc(draw_x * cell_width + cell_width / 2, + draw_y * cell_width + cell_width / 2, + radius, 0, 2 * Math.PI, game.transition.invert_shape); + ctx.clip(); + ctx.fillRect(draw_x * cell_width, draw_y * cell_width, cell_width, cell_width); + ctx.restore(); + } else { + if (game.transition.invert_shape) { + ctx.fillRect(draw_x * cell_width, draw_y * cell_width, cell_width + 3, cell_width + 3); + } + } + } + } + } + + /* ---- former contents of ready.js ---- */ + + // Due to Timo Huovinen + // https://stackoverflow.com/questions/799981/document-ready-equivalent-without-jquery + var ready = (function(){ + + var readyList, + DOMContentLoaded, + class2type = {}; + class2type["[object Boolean]"] = "boolean"; + class2type["[object Number]"] = "number"; + class2type["[object String]"] = "string"; + class2type["[object Function]"] = "function"; + class2type["[object Array]"] = "array"; + class2type["[object Date]"] = "date"; + class2type["[object RegExp]"] = "regexp"; + class2type["[object Object]"] = "object"; + + var ReadyObj = { + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + ReadyObj.readyWait++; + } else { + ReadyObj.ready( true ); + } + }, + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--ReadyObj.readyWait) || (wait !== true && !ReadyObj.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( ReadyObj.ready, 1 ); + } + + // Remember that the DOM is ready + ReadyObj.isReady = true; + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --ReadyObj.readyWait > 0 ) { + return; + } + // If there are functions bound, to execute + readyList.resolveWith( document, [ ReadyObj ] ); + + // Trigger any bound ready events + //if ( ReadyObj.fn.trigger ) { + // ReadyObj( document ).trigger( "ready" ).unbind( "ready" ); + //} + } + }, + bindReady: function() { + if ( readyList ) { + return; + } + readyList = ReadyObj._Deferred(); + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( ReadyObj.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + // A fallback to window.onload, that will always work + window.addEventListener( "load", ReadyObj.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", ReadyObj.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + _Deferred: function() { + var // callbacks list + callbacks = [], + // stored [ context , args ] + fired, + // to avoid firing when already doing so + firing, + // flag to know if the deferred has been cancelled + cancelled, + // the deferred itself + deferred = { + + // done( f1, f2, ...) + done: function() { + if ( !cancelled ) { + var args = arguments, + i, + length, + elem, + type, + _fired; + if ( fired ) { + _fired = fired; + fired = 0; + } + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = ReadyObj.type( elem ); + if ( type === "array" ) { + deferred.done.apply( deferred, elem ); + } else if ( type === "function" ) { + callbacks.push( elem ); + } + } + if ( _fired ) { + deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] ); + } + } + return this; + }, + + // resolve with given context and args + resolveWith: function( context, args ) { + if ( !cancelled && !fired && !firing ) { + // make sure args are available (#8421) + args = args || []; + firing = 1; + try { + while( callbacks[ 0 ] ) { + callbacks.shift().apply( context, args );//shifts a callback, and applies it to document + } + } + finally { + fired = [ context, args ]; + firing = 0; + } + } + return this; + }, + + // resolve with this as context and given arguments + resolve: function() { + deferred.resolveWith( this, arguments ); + return this; + }, + + // Has this deferred been resolved? + isResolved: function() { + return !!( firing || fired ); + }, + + // Cancel + cancel: function() { + cancelled = 1; + callbacks = []; + return this; + } + }; + + return deferred; + }, + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ Object.prototype.toString.call(obj) ] || "object"; + } + } + // The DOM ready check for Internet Explorer + function doScrollCheck() { + if ( ReadyObj.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + ReadyObj.ready(); + } + // Cleanup functions for the document ready method + if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + ReadyObj.ready(); + }; + + } else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + ReadyObj.ready(); + } + }; + } + function ready( fn ) { + // Attach the listeners + ReadyObj.bindReady(); + + var type = ReadyObj.type( fn ); + + // Add the callback + readyList.done( fn );//readyList is result of _Deferred() + } + return ready; + })(); + + return { + create_game: create_game, + ready: ready, + mod: mod, + copy_list: copy_list, + copy_flat_objlist: copy_flat_objlist, + sprite_draw: sprite_draw, + screen_draw: screen_draw, + transition: TransitionType, + } +})();