--- /dev/null
+"use strict";
+
+let game;
+
+/* --------- definitions ---------- */
+
+/* state:
+ * TITLE: title screen
+ * STAND: waiting for input
+ * DRAG: connecting tiles together
+ * DESTROY: tiles exploding
+ * REPLACE: new tiles falling into place
+ * LIGHTNING: selecting something to lightning
+ * GAMEOVER: game over
+ */
+let State = { TITLE: -1, STAND: 0, DRAG: 1, DESTROY: 2, REPLACE: 3, LIGHTNING: 4, GAMEOVER: 5 };
+
+let can_continue = false;
+
+let save_data = 1;
+const SAVE_KEY = "casso.signalconnectinggame.save"
+
+zb.ready(function() {
+ game = zb.create_game({
+ canvas: 'canvas',
+ canvas_w: 486,
+ canvas_h: 756,
+ draw_scale: 3,
+ tile_size: 16,
+ level_w: 8,
+ level_h: 8,
+ background_color: '#f4ecce',
+ run_in_background: true,
+ load_with_progress_bar: true,
+ save_key: SAVE_KEY,
+ state: State.STAND,
+ events: {
+ gamestart: handle_gamestart,
+ mousemove: handle_mousemove,
+ mouseup: handle_mouseup,
+ mousedown: handle_mousedown,
+ },
+ modes: {
+ menu: {
+ draw: do_draw_menu,
+ update: do_update_menu,
+ },
+ youwon: {
+ draw: do_draw_youwon,
+ },
+ game: {
+ draw: do_draw,
+ update: do_update,
+ },
+ },
+ mode: 'game',
+ buttons: {},
+ });
+
+ game.register_images({
+ connector: 'img/connector.png',
+ gameover: 'img/gameover.png',
+ lightning: 'img/lightning.png',
+ lightningbutton: 'img/lightningbutton.png',
+ lightningnumbers: 'img/lightningnumbers.png',
+ playbutton: 'img/playbutton.png',
+ replaybutton: 'img/replaybutton.png',
+ scorenumbers: 'img/scorenumbers.png',
+ selector: 'img/selector.png',
+ text: 'img/text.png',
+ tiles: 'img/tiles.png',
+ timebar_inside: 'img/timebar_inside.png',
+ timebar_outside: 'img/timebar_outside.png',
+ title: 'img/title.png',
+ });
+
+ game.register_sfx({
+ ok: {
+ path: 'sfx/ok.wav',
+ volume: 0.7,
+ },
+ thud: {
+ path: 'sfx/thud.wav',
+ volume: 1.0,
+ copies: 20,
+ },
+ bap: {
+ path: 'sfx/bap.wav',
+ volume: 0.2,
+ },
+ lightning: {
+ path: 'sfx/lightning.wav',
+ volume: 0.4,
+ },
+ gameover: {
+ path: 'sfx/gameover.mp3',
+ volume: 0.3,
+ },
+ });
+
+ game.register_music({
+ bgm: {
+ path: 'music/suitgamesong',
+ volume: 0.5,
+ },
+ });
+
+ game.resources_ready();
+
+ board_offset_x = game.screen_w / 2 - TILE_W * BOARD_W / 2;
+ board_offset_y = game.screen_h / 2 - TILE_H * BOARD_H / 2;
+
+ high_score = game.load('high_score') || 0;
+
+ game.buttons.menu = zb.buttons.create({
+ img: game.img.menubuttons,
+ button_w: 64,
+ button_h: 16,
+ buttons: [ ],
+ });
+
+ game.buttons.lightning = zb.buttons.create({
+ img: game.img.lightningbutton,
+ button_w: 36,
+ button_h: 18,
+ buttons: [
+ {
+ /* Lightning button */
+ x: game.screen_w / 2 - 16,
+ y: board_offset_y + BOARD_H * TILE_H + 12,
+ id: 0,
+ disabled: true,
+ callback: function() {
+ if (game.state === State.STAND) {
+ game.state = State.LIGHTNING;
+ game.buttons.lightning.buttons[0].disabled = true;
+ }
+ },
+ },
+ ],
+ });
+
+ game.buttons.play = zb.buttons.create({
+ img: game.img.playbutton,
+ button_w: 36,
+ button_h: 18,
+ buttons: [
+ {
+ /* Play button */
+ x: game.screen_w / 2 - 16,
+ y: board_offset_y + BOARD_H * TILE_H + 12,
+ id: 0,
+ callback: function() {
+ if (game.state === State.TITLE) {
+ initialize();
+ score = 0;
+ displayed_score = 0;
+ timer_running = false;
+ lightnings = 0;
+ }
+ },
+ },
+ ],
+ });
+
+
+ game.buttons.replay = zb.buttons.create({
+ img: game.img.replaybutton,
+ button_w: 36,
+ button_h: 18,
+ buttons: [
+ {
+ /* Replay button */
+ x: game.screen_w / 2 - 16,
+ y: board_offset_y + BOARD_H * TILE_H + 12,
+ id: 0,
+ callback: function() {
+ if (game.state === State.GAMEOVER) {
+ initialize();
+ score = 0;
+ displayed_score = 0;
+ timer_running = false;
+ lightnings = 0;
+ n_colors = 2;
+ game.music.bgm.currentTime = 0;
+ }
+ },
+ },
+ ],
+ });
+
+ game.transition.color = game.background_color;
+
+ initialize();
+});
+
+/* ------ relevant constant types --------- */
+
+const suitID = {
+ spades: 0,
+ clubs: 1,
+ hearts: 2,
+ diamonds: 3,
+};
+
+const colorID = {
+ green: 0,
+ blue: 1,
+ orange: 2,
+ purple: 3,
+};
+
+const TILE_W = 16;
+const TILE_H = 16;
+
+const BOARD_W = 8;
+const BOARD_H = 8;
+
+const NSUITS = 6;
+let n_colors = 2;
+
+const MAX_N_COLORS = 6;
+
+const WILD_SUIT = 4;
+const EVIL_SUIT = 5;
+
+const MAX_LIGHTNINGS = 99;
+
+const GRAVITY = 400;
+
+const MAX_TIMER = 40000;
+const TIME_GAIN_PER_BLOCK = 150;
+const TIME_INCREASE_PER_BLOCK = 40;
+
+/* ------ timers & static timer values --------- */
+
+const TILE_DELAY_PER_ROW = 70;
+const TILE_DELAY_PER_COL = 60;
+
+const THUD_SPACING = 60;
+let thud_timeout = 0;
+
+/* ------- game global state -------- */
+
+let level = Array(BOARD_W * BOARD_H).fill(null);
+
+let selector = {
+ x: -1,
+ y: -1,
+};
+
+let board_offset_x;
+let board_offset_y;
+
+let path = [];
+
+let lightnings = 0;
+
+let lightningmode = false;
+
+let lightning_particles = [];
+
+let game_just_started = true;
+let show_title = true;
+
+let score = 0;
+let displayed_score = 0;
+
+let high_score = 0;
+
+let mouse_x = -1;
+let mouse_y = -1;
+
+let timer;
+let timer_running = false;
+
+/* ------- game behavior functions -------- */
+
+function tile_at(x, y) {
+ if (x < 0 || x >= game.level_w) return null;
+ if (y < 0 || y >= game.level_h) return null;
+
+ let result = level[y * game.level_w + x];
+ return result;
+}
+
+function set_tile_at(x, y, value) {
+ if (x < 0 || x >= game.level_w) return null;
+ if (y < 0 || y >= game.level_h) return null;
+
+ level[y * game.level_w + x] = value;
+ if (level[y * game.level_w + x]) {
+ level[y * game.level_w + x].tile_x = x;
+ level[y * game.level_w + x].tile_y = y;
+ }
+}
+
+function place_random_tile(x, y) {
+ let color = Math.floor(Math.random() * n_colors);
+ let suit = Math.floor(Math.random() * NSUITS);
+
+ if (suit === EVIL_SUIT && (Math.random() < 0.7 || game_just_started)
+ || suit === WILD_SUIT && Math.random() < 0.4) {
+ do {
+ suit = Math.floor(Math.random() * NSUITS);
+ } while (suit === EVIL_SUIT || suit === WILD_SUIT);
+ }
+
+ set_tile_at(x, y, {
+ color: color,
+ suit: suit,
+ delay: TILE_DELAY_PER_ROW * x + TILE_DELAY_PER_COL * (BOARD_H - y - 1),
+ tile_x: x,
+ tile_y: y,
+ x: x * TILE_W,
+ y: -game.screen_h * 0.75,
+ vy: 0,
+ });
+}
+
+/* -------- game phase functions ------------ */
+
+function initialize() {
+ game_just_started = true;
+ n_colors = 2;
+ timer = MAX_TIMER;
+ game.state = State.DESTROY;
+ for (let y = 0; y < BOARD_H; y++) {
+ for (let x = 0; x < BOARD_H; x++) {
+ place_random_tile(x, y);
+ }
+ }
+}
+
+function check_high_score() {
+ if (displayed_score > high_score) {
+ high_score = displayed_score;
+ game.save('high_score', high_score);
+ }
+}
+
+function reset_save() {
+ game.save('high_score', 0);
+}
+
+function win() {
+}
+
+function game_over() {
+ console.log("Game over... :(");
+ game.music.bgm.pause();
+ game.sfx.gameover.play();
+ game.state = State.GAMEOVER;
+}
+
+/* ---------- update functions ------------ */
+
+function get_score_threshold(n_colors) {
+ return Math.floor(Math.pow(n_colors - 1, 1.7) * 5) * 1000 - 2000;
+}
+
+for (let n_colors = 2; n_colors <= MAX_N_COLORS; n_colors ++) {
+ console.log("Score threshold: ", n_colors, get_score_threshold(n_colors));
+}
+
+/* MAIN UPDATE FUNCTION */
+function do_update(delta) {
+ if (game.state === State.LIGHTNING) {
+ game.canvas.style.cursor = 'none';
+ } else {
+ game.canvas.style.cursor = 'default';
+ }
+
+ if (displayed_score < score * 100) {
+ displayed_score += Math.max(700, Math.floor(Math.abs(displayed_score - score * 100) / 30));
+ if (displayed_score > score * 100) {
+ displayed_score = score * 100;
+ }
+
+ check_high_score();
+
+ high_score = Math.max(high_score, displayed_score);
+ }
+
+ if (score > get_score_threshold(n_colors)) {
+ n_colors ++;
+ console.log("advance to ", n_colors);
+ if (n_colors > MAX_N_COLORS) {
+ n_colors = MAX_N_COLORS;
+ }
+ }
+
+ for (let p of lightning_particles) {
+ p.x = p.x * 0.9 + p.target_x * 0.1;
+ p.y = p.y * 0.9 + p.target_y * 0.1;
+ if (Math.abs(p.x - p.target_x) < 1 && Math.abs(p.y - p.target_y) < 1) {
+ if (p.increases) {
+ lightnings ++;
+ game.buttons.lightning.buttons[0].disabled = false;
+ if (lightnings > MAX_LIGHTNINGS) {
+ lightnings = MAX_LIGHTNINGS;
+ }
+ }
+ p.deleteme = true;
+ }
+ }
+
+ lightning_particles = lightning_particles.filter(p => !p.deleteme);
+
+ if (game.state === State.STAND) {
+ if (timer_running) {
+ timer -= delta;
+ }
+
+ if (timer <= 0) {
+ timer = 0;
+ game_over();
+ }
+
+ if (lightnings > 0) {
+ game.buttons.lightning.buttons[0].disabled = false;
+ } else {
+ game.buttons.lightning.buttons[0].disabled = true;
+ }
+ } else if (game.state === State.REPLACE) {
+ let complete_tile_count = 0;
+ thud_timeout -= delta;
+
+ for (let t of level) {
+ if (t == null) continue;
+
+ if (t.delay > 0) {
+ t.delay -= delta;
+ continue;
+ }
+
+ t.vy += GRAVITY * delta / 1000;
+ t.y += t.vy * delta / 1000;
+
+ if (t.y >= t.tile_y * TILE_H) {
+ if (!t.done && thud_timeout <= 0) {
+ game.sfx.thud.play();
+ thud_timeout = THUD_SPACING;
+ }
+ t.y = t.tile_y * TILE_H;
+ if (t.suit === EVIL_SUIT && t.tile_y === BOARD_H - 1) {
+ game.state = State.DESTROY;
+ } else {
+ t.vy = 0;
+ t.done = true;
+ complete_tile_count ++;
+ }
+ }
+ }
+
+ if (complete_tile_count === BOARD_W * BOARD_H && lightning_particles.length === 0) {
+ game.state = State.STAND;
+ if (!game.music.bgm.playing) game.music.bgm.play();
+ selector.x = -1;
+ selector.y = -1;
+ game_just_started = false;
+ show_title = false;
+ }
+ } else if (game.state === State.DESTROY) {
+ let score_increase_amount = 20;
+ let time_increase_amount = TIME_GAIN_PER_BLOCK;
+
+ if (!game_just_started) {
+ timer_running = true;
+ }
+
+ if (path.length > 0) {
+ game.sfx.bap.play();
+ }
+
+ for (let p of path) {
+ set_tile_at(p[0], p[1], null);
+ score += score_increase_amount;
+ timer += time_increase_amount;
+ if (timer > MAX_TIMER) timer = MAX_TIMER;
+ score_increase_amount += 6;
+ time_increase_amount += TIME_INCREASE_PER_BLOCK;
+ }
+
+ path = [];
+
+ for (let x = 0; x < BOARD_W; x++) {
+ if (tile_at(x, BOARD_H - 1) !== null && tile_at(x, BOARD_H - 1).suit == EVIL_SUIT) {
+ set_tile_at(x, BOARD_H - 1, null);
+ if (!game_just_started) {
+ game.sfx.ok.play();
+ lightning_particles.push({
+ x: board_offset_x + x * TILE_W + TILE_W / 2,
+ y: board_offset_y + (BOARD_H - 1) * TILE_H + TILE_H / 2,
+ target_x: game.buttons.lightning.buttons[0].x + 10,
+ target_y: game.buttons.lightning.buttons[0].y + 8,
+ increases: true,
+ });
+ score += 1000;
+ }
+ }
+ }
+
+ let min_delay = 10000;
+ let created = [];
+
+ for (let y = BOARD_H - 1; y >= 0; y--) {
+ for (let x = 0; x < BOARD_W; x++) {
+ if (tile_at(x, y) === null) {
+ /* find some thing above us to fall down */
+ let found = false;
+ for (let search_y = y - 1; search_y >= 0; search_y --) {
+ let search_tile = tile_at(x, search_y);
+ if (search_tile !== null) {
+ search_tile.vy = 0;
+ set_tile_at(x, y, search_tile);
+ set_tile_at(x, search_y, null);
+ found = true;
+ break;
+ }
+ }
+
+ /* if we found nothing, make something fall down from the sky */
+ if (!found) {
+ place_random_tile(x, y);
+ created.push([x, y]);
+ if (tile_at(x, y).delay < min_delay) {
+ min_delay = tile_at(x, y).delay;
+ }
+ }
+ }
+ }
+ }
+
+ for (let p of created) {
+ tile_at(...p).delay -= min_delay;
+ }
+
+ game.state = State.REPLACE;
+ for (let t of level) {
+ if (t.tile_y * TILE_H > t.y) {
+ t.done = false;
+ }
+ }
+ }
+}
+
+function do_update_menu(delta) {
+ title_timer += delta;
+}
+
+/* ---------- draw functions ----------- */
+
+/* DRAW */
+function do_draw(ctx) {
+ ctx.save();
+
+ if (game.state === State.TITLE || show_title) {
+ zb.image_draw(ctx, game.img.title, board_offset_x + TILE_W * BOARD_W / 2 - game.img.title.width / 2, board_offset_y + TILE_H * BOARD_H / 2 - game.img.title.height / 2);
+
+ zb.buttons.draw(ctx, game.buttons.play);
+ }
+
+ draw_score(ctx);
+
+ draw_tiles(ctx);
+
+ draw_lightning(ctx);
+
+ draw_selector(ctx);
+
+ if (game.state === State.LIGHTNING) {
+ zb.sprite_draw(ctx, game.img.lightning, 9, 9, 0, 0, mouse_x - 4, mouse_y - 4);
+ }
+
+ if (game.state === State.GAMEOVER) {
+ zb.image_draw(ctx, game.img.gameover, board_offset_x + BOARD_W * TILE_W / 2 - game.img.gameover.width / 2, board_offset_y + BOARD_H * TILE_H / 2 - game.img.gameover.height / 2);
+
+ zb.buttons.draw(ctx, game.buttons.replay);
+ }
+
+ ctx.restore();
+}
+
+function draw_score_num(ctx, x, y, num) {
+ let render_count = num;
+ let col = 1;
+ let lb = game.buttons.lightning.buttons[0];
+ while (col <= 8 || render_count > 0) {
+ zb.sprite_draw(ctx, game.img.scorenumbers, 8, 8, render_count % 10, 0, x + (8 - col) * 8, y);
+ render_count = Math.floor(render_count / 10);
+ col ++;
+ }
+}
+
+function draw_score(ctx) {
+ let timebar_x = board_offset_x + BOARD_W * TILE_W / 2 - game.img.timebar_outside.width / 2 + 16;
+ let timebar_y = board_offset_y - 18 - game.img.timebar_outside.height / 2;
+
+ if (game.state === State.GAMEOVER || game.state === State.TITLE) {
+ draw_score_num(ctx, timebar_x, timebar_y + 2, high_score);
+
+ zb.sprite_draw(ctx, game.img.text, 32, 12, 0, 0, timebar_x - game.img.text.width, timebar_y + 2); // "best"
+ } else {
+ zb.image_draw(ctx, game.img.timebar_outside, timebar_x, timebar_y);
+ let timebar_width = Math.round((timer / MAX_TIMER) * game.img.timebar_inside.width);
+ zb.sprite_draw(ctx, game.img.timebar_inside, timebar_width, game.img.timebar_inside.height, 0, 0, timebar_x, timebar_y);
+
+ zb.sprite_draw(ctx, game.img.text, 32, 12, 0, 2, timebar_x - game.img.text.width, timebar_y + 2); // "time"
+ }
+
+ zb.sprite_draw(ctx, game.img.text, 32, 12, 0, 1, timebar_x - game.img.text.width, timebar_y - 10); // "score"
+
+ draw_score_num(ctx, timebar_x, timebar_y - 10, displayed_score);
+}
+
+function draw_tiles(ctx) {
+ ctx.save();
+
+ ctx.translate(board_offset_x, board_offset_y);
+
+ for (let t of level) {
+ if (t == null) continue;
+
+ zb.sprite_draw(ctx, game.img.tiles, TILE_W, TILE_H, t.suit, t.color, t.x, t.y);
+ }
+
+ ctx.restore();
+}
+
+function draw_selector(ctx) {
+ ctx.save();
+
+ ctx.translate(board_offset_x, board_offset_y);
+
+ if ((game.state === State.STAND || game.state === State.LIGHTNING) && selector.x !== -1 && selector.y !== -1) {
+ zb.sprite_draw(ctx, game.img.selector, 20, 20, 0, 0, selector.x * TILE_W - 2, selector.y * TILE_H - 2);
+ } else if (game.state === State.DRAG) {
+ let last_p = null;
+ for (let p of path) {
+ if (last_p === null) {
+ zb.sprite_draw(ctx, game.img.connector, 20, 20, 0, 0, p[0] * TILE_W - 2, p[1] * TILE_H - 2);
+ } else {
+ zb.sprite_draw(ctx, game.img.connector, 20, 20, 0, 0, p[0] * TILE_W - 2, p[1] * TILE_H - 2);
+ }
+ if (last_p) {
+ if (last_p[0] == p[0]) {
+ for (let y = Math.min(last_p[1], p[1]) + 1; y <= Math.max(last_p[1], p[1]) - 1; y++) {
+ zb.sprite_draw(ctx, game.img.connector, 20, 20, 2, 0, p[0] * TILE_W - 2, y * TILE_H - 2);
+ }
+ } else if (last_p[1] == p[1]) {
+ for (let x = Math.min(last_p[0], p[0]) + 1; x <= Math.max(last_p[0], p[0]) - 1; x++) {
+ zb.sprite_draw(ctx, game.img.connector, 20, 20, 1, 0, x * TILE_W - 2, p[1] * TILE_H - 2);
+ }
+ }
+ }
+ last_p = p;
+ }
+
+ last_p = null;
+ for (let p of path) {
+ if (last_p === null) {
+ zb.sprite_draw(ctx, game.img.connector, 20, 20, 0, 1, p[0] * TILE_W - 2, p[1] * TILE_H - 2);
+ } else {
+ zb.sprite_draw(ctx, game.img.connector, 20, 20, 0, 1, p[0] * TILE_W - 2, p[1] * TILE_H - 2);
+ }
+ if (last_p) {
+ if (last_p[0] == p[0]) {
+ for (let y = Math.min(last_p[1], p[1]) + 1; y <= Math.max(last_p[1], p[1]) - 1; y++) {
+ zb.sprite_draw(ctx, game.img.connector, 20, 20, 2, 1, p[0] * TILE_W - 2, y * TILE_H - 2);
+ }
+ } else if (last_p[1] == p[1]) {
+ for (let x = Math.min(last_p[0], p[0]) + 1; x <= Math.max(last_p[0], p[0]) - 1; x++) {
+ zb.sprite_draw(ctx, game.img.connector, 20, 20, 1, 1, x * TILE_W - 2, p[1] * TILE_H - 2);
+ }
+ }
+ }
+ last_p = p;
+ }
+ }
+
+ ctx.restore();
+}
+
+function draw_lightning(ctx) {
+ if (game.state !== State.GAMEOVER && game.state !== State.TITLE) {
+ zb.buttons.draw(ctx, game.buttons.lightning);
+
+ let render_count = lightnings;
+ let col = 1;
+ let lb = game.buttons.lightning.buttons[0];
+ while (render_count > 0 || col == 1) {
+ zb.sprite_draw(ctx, game.img.lightningnumbers, 5, 8, render_count % 10, 0, lb.x + game.buttons.lightning.button_w - 7 - 5 * col, lb.y + 4);
+ render_count = Math.floor(render_count / 10);
+ col ++;
+ }
+ }
+
+ for (let lp of lightning_particles) {
+ zb.sprite_draw(ctx, game.img.lightning, 9, 9, 0, 0, lp.x - 4, lp.y - 4);
+ }
+}
+
+function do_draw_menu(ctx) {
+ ctx.save();
+ ctx.translate(0, Math.sin(title_timer / TITLE_BOUNCE_LENGTH * 2 * Math.PI) * TITLE_BOUNCE_DISTANCE);
+ zb.screen_draw(ctx, game.img.title);
+ ctx.restore();
+
+ zb.screen_draw(ctx, game.img.titlegraphic);
+ zb.buttons.draw(ctx, game.buttons.menu);
+}
+
+function do_draw_youwon(ctx) {
+ zb.screen_draw(ctx, game.img.youwin);
+}
+
+/* ---------- event handlers ------------ */
+
+function handle_gamestart(game) {
+ console.log("Game start!");
+
+ game.state = State.TITLE;
+
+ //game.music.bgm_menu.play();
+}
+
+function toggle_mute_button() {
+ /*if (game.buttons.ingame.buttons[2].id === 2) {
+ game.buttons.ingame.buttons[2].id = 3;
+ } else {
+ game.buttons.ingame.buttons[2].id = 2;
+ }*/
+ game.toggle_mute();
+}
+
+function handle_mousedown(game, e, x, y) {
+ if (game.state === State.TITLE) {
+ if (zb.buttons.click(game.buttons.play, x, y)) return;
+ } else if (game.state === State.GAMEOVER) {
+ if (zb.buttons.click(game.buttons.replay, x, y)) return;
+ } else {
+ if (zb.buttons.click(game.buttons.lightning, x, y)) return;
+
+ if (game.state === State.STAND) {
+ if (selector.x !== -1 && selector.y !== -1) {
+ path = [ [selector.x, selector.y] ];
+ game.state = State.DRAG;
+ }
+ } else if (game.state === State.LIGHTNING) {
+ if (selector.x !== -1 && selector.y !== -1) {
+ clicked_once_lightning = false;
+ let destroy_tile = tile_at(selector.x, selector.y);
+ let destroy_suit = destroy_tile.suit;
+ let destroy_color = destroy_tile.color;
+
+ if (destroy_suit === EVIL_SUIT) return;
+
+ lightnings --;
+
+ let cx = board_offset_x + selector.x * TILE_W + TILE_W / 2;
+ let cy = board_offset_y + selector.y * TILE_W + TILE_W / 2;
+ game.sfx.lightning.play();
+ for (let i = 0; i < 10; i++) {
+ lightning_particles.push({
+ x: cx,
+ y: cy,
+ target_x: cx + 50 * Math.cos(i * (2 * Math.PI / 10)),
+ target_y: cy + 50 * Math.sin(i * (2 * Math.PI / 10)),
+ increases: false,
+ });
+ }
+
+ for (let x = 0; x < BOARD_W; x ++) {
+ for (let y = 0; y < BOARD_H; y ++) {
+ let tile = tile_at(x, y);
+ if (tile.suit === destroy_suit && tile.color === destroy_color || tile.suit === WILD_SUIT && destroy_suit === WILD_SUIT) {
+ set_tile_at(x, y, null);
+ if (destroy_suit === WILD_SUIT) {
+ score += 400;
+ } else {
+ score += 25;
+ }
+ }
+ }
+ }
+ game.state = State.DESTROY;
+ }
+ }
+ }
+}
+
+function handle_mousemove(game, e, x, y) {
+ mouse_x = x;
+ mouse_y = y;
+
+ if (game.state === State.TITLE) {
+ zb.buttons.update(game.buttons.play, x, y);
+ } else if (game.state === State.GAMEOVER) {
+ zb.buttons.update(game.buttons.replay, x, y);
+ } else {
+ zb.buttons.update(game.buttons.lightning, x, y);
+
+ let tile_x = -1, tile_y = -1;
+ if (x >= board_offset_x && x < board_offset_x + TILE_W * BOARD_W
+ && y >= board_offset_y && y < board_offset_y + TILE_H * BOARD_H) {
+ tile_x = Math.floor((x - board_offset_x) / TILE_W);
+ tile_y = Math.floor((y - board_offset_y) / TILE_H);
+ }
+
+ if (game.state === State.STAND) {
+ if (tile_x !== -1 && tile_y !== -1 && tile_at(tile_x, tile_y).suit !== EVIL_SUIT) {
+ selector.x = tile_x;
+ selector.y = tile_y;
+ } else {
+ selector.x = -1;
+ selector.y = -1;
+ }
+ } else if (game.state === State.DRAG) {
+ if (tile_x === -1 || tile_y === -1) return;
+
+ let last_path_coords = path[path.length - 1];
+ let second_last_path_coords = null;
+ if (path.length >= 2) {
+ second_last_path_coords = path[path.length - 2];
+ }
+
+ let already_in_path = path.findIndex(p => p[0] === tile_x && p[1] === tile_y);
+
+ if (already_in_path === -1) {
+ if (tile_x === last_path_coords[0] && Math.abs(tile_y - last_path_coords[1]) === 1) {
+ let ydir = 1;
+ if (tile_y < last_path_coords[1]) ydir = -1;
+ for (let y = last_path_coords[1] + ydir; y != tile_y + ydir; y += ydir) {
+ last_path_coords = path[path.length - 1];
+ let end_of_path_tile = tile_at(last_path_coords[0], last_path_coords[1]);
+ let hover_tile = tile_at(tile_x, y);
+
+ if (hover_tile.suit !== EVIL_SUIT &&
+ (hover_tile.suit === end_of_path_tile.suit || hover_tile.color === end_of_path_tile.color
+ || hover_tile.suit === WILD_SUIT || end_of_path_tile.suit === WILD_SUIT)) {
+ if (Math.abs(tile_x - last_path_coords[0]) > 0 || Math.abs(y - last_path_coords[1]) > 0) {
+ path.push([tile_x, y]);
+ }
+ }
+ }
+ } else if (Math.abs(tile_x - last_path_coords[0]) === 1 && tile_y === last_path_coords[1]) {
+ let xdir = 1;
+ if (tile_x < last_path_coords[0]) xdir = -1;
+ for (let x = last_path_coords[0] + xdir; x != tile_x + xdir; x += xdir) {
+ last_path_coords = path[path.length - 1];
+ let end_of_path_tile = tile_at(last_path_coords[0], last_path_coords[1]);
+ let hover_tile = tile_at(x, tile_y);
+
+ if (hover_tile.suit !== EVIL_SUIT &&
+ (hover_tile.suit === end_of_path_tile.suit || hover_tile.color === end_of_path_tile.color
+ || hover_tile.suit === WILD_SUIT || end_of_path_tile.suit === WILD_SUIT)) {
+ if (Math.abs(x - last_path_coords[0]) > 0 || Math.abs(tile_y - last_path_coords[1]) > 0) {
+ path.push([x, tile_y]);
+ }
+ }
+ }
+ }
+ } else {
+ path.length = already_in_path + 1;
+ }
+ } else if (game.state === State.LIGHTNING) {
+ if (tile_x !== -1 && tile_y !== -1) {
+ selector.x = tile_x;
+ selector.y = tile_y;
+ } else {
+ selector.x = -1;
+ selector.y = -1;
+ }
+ }
+ }
+}
+
+let clicked_once_lightning = false;
+
+function handle_mouseup(game, e, x, y) {
+ if (game.state === State.TITLE) {
+ zb.buttons.update(game.buttons.play, x, y);
+ } else if (game.state === State.GAMEOVER) {
+ zb.buttons.update(game.buttons.replay, x, y);
+ } else if (game.mode === 'youwon') {
+ game.long_transition(zb.transition.type.fade, 1000, function() {
+ game.mode = 'menu';
+ continue_level = 1;
+ });
+ } else {
+ zb.buttons.update(game.buttons.lightning, x, y);
+
+ let tile_x = -1, tile_y = -1;
+ if (x >= board_offset_x && x < board_offset_x + TILE_W * BOARD_W
+ && y >= board_offset_y && y < board_offset_y + TILE_H * BOARD_H) {
+ tile_x = Math.floor((x - board_offset_x) / TILE_W);
+ tile_y = Math.floor((y - board_offset_y) / TILE_H);
+ }
+
+ if (game.state === State.DRAG) {
+ let start_of_path_tile = tile_at(path[0][0], path[0][1]);
+ let end_of_path_tile = tile_at(path[path.length - 1][0], path[path.length - 1][1]);
+ if (path.length > 1 && start_of_path_tile.suit === end_of_path_tile.suit && start_of_path_tile.color === end_of_path_tile.color && start_of_path_tile.suit !== WILD_SUIT) {
+ game.state = State.DESTROY;
+ } else {
+ game.state = State.STAND;
+ path = [];
+
+ if (tile_x !== -1 && tile_y !== -1) {
+ selector.x = tile_x;
+ selector.y = tile_y;
+ } else {
+ selector.x = -1;
+ selector.y = -1;
+ }
+ }
+ } else if (game.state === State.LIGHTNING) {
+ if (tile_x !== -1 && tile_y !== -1) {
+ clicked_once_lightning = false;
+ } else {
+ if (!clicked_once_lightning) {
+ clicked_once_lightning = true;
+ return;
+ } else {
+ clicked_once_lightning = false;
+ game.state = State.STAND;
+ game.buttons.lightning.buttons[0].disabled = false;
+ selector.x = -1;
+ selector.y = -1;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+"use strict";
+
+/* Please forgive my strictly procedural/functional style here.
+ * Thanks for reading though! */
+
+let zb = (function() {
+ /* ---- Util ---- */
+
+ function mod(x, n) {
+ return ((x%n)+n)%n;
+ }
+
+ function sgn(x) {
+ if (x === 0) {
+ return 0;
+ } else {
+ return x / Math.abs(x);
+ }
+ }
+
+ function as_hex(num, length) {
+ let result = Math.round(num).toString(16);
+
+ if (length) {
+ while (result.length < length) {
+ result = "0" + result;
+ }
+ }
+
+ return result;
+ }
+
+ function copy_list(list) {
+ let newlist = [];
+ for (let x of list) {
+ newlist.push(x);
+ }
+ return newlist;
+ }
+
+ function copy_flat_objlist(list) {
+ let newlist = [];
+ for (let x of list) {
+ newlist.push({ ...x });
+ }
+ return newlist;
+ }
+
+ function rand_int(a, b) {
+ }
+
+ /* ---- Resource loading / loading screen ---- */
+
+ let _audiocheck = document.createElement('audio');
+
+ let _SFX_ARRAY_SIZE = 10;
+
+ /* Returns a callback that should be called when a resource finishes loading. */
+ function _register_resource(name, game, callback) {
+ game._total_things_to_load ++;
+ console.log("Loading", name + ". Things to load:", game._total_things_to_load);
+ return function() {
+ if (!game.ready_to_go) {
+ game._things_loaded ++;
+ console.log("Loaded", name + ". Things loaded:", game._things_loaded, "/", game._total_things_to_load);
+ if (game.load_with_progress_bar) {
+ if (game._progressbar_img_loaded) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(
+ game.img._progressbar,
+ 0,
+ 0,
+ Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width),
+ game.img._progressbar.height,
+ Math.round(game.canvas_w / 2 / game.draw_scale - game.img._progressbar.width / 2),
+ Math.round(game.canvas_h / 2 / game.draw_scale - game.img._progressbar.height / 2),
+ Math.round(game._things_loaded / game._total_things_to_load * game.img._progressbar.width),
+ game.img._progressbar.height,
+ );
+ game.ctx.global.restore();
+ }
+ }
+ _check_if_loaded(game);
+ if (callback) {
+ callback();
+ }
+ }
+ }
+ }
+
+ /* Check if audio failed to load bc file doesn't exist and print error message. */
+ /* TODO add the same thing for an image */
+ function _sound_resource_error(name, game, audio) {
+ return function(e) {
+ if (audio === undefined) {
+ console.error("Error loading resource, skipping:", name);
+ game._things_loaded ++;
+ _check_if_loaded(game);
+ }
+ }
+ }
+
+ /* Check if the game has loaded everything and if so show the 'click to start' image */
+ function _check_if_loaded(game) {
+ if (game.ready_to_go) return;
+
+ if (game._things_loaded >= game._total_things_to_load) {
+ console.log("Ready");
+ game.ready_to_go = true;
+ game._on_ready();
+ }
+ }
+
+ function _register_sfx(sfxdata, game) {
+ for (let key in sfxdata) {
+ let sfx_size = sfxdata[key].copies || _SFX_ARRAY_SIZE;
+ let sfx_array = new Array(sfx_size);
+ for (let i = 0; i < sfx_size; i++) {
+ sfx_array[i] = new Audio(sfxdata[key].path);
+ sfx_array[i].addEventListener('canplaythrough',
+ _register_resource(sfxdata[key].path + '#' + (i+1), game), false);
+ sfx_array[i].addEventListener('error',
+ _sound_resource_error(sfxdata[key].path + '#' + (i+1), game), false, sfx_array[i]);
+ if (sfxdata[key].hasOwnProperty('volume')) {
+ sfx_array[i].volume = sfxdata[key].volume;
+ }
+ }
+ game.sfx[key] = {
+ _array: sfx_array,
+ _index: 0,
+ play: function() {
+ if (!game.muted) {
+ this._array[this._index].currentTime = 0;
+ this._array[this._index].play();
+ this._index ++;
+ this._index = mod(this._index, _SFX_ARRAY_SIZE);
+ }
+ }
+ }
+ }
+ }
+
+ function _register_music(musicdata, game) {
+ for (let key in musicdata) {
+ let musicpath;
+ if (musicdata[key].path.match(/\.[a-z]{3}$/)) {
+ musicpath = musicdata[key].path;
+ } else if (_audiocheck.canPlayType('audio/mpeg')) {
+ musicpath = musicdata[key].path + '.mp3';
+ } else if (_audiocheck.canPlayType('audio/ogg')) {
+ musicpath = musicdata[key].path + '.ogg';
+ } else {
+ console.log("No supported music type known :(");
+ return;
+ }
+ let music = new Audio(musicpath);
+ if (musicdata[key].hasOwnProperty('volume')) {
+ music.volume = musicdata[key].volume;
+ }
+
+ if (!musicdata[key].hasOwnProperty('loop') || musicdata[key].loop) {
+ /* We loop by default unless 'loop: false' is specified. */
+ music.loop = true;
+ }
+ music.addEventListener('canplaythrough', _register_resource(musicpath, game), false);
+ music.addEventListener('error', _sound_resource_error(musicpath, game, music), false);
+
+ /* Handle music changes when muted, so when we unmute,
+ * the correct music will be playing. */
+ music._og_play = music.play;
+ music.play = function() {
+ if (game.muted) {
+ music.was_playing = true;
+ } else {
+ music._og_play();
+ }
+ }
+
+ music._og_pause = music.pause;
+ music.pause = function() {
+ if (game.muted) {
+ music.was_playing = false;
+ } else {
+ music._og_pause();
+ }
+ }
+
+ game.music[key] = music;
+ }
+ }
+
+ function _register_images(imgdata, game) {
+ function _recursive_load_images(pathmap) {
+ let result = {};
+ for (let key in pathmap) {
+ if (typeof pathmap[key] === 'object') {
+ result[key] = _recursive_load_images(pathmap[key]);
+ } else {
+ result[key] = new Image();
+ result[key].onload = _register_resource(pathmap[key], game);
+ result[key].src = pathmap[key];
+ }
+ }
+ return result;
+ }
+
+ let loaded_imgs = _recursive_load_images(imgdata);
+ for (let k in loaded_imgs) {
+ game.img[k] = loaded_imgs[k];
+ }
+ }
+
+ function _create_canvas_context(canvas) {
+ let new_canvas = document.createElement('canvas');
+ new_canvas.width = canvas.width;
+ new_canvas.height = canvas.height;
+
+ let new_ctx = new_canvas.getContext('2d');
+ new_ctx.imageSmoothingEnabled = false;
+ new_ctx.webkitImageSmoothingEnabled = false;
+ new_ctx.mozImageSmoothingEnabled = false;
+
+ return new_ctx;
+ }
+
+ function create_game(params) {
+ let game_props = {...params};
+
+ game_props.frame_rate = params.frame_rate || 60;
+
+ game_props.canvas_w = params.canvas_w || 640;
+ game_props.canvas_h = params.canvas_h || 480;
+ game_props.draw_scale = params.draw_scale || 4;
+ game_props.top_border = params.top_border || 0;
+ game_props.bottom_border = params.bottom_border || 8;
+ game_props.left_border = params.left_border || 0;
+ game_props.right_border = params.right_border || 0;
+
+ game_props.tile_size = params.tile_size || 16;
+ game_props.level_w = params.level_w || 10;
+ game_props.level_h = params.level_h || 7;
+
+ game_props.screen_w = game_props.canvas_w / game_props.draw_scale;
+ game_props.screen_h = game_props.canvas_h / game_props.draw_scale;
+
+ game_props.background_color = params.background_color || '#000000';
+
+ let canvas = document.getElementById(game_props.canvas);
+
+ let global_ctx = canvas.getContext('2d');
+ global_ctx.imageSmoothingEnabled = false;
+ global_ctx.webkitImageSmoothingEnabled = false;
+ global_ctx.mozImageSmoothingEnabled = false;
+
+ let mask_canvas = document.createElement('canvas');
+ mask_canvas.width = canvas.width;
+ mask_canvas.height = canvas.height;
+ let mask_ctx = mask_canvas.getContext('2d');
+ mask_ctx.imageSmoothingEnabled = false;
+ mask_ctx.webkitImageSmoothingEnabled = false;
+ mask_ctx.mozImageSmoothingEnabled = false;
+
+ let copy_canvas = document.createElement('canvas');
+ copy_canvas.width = canvas.width;
+ copy_canvas.height = canvas.height;
+ let copy_ctx = copy_canvas.getContext('2d');
+ copy_ctx.imageSmoothingEnabled = false;
+ copy_ctx.webkitImageSmoothingEnabled = false;
+ copy_ctx.mozImageSmoothingEnabled = false;
+
+ let draw_canvas = document.createElement('canvas');
+ draw_canvas.width = canvas.width;
+ draw_canvas.height = canvas.height;
+ let draw_ctx = draw_canvas.getContext('2d');
+ draw_ctx.imageSmoothingEnabled = false;
+ draw_ctx.webkitImageSmoothingEnabled = false;
+ draw_ctx.mozImageSmoothingEnabled = false;
+
+ let game = {
+ /* General properties */
+ ...game_props,
+
+ canvas: canvas,
+
+ /* Loading */
+ ready_to_go: false,
+ _total_things_to_load: 1,
+ _things_loaded: 0,
+ resources_ready: function() {
+ this._things_loaded ++;
+ console.log("Finished enumerating resources to load. Things loaded:",
+ this._things_loaded, "/", this._total_things_to_load);
+ _check_if_loaded(this);
+ },
+ _on_ready: function() {
+ this.ready_to_go = true;
+ if (game.img._clicktostart && game.img._clicktostart.complete) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(game.img._clicktostart, 0, 0);
+ game.ctx.global.restore();
+ }
+ },
+ playing: false,
+ play: function() {
+ this.playing = true;
+ if (this.events.gamestart) {
+ this.events.gamestart(this);
+ }
+ _loop(this);
+ },
+ _norun: false,
+
+ /* Audio */
+ sfx: {},
+ music: {},
+ muted: false,
+ mute: function() {
+ _mute(this);
+ },
+ unmute: function() {
+ _unmute(this);
+ },
+ toggle_mute: function() {
+ if (this.muted) {
+ _unmute(this);
+ } else {
+ _mute(this);
+ }
+ },
+ register_sfx: function(sfxdata) {
+ _register_sfx(sfxdata, this);
+ },
+ register_music: function(musicdata) {
+ _register_music(musicdata, this);
+ },
+
+ /* Drawing */
+ ctx: {
+ global: global_ctx, /* context for the actual real canvas */
+ mask: mask_ctx, /* context for drawing the transition mask, gets scaled up */
+ copy: copy_ctx, /* context for copying the old screen on transition */
+ draw: draw_ctx, /* context for drawing the real level */
+ },
+ create_canvas_context: function() {
+ /* Function to return a fresh screen-sized canvas context, if we need it. */
+ return _create_canvas_context(this.canvas);
+ },
+ img: {},
+ register_images: function(imgdata) {
+ _register_images(imgdata, this);
+ },
+
+ /* Save */
+ save: function(key, data) {
+ _save(game, key, data);
+ },
+ load: function(key) {
+ return _load(game, key);
+ },
+
+ /* Transition system */
+ transition: _transition,
+ start_transition: function(type, length, callback, on_done) {
+ _start_transition(this, type, length, callback, on_done);
+ },
+ long_transition: function(type, length, callback, on_done) {
+ _long_transition(this, type, length, callback, on_done);
+ },
+
+ /* Input */
+ touchmode: false,
+
+ /* Mode */
+ change_mode: function(newmode) {
+ _change_mode(game, newmode);
+ }
+ };
+
+ window.requestAnimFrame = (function() {
+ return window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ function(callback, element) {
+ window.setTimeout(callback, 1000/game.frame_rate);
+ };
+ })();
+
+ /* Register event listeners */
+ for (let ev in params.events) {
+ /* Register any other events I guess */
+ canvas['on' + ev] = function(e) {
+ if (game.playing && !game._norun) {
+ params.events[ev](game, e);
+ }
+ }
+ }
+
+ /* Override with special events */
+ canvas.onmousedown = function(e) {
+ if (!game._norun) {
+ _handle_mousedown(game, e);
+ }
+ }
+
+ canvas.onmousemove = function(e) {
+ if (!game._norun) {
+ _handle_mousemove(game, e);
+ }
+ }
+
+ canvas.onmouseup = function(e) {
+ if (!game._norun) {
+ _handle_mouseup(game, e);
+ }
+ }
+
+ canvas.ontouchstart = function(e) {
+ if (!game._norun) {
+ game.touchmode = true;
+ _handle_touchstart(game, e);
+ }
+ e.preventDefault();
+ }
+
+ canvas.ontouchmove = function(e) {
+ if (!game._norun) {
+ game.touchmode = true;
+ _handle_touchmove(game, e);
+ }
+ e.preventDefault();
+ }
+
+ canvas.ontouchend = function(e) {
+ if (!game._norun) {
+ game.touchmode = true;
+ _handle_touchend(game, e);
+ }
+ e.preventDefault();
+ }
+
+ canvas.onkeydown = function(e) {
+ if (!game._norun && game.events.keydown) {
+ game.events.keydown(game, e);
+ e.preventDefault();
+ }
+ }
+
+ canvas.onkeyup = function(e) {
+ if (!game._norun && game.events.keyup) {
+ game.events.keyup(game, e);
+ e.preventDefault();
+ }
+ }
+
+ canvas.onblur = function(e) {
+ if (game.playing && !game.run_in_background) {
+ game._norun = true;
+ _stop_music(game);
+ }
+ }
+
+ canvas.onfocus = function(e) {
+ if (game.playing && !game.run_in_background) {
+ game._norun = false;
+ if (!game.muted) {
+ _start_music(game);
+ }
+ _loop(game);
+ }
+ }
+
+ /* Set loading stuff */
+ let loading_img = new Image();
+ loading_img.onload = _register_resource('loading.png', game, function() {
+ if (!game.ready_to_go) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(loading_img, 0, 0);
+ game.ctx.global.restore();
+ }
+ });
+ loading_img.src = 'loading.png';
+
+ if (game.load_with_progress_bar) {
+ game._progressbar_img_loaded = false;
+ game._progressbar_width = 0;
+ game._progressbar_height = 0;
+
+ let progressbar_img = new Image();
+ progressbar_img.onload = _register_resource('progressbar.png', game, function() {
+ game._progressbar_img_loaded = true;
+ game._progressbar_width = progressbar_img.width;
+ game._progressbar_height = progressbar_img.height;
+ if (!game.ready_to_go) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(loading_img, 0, 0);
+ game.ctx.global.restore();
+ }
+ });
+ progressbar_img.src = 'progressbar.png';
+ game.img._progressbar = progressbar_img;
+ }
+
+ if (!game.run_in_background) {
+ let pause_img = new Image();
+ pause_img.onload = _register_resource('pause.png', game);
+ pause_img.src = 'pause.png';
+ game.img._pause = pause_img;
+ }
+
+ if (game.hasOwnProperty('save_key')) {
+ let saveerror_img = new Image();
+ saveerror_img.onload = _register_resource('saveerror.png', game);
+ saveerror_img.src = 'saveerror.png';
+ game.img._saveerror = saveerror_img;
+ }
+
+ let clicktostart_img = new Image();
+ clicktostart_img.onload = _register_resource('clicktostart.png', game, function() {
+ if (game.ready_to_go) {
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(game.img._clicktostart, 0, 0);
+ game.ctx.global.restore();
+ }
+ });
+ clicktostart_img.src = 'clicktostart.png';
+ game.img._clicktostart = clicktostart_img;
+
+ return game;
+ }
+
+ /* ---- Audio ---- */
+
+ function _stop_music(game) {
+ if (game.muted) return;
+
+ for (let m in game.music) {
+ if (!game.music[m].paused) {
+ game.music[m].was_playing = true;
+ game.music[m].pause();
+ } else {
+ game.music[m].was_playing = false;
+ }
+ }
+ }
+
+ function _start_music(game, unmuting) {
+ if (game.muted && !unmuting) return;
+
+ for (let m in game.music) {
+ if (game.music[m].was_playing) {
+ game.music[m].play();
+ }
+ }
+ }
+
+ function _mute(game) {
+ _stop_music(game);
+ game.muted = true;
+ }
+
+ function _unmute(game) {
+ game.muted = false;
+ _start_music(game);
+ }
+
+ /* ---- Game update stuff ---- */
+
+ let _last_frame_time;
+ let _timedelta = 0;
+ function _loop(game, timestamp) {
+ if (timestamp == undefined) {
+ timestamp = 0;
+ _last_frame_time = timestamp;
+ } else if (_last_frame_time == 0) {
+ _last_frame_time = timestamp;
+ } else {
+ _timedelta += timestamp - _last_frame_time;
+ _last_frame_time = timestamp;
+
+ while (_timedelta >= 1000 / game.frame_rate) {
+ _update(game, 1000 / game.frame_rate);
+ _timedelta -= 1000 / game.frame_rate;
+ }
+ }
+
+ _draw(game);
+
+ if (!game._norun) {
+ requestAnimFrame(function(timestamp) {
+ _loop(game, timestamp);
+ });
+ }
+ }
+
+ function _update(game, delta) {
+ if (game.update_func) {
+ game.update_func(delta);
+ }
+
+ if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].update) {
+ game.modes[game.mode].update(delta);
+ }
+
+ if (game.transition.is_transitioning) {
+ game.transition.timer += delta;
+ if (game.transition.timer > game.transition.end_time) {
+ _finish_transition(game);
+ }
+ }
+ }
+
+ function _change_mode(game, newmode) {
+ game.mode = newmode;
+ }
+
+ /* ---- UI ---- */
+
+ function create_buttons(button_data) {
+ for (let b of button_data.buttons) {
+ b.state = 0;
+ }
+
+ return button_data;
+ }
+
+ /* Call with a button set and given x / y mouse coordinates.
+ * Will update the button set in place.
+ * Should generally be called in mouse move and mouse up event handlers. */
+ function update_buttons(button_data, x, y) {
+ for (let button of button_data.buttons) {
+ if (button.state === 2) continue;
+ if (button.disabled) continue;
+
+ if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) {
+ if (!game.touchmode) {
+ button.state = 1;
+ }
+ } else if (button.state !== 2) {
+ button.state = 0;
+ }
+ }
+ }
+
+ /* Call with a button set and given x / y mouse coordinates.
+ * If a button is clicked, triggers its callback and returns true.
+ * Otherwise, returns false.
+ * Should be called in mouse down event handler. */
+ function click_button(button_data, x, y) {
+ for (let button of button_data.buttons) {
+ if (button.disabled) continue;
+
+ if (x >= button.x && x < button.x + button_data.button_w && y >= button.y && y < button.y + button_data.button_h) {
+ button.state = 2;
+ game._clicked_button = button;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function _handle_clicked_button(game) {
+ if (game._clicked_button) {
+ game._clicked_button.state = 0;
+ _draw(game);
+ if (game._clicked_button.callback) {
+ game._clicked_button.callback();
+ }
+ game._clicked_button = null;
+ return;
+ }
+ }
+
+ /* ---- Mouse ---- */
+
+ function _handle_mousedown(game, e) {
+ if (!game.playing) return;
+
+ game.touchmode = false;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (e.button === 0 && game.events.mousedown) {
+ game.events.mousedown(game, e, x, y);
+ }
+ }
+
+ function _handle_mouseup(game, e) {
+ if (!game.playing && game.ready_to_go) {
+ /* Click to start */
+ if (game.hasOwnProperty('save_key') && !game._show_save_error) {
+ /* Test if save/load is working */
+ try {
+ console.log("Test saving");
+ localStorage.setItem(game.save_key + ".test", "savetest");
+ let savetest = localStorage.getItem(game.save_key + ".test");
+ if (savetest !== "savetest") {
+ throw new Error("oops");
+ }
+ } catch (e) {
+ /* If error saving, show save error message first */
+ /* We set _show_save_error so that the second time the user clicks,
+ * the game will actually start */
+ console.log("- SAVE FAILED -");
+ game._show_save_error = true;
+ game.ctx.global.save();
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+ game.ctx.global.drawImage(game.img._saveerror, 0, 0);
+ game.ctx.global.restore();
+ return;
+ }
+ }
+ game.play();
+ return;
+ }
+
+ game.touchmode = false;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+
+ _handle_clicked_button(game);
+
+ if (e.button === 0 && game.events.mouseup) {
+ game.events.mouseup(game, e, x, y);
+ }
+ }
+
+ function _handle_mousemove(game, e) {
+ game.touchmode = false;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ }
+
+ /* ---- Touch (defaults to same as mouse) ---- */
+
+ let _last_touch_event;
+
+ function _handle_touchstart(game, e) {
+ if (!game.playing) return;
+
+ game.touchmode = true;
+
+ if (e.touches) e = e.touches[0];
+
+ _last_touch_event = e;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (game.events.touchstart) {
+ game.events.touchstart(game, e, x, y);
+ } else if (game.events.mousedown) {
+ if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ game.events.mousedown(game, e, x, y);
+ }
+ }
+
+ function _handle_touchend(game, e) {
+ if (!game.playing && game.ready_to_go) {
+ /* Click to start */
+ game.play();
+ return;
+ }
+
+ game.touchmode = true;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((_last_touch_event.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((_last_touch_event.clientY - rect.top) / rect.height * game.screen_h) - 1;
+
+ _handle_clicked_button(game);
+
+ if (game.events.touchend) {
+ game.events.touchend(game, e, x, y);
+ } else if (game.events.mouseup) {
+ if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ game.events.mouseup(game, e, x, y);
+ }
+ }
+
+ function _handle_touchmove(game, e) {
+ if (!game.playing) return;
+
+ game.touchmode = true;
+
+ if (e.touches) e = e.touches[0];
+
+ _last_touch_event = e;
+
+ const rect = game.canvas.getBoundingClientRect();
+ let x = Math.round((e.clientX - rect.left) / rect.width * game.screen_w) - 1;
+ let y = Math.round((e.clientY - rect.top) / rect.height * game.screen_h) - 1;
+ if (game.events.touchmove) {
+ game.events.touchmove(game, e, x, y);
+ } else if (game.events.mousemove) {
+ game.events.mousemove(game, e, x, y);
+ }
+ }
+
+ /* ---- Drawing stuff ---- */
+
+ function _draw(game) {
+ game.ctx.draw.save();
+
+ game.ctx.draw.fillStyle = game.background_color;
+
+ game.ctx.draw.fillRect(0, 0, game.screen_w, game.screen_h);
+
+ if (game.draw_func) {
+ game.draw_func(game.ctx.draw);
+ }
+
+ if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw) {
+ game.modes[game.mode].draw(game.ctx.draw);
+ }
+
+ if (game.transition.mid_long) {
+ game.ctx.draw.fillStyle = game.transition.color;
+ game.ctx.draw.fillRect(-1, -1, game.canvas_w + 5, game.canvas_h + 5);
+ }
+
+ game.ctx.draw.restore();
+
+ if (game.transition.is_transitioning) {
+ _draw_transition(game, game.ctx.mask);
+ } else {
+ game.ctx.mask.drawImage(game.ctx.draw.canvas, 0, 0);
+ }
+
+ if (game.modes && game.modes.hasOwnProperty(game.mode) && game.modes[game.mode].draw_after_transition) {
+ game.modes[game.mode].draw_after_transition(game.ctx.mask);
+ }
+
+
+ game.ctx.global.fillStyle = 'rgb(0, 0, 0)';
+ game.ctx.global.fillRect(0, 0, game.screen_w * game.draw_scale, game.screen_h * game.draw_scale);
+
+ game.ctx.global.save();
+
+ game.ctx.global.scale(game.draw_scale, game.draw_scale);
+
+ game.ctx.global.drawImage(game.ctx.mask.canvas, 0, 0);
+
+ if (game._norun) {
+ game.ctx.global.drawImage(game.img._pause, 0, 0);
+ }
+
+ game.ctx.global.restore();
+ }
+
+ /* Draw a particular section of an image/spritesheet,
+ * without having to do as much math or type the destination size */
+ function sprite_draw(ctx, img, section_w, section_h, section_x, section_y, dest_x, dest_y) {
+ ctx.drawImage(img,
+ Math.round(section_w * section_x), Math.round(section_h * section_y), section_w, section_h,
+ Math.round(dest_x), Math.round(dest_y), section_w, section_h)
+ }
+
+ /* Draw an image over the whole screen lol */
+ function screen_draw(ctx, img) {
+ ctx.drawImage(img, 0, 0);
+ }
+
+ /* Draw an image at a specified position */
+ function image_draw(ctx, img, dest_x, dest_y) {
+ ctx.drawImage(img, Math.round(dest_x), Math.round(dest_y));
+ }
+
+ /* Draw a set of buttons. */
+ function button_draw(ctx, button_data) {
+ for (let button of button_data.buttons) {
+ if (!button.state) button.state = 0;
+ let button_state = button.state;
+ if (button.disabled) button_state = 3;
+ sprite_draw(ctx, button_data.img, button_data.button_w, button_data.button_h, button.id, button_state, button.x, button.y);
+ }
+ }
+
+ /* ---- Save ---- */
+
+ function _save(game, key, data) {
+ let save_data;
+
+ if (!game.hasOwnProperty('save_key')) {
+ throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object");
+ }
+
+ try {
+ save_data = localStorage.getItem(game.save_key) || "{}";
+ } catch (e) {
+ console.error("Saving is disabled; cannot save " + key, e);
+ return;
+ }
+
+ try {
+ save_data = JSON.parse(save_data);
+ } catch (e) {
+ console.error("Cannot parse stored save data (when trying to save " + key + ")", e);
+ return;
+ }
+
+ save_data[key] = JSON.stringify(data);
+
+ try {
+ save_data = JSON.stringify(save_data);
+ } catch (e) {
+ console.error("Failed to stringify save data (while trying to save " + key + ")", e);
+ return;
+ }
+
+ try {
+ localStorage.setItem(game.save_key, save_data);
+ } catch (e) {
+ console.error("Failed to save to localStorage (while saving key " + key + ")", e);
+ return;
+ }
+ }
+
+ function _load(game, key) {
+ let save_data;
+
+ if (!game.hasOwnProperty('save_key')) {
+ throw new Error("In order to enable saving/loading, please specify a save_key property when creating the game object");
+ }
+
+ try {
+ save_data = localStorage.getItem(game.save_key);
+ } catch (e) {
+ console.error("Loading is disabled; cannot load " + key, e);
+ return;
+ }
+
+ try {
+ save_data = JSON.parse(save_data);
+ } catch (e) {
+ console.error("Cannot parse stored save data (when trying to load " + key + ")", e);
+ return;
+ }
+
+ if (!save_data) {
+ return null;
+ }
+
+ return save_data[key];
+ }
+
+ /* ---- Transition stuff ---- */
+
+ let _transition = {
+ is_transitioning: false,
+ timer: 0,
+ color: '#000000',
+ w: 20,
+ h: 14,
+ dir_invert_v: false,
+ dir_invert_h: false,
+ invert_shape: true,
+ mid_long: false,
+ done_func: null,
+ type: _draw_fade_transition,
+ nodraw: false,
+ end_time: 100,
+ }
+
+ function _long_transition(game, type, length, callback) {
+ if (game.transition.is_transitioning) return;
+
+ _draw(game);
+
+ game.transition.invert_shape = false;
+ _internal_start_transition(game, type, length, function() {
+ game.transition.mid_long = true;
+ }, function() {
+ game.transition.invert_shape = true;
+ game.transition.is_transitioning = true;
+ let tdiv = game.transition.dir_invert_v;
+ let tdih = game.transition.dir_invert_h;
+ _internal_start_transition(game, type, length, function() {
+ game.transition.mid_long = false;
+ callback();
+ game.transition.dir_invert_v = tdiv;
+ game.transition.dir_invert_h = tdih;
+ });
+ });
+ }
+
+ function _start_transition(game, type, length, callback, on_done) {
+ if (game.transition.is_transitioning) return;
+ if (!game.transition.nodraw) _draw(game);
+
+ _internal_start_transition(game, type, length, callback, on_done);
+ }
+
+ function _internal_start_transition(game, type, length, callback, on_done) {
+ if (on_done) {
+ game.transition.done_func = on_done;
+ }
+
+ game.transition.timer = 0;
+ game.transition.type = type;
+ game.transition.end_time = length;
+
+ game.ctx.copy.drawImage(game.ctx.draw.canvas, 0, 0);
+
+ callback();
+
+ game.transition.is_transitioning = true;
+ }
+
+ function _finish_transition(game) {
+ game.transition.is_transitioning = false;
+ game.transition.timer = 0;
+
+ if (game.transition.done_func) {
+ setTimeout(function() {
+ game.transition.done_func();
+ game.transition.done_func = null;
+ }, 400);
+ }
+ }
+
+ function _draw_transition(game, ctx) {
+ let frac = game.transition.timer / game.transition.end_time;
+
+ ctx.save();
+ ctx.clearRect(0, 0, game.screen_w, game.screen_h);
+ game.transition.type(game, ctx, game.ctx.copy, game.ctx.draw, frac, game.transition.invert_shape);
+ ctx.restore();
+ }
+
+ function _draw_slide_down_transition(game, ctx, oldc, newc, frac, reverse) {
+ let offset = frac * game.screen_h;
+
+ ctx.drawImage(oldc.canvas, 0, -offset);
+ ctx.drawImage(newc.canvas, 0, game.screen_h - offset);
+ }
+
+ function _draw_slide_up_transition(game, ctx, oldc, newc, frac, reverse) {
+ let offset = frac * game.screen_h;
+
+ ctx.drawImage(oldc.canvas, 0, offset);
+ ctx.drawImage(newc.canvas, 0, - game.screen_h + offset);
+ }
+
+ function _draw_fade_transition(game, ctx, oldc, newc, frac, reverse) {
+ ctx.globalAlpha = 1;
+ ctx.drawImage(oldc.canvas, 0, 0);
+ ctx.globalAlpha = frac;
+ ctx.drawImage(newc.canvas, 0, 0);
+ }
+
+ /* ---- former contents of ready.js ---- */
+
+ // Due to Timo Huovinen
+ // https://stackoverflow.com/questions/799981/document-ready-equivalent-without-jquery
+ var ready = (function(){
+
+ var readyList,
+ DOMContentLoaded,
+ class2type = {};
+ class2type["[object Boolean]"] = "boolean";
+ class2type["[object Number]"] = "number";
+ class2type["[object String]"] = "string";
+ class2type["[object Function]"] = "function";
+ class2type["[object Array]"] = "array";
+ class2type["[object Date]"] = "date";
+ class2type["[object RegExp]"] = "regexp";
+ class2type["[object Object]"] = "object";
+
+ var ReadyObj = {
+ // Is the DOM ready to be used? Set to true once it occurs.
+ isReady: false,
+ // A counter to track how many items to wait for before
+ // the ready event fires. See #6781
+ readyWait: 1,
+ // Hold (or release) the ready event
+ holdReady: function( hold ) {
+ if ( hold ) {
+ ReadyObj.readyWait++;
+ } else {
+ ReadyObj.ready( true );
+ }
+ },
+ // Handle when the DOM is ready
+ ready: function( wait ) {
+ // Either a released hold or an DOMready/load event and not yet ready
+ if ( (wait === true && !--ReadyObj.readyWait) || (wait !== true && !ReadyObj.isReady) ) {
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( !document.body ) {
+ return setTimeout( ReadyObj.ready, 1 );
+ }
+
+ // Remember that the DOM is ready
+ ReadyObj.isReady = true;
+ // If a normal DOM Ready event fired, decrement, and wait if need be
+ if ( wait !== true && --ReadyObj.readyWait > 0 ) {
+ return;
+ }
+ // If there are functions bound, to execute
+ readyList.resolveWith( document, [ ReadyObj ] );
+
+ // Trigger any bound ready events
+ //if ( ReadyObj.fn.trigger ) {
+ // ReadyObj( document ).trigger( "ready" ).unbind( "ready" );
+ //}
+ }
+ },
+ bindReady: function() {
+ if ( readyList ) {
+ return;
+ }
+ readyList = ReadyObj._Deferred();
+
+ // Catch cases where $(document).ready() is called after the
+ // browser event has already occurred.
+ if ( document.readyState === "complete" ) {
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
+ return setTimeout( ReadyObj.ready, 1 );
+ }
+
+ // Mozilla, Opera and webkit nightlies currently support this event
+ if ( document.addEventListener ) {
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+ // A fallback to window.onload, that will always work
+ window.addEventListener( "load", ReadyObj.ready, false );
+
+ // If IE event model is used
+ } else if ( document.attachEvent ) {
+ // ensure firing before onload,
+ // maybe late but safe also for iframes
+ document.attachEvent( "onreadystatechange", DOMContentLoaded );
+
+ // A fallback to window.onload, that will always work
+ window.attachEvent( "onload", ReadyObj.ready );
+
+ // If IE and not a frame
+ // continually check to see if the document is ready
+ var toplevel = false;
+
+ try {
+ toplevel = window.frameElement == null;
+ } catch(e) {}
+
+ if ( document.documentElement.doScroll && toplevel ) {
+ doScrollCheck();
+ }
+ }
+ },
+ _Deferred: function() {
+ var // callbacks list
+ callbacks = [],
+ // stored [ context , args ]
+ fired,
+ // to avoid firing when already doing so
+ firing,
+ // flag to know if the deferred has been cancelled
+ cancelled,
+ // the deferred itself
+ deferred = {
+
+ // done( f1, f2, ...)
+ done: function() {
+ if ( !cancelled ) {
+ var args = arguments,
+ i,
+ length,
+ elem,
+ type,
+ _fired;
+ if ( fired ) {
+ _fired = fired;
+ fired = 0;
+ }
+ for ( i = 0, length = args.length; i < length; i++ ) {
+ elem = args[ i ];
+ type = ReadyObj.type( elem );
+ if ( type === "array" ) {
+ deferred.done.apply( deferred, elem );
+ } else if ( type === "function" ) {
+ callbacks.push( elem );
+ }
+ }
+ if ( _fired ) {
+ deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
+ }
+ }
+ return this;
+ },
+
+ // resolve with given context and args
+ resolveWith: function( context, args ) {
+ if ( !cancelled && !fired && !firing ) {
+ // make sure args are available (#8421)
+ args = args || [];
+ firing = 1;
+ try {
+ while( callbacks[ 0 ] ) {
+ callbacks.shift().apply( context, args );//shifts a callback, and applies it to document
+ }
+ }
+ finally {
+ fired = [ context, args ];
+ firing = 0;
+ }
+ }
+ return this;
+ },
+
+ // resolve with this as context and given arguments
+ resolve: function() {
+ deferred.resolveWith( this, arguments );
+ return this;
+ },
+
+ // Has this deferred been resolved?
+ isResolved: function() {
+ return !!( firing || fired );
+ },
+
+ // Cancel
+ cancel: function() {
+ cancelled = 1;
+ callbacks = [];
+ return this;
+ }
+ };
+
+ return deferred;
+ },
+ type: function( obj ) {
+ return obj == null ?
+ String( obj ) :
+ class2type[ Object.prototype.toString.call(obj) ] || "object";
+ }
+ }
+ // The DOM ready check for Internet Explorer
+ function doScrollCheck() {
+ if ( ReadyObj.isReady ) {
+ return;
+ }
+
+ try {
+ // If IE is used, use the trick by Diego Perini
+ // http://javascript.nwbox.com/IEContentLoaded/
+ document.documentElement.doScroll("left");
+ } catch(e) {
+ setTimeout( doScrollCheck, 1 );
+ return;
+ }
+
+ // and execute any waiting functions
+ ReadyObj.ready();
+ }
+ // Cleanup functions for the document ready method
+ if ( document.addEventListener ) {
+ DOMContentLoaded = function() {
+ document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+ ReadyObj.ready();
+ };
+
+ } else if ( document.attachEvent ) {
+ DOMContentLoaded = function() {
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( document.readyState === "complete" ) {
+ document.detachEvent( "onreadystatechange", DOMContentLoaded );
+ ReadyObj.ready();
+ }
+ };
+ }
+ function ready( fn ) {
+ // Attach the listeners
+ ReadyObj.bindReady();
+
+ var type = ReadyObj.type( fn );
+
+ // Add the callback
+ readyList.done( fn );//readyList is result of _Deferred()
+ }
+ return ready;
+ })();
+
+ /* ---- direction objects ---- */
+ const _dir_up = { x: 0, y: -1, index: 0, rotate: 0, horizontal: false, vertical: true };
+ const _dir_right = { x: 1, y: 0, index: 1, rotate: Math.PI/2, horizontal: true, vertical: false };
+ const _dir_down = { x: 0, y: 1, index: 2, rotate: Math.PI, horizontal: false, vertical: true };
+ const _dir_left = { x: -1, y: 0, index: 3, rotate: 3*Math.PI/2, horizontal: true, vertical: false };
+ _dir_up.opposite = _dir_down;
+ _dir_down.opposite = _dir_up;
+ _dir_right.opposite = _dir_left;
+ _dir_left.opposite = _dir_right;
+
+ return {
+ create_game: create_game,
+
+ ready: ready,
+ mod: mod,
+ sgn: sgn,
+ as_hex: as_hex,
+ copy_list: copy_list,
+ copy_flat_objlist: copy_flat_objlist,
+
+ sprite_draw: sprite_draw,
+ screen_draw: screen_draw,
+ image_draw: image_draw,
+
+ buttons: {
+ create: create_buttons,
+ update: update_buttons,
+ click: click_button,
+ draw: button_draw,
+ },
+
+ dir: {
+ up: _dir_up,
+ down: _dir_down,
+ left: _dir_left,
+ right: _dir_right,
+ },
+
+ phi: (1 + Math.sqrt(5)) / 2,
+
+ transition: {
+ type: {
+ fade: _draw_fade_transition,
+ slide_up: _draw_slide_up_transition,
+ slide_down: _draw_slide_down_transition,
+ },
+ },
+ }
+})();