From: cassowarii <2374677+cassowarii@users.noreply.github.com> Date: Sun, 19 Apr 2026 06:34:15 +0000 (-0700) Subject: initial commit X-Git-Url: https://www.git.cassowary.me/gitweb.cgi?a=commitdiff_plain;h=bbe82af8d3fc2929cab2a35e09250ed3921b2667;p=ld59.git initial commit --- bbe82af8d3fc2929cab2a35e09250ed3921b2667 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b79e268 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.xcf +*.swp +*.swo +sfx_raw/* +ase/* +packaging.zip +packaging/* +*-sheet.png diff --git a/clicktostart.png b/clicktostart.png new file mode 100644 index 0000000..eb5a61f Binary files /dev/null and b/clicktostart.png differ diff --git a/cover.png b/cover.png new file mode 100644 index 0000000..5026f84 Binary files /dev/null and b/cover.png differ diff --git a/game.js b/game.js new file mode 100644 index 0000000..d22e86e --- /dev/null +++ b/game.js @@ -0,0 +1,935 @@ +"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; + } + } + } + } +} diff --git a/img/connector.png b/img/connector.png new file mode 100644 index 0000000..42e2bb0 Binary files /dev/null and b/img/connector.png differ diff --git a/img/gameover.png b/img/gameover.png new file mode 100644 index 0000000..bbb2c2c Binary files /dev/null and b/img/gameover.png differ diff --git a/img/lightning.png b/img/lightning.png new file mode 100644 index 0000000..2d46b0a Binary files /dev/null and b/img/lightning.png differ diff --git a/img/lightningbutton.png b/img/lightningbutton.png new file mode 100644 index 0000000..69a61f8 Binary files /dev/null and b/img/lightningbutton.png differ diff --git a/img/lightningnumbers.png b/img/lightningnumbers.png new file mode 100644 index 0000000..b328611 Binary files /dev/null and b/img/lightningnumbers.png differ diff --git a/img/playbutton.png b/img/playbutton.png new file mode 100644 index 0000000..a5cbc3a Binary files /dev/null and b/img/playbutton.png differ diff --git a/img/replaybutton.png b/img/replaybutton.png new file mode 100644 index 0000000..4c60293 Binary files /dev/null and b/img/replaybutton.png differ diff --git a/img/scorenumbers.png b/img/scorenumbers.png new file mode 100644 index 0000000..4054f59 Binary files /dev/null and b/img/scorenumbers.png differ diff --git a/img/selector.png b/img/selector.png new file mode 100644 index 0000000..1cfc563 Binary files /dev/null and b/img/selector.png differ diff --git a/img/text.png b/img/text.png new file mode 100644 index 0000000..ae65437 Binary files /dev/null and b/img/text.png differ diff --git a/img/tiles.png b/img/tiles.png new file mode 100644 index 0000000..9bf36b9 Binary files /dev/null and b/img/tiles.png differ diff --git a/img/timebar_inside.png b/img/timebar_inside.png new file mode 100644 index 0000000..ed31043 Binary files /dev/null and b/img/timebar_inside.png differ diff --git a/img/timebar_outside.png b/img/timebar_outside.png new file mode 100644 index 0000000..c93bdc3 Binary files /dev/null and b/img/timebar_outside.png differ diff --git a/img/title.png b/img/title.png new file mode 100644 index 0000000..50e4487 Binary files /dev/null and b/img/title.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..9d80167 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + suit yourself + + + + + + + + + + + diff --git a/loading.png b/loading.png new file mode 100644 index 0000000..871c7b4 Binary files /dev/null and b/loading.png differ diff --git a/music/suitgamesong.mp3 b/music/suitgamesong.mp3 new file mode 100644 index 0000000..df90da8 Binary files /dev/null and b/music/suitgamesong.mp3 differ diff --git a/pause.png b/pause.png new file mode 100644 index 0000000..7479e89 Binary files /dev/null and b/pause.png differ diff --git a/progressbar.png b/progressbar.png new file mode 100644 index 0000000..97f8900 Binary files /dev/null and b/progressbar.png differ diff --git a/saveerror.png b/saveerror.png new file mode 100644 index 0000000..fd1f8e7 Binary files /dev/null and b/saveerror.png differ diff --git a/sfx/bap.wav b/sfx/bap.wav new file mode 100644 index 0000000..349c45f Binary files /dev/null and b/sfx/bap.wav differ diff --git a/sfx/gameover.mp3 b/sfx/gameover.mp3 new file mode 100644 index 0000000..047cf2b Binary files /dev/null and b/sfx/gameover.mp3 differ diff --git a/sfx/lightning.wav b/sfx/lightning.wav new file mode 100644 index 0000000..3a3e3d3 Binary files /dev/null and b/sfx/lightning.wav differ diff --git a/sfx/ok.wav b/sfx/ok.wav new file mode 100644 index 0000000..44c0008 Binary files /dev/null and b/sfx/ok.wav differ diff --git a/sfx/thud.wav b/sfx/thud.wav new file mode 100644 index 0000000..b67e836 Binary files /dev/null and b/sfx/thud.wav differ diff --git a/zucchinibread.js b/zucchinibread.js new file mode 100644 index 0000000..f46ddc6 --- /dev/null +++ b/zucchinibread.js @@ -0,0 +1,1349 @@ +"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, + }, + }, + } +})();