diff options
author | Tor Andersson <tor@ccxvii.net> | 2021-05-01 00:48:44 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2022-11-16 19:08:16 +0100 |
commit | 573b5bb43f409e6270fd243ff80a34ba2c727654 (patch) | |
tree | c78e49093b957fb073a4a7c26a3709e7b538d5ca /ui.js | |
download | julius-caesar-573b5bb43f409e6270fd243ff80a34ba2c727654.tar.gz |
caesar: Import Julius Caesar.
Diffstat (limited to 'ui.js')
-rw-r--r-- | ui.js | 776 |
1 files changed, 776 insertions, 0 deletions
@@ -0,0 +1,776 @@ +"use strict"; + +const DEAD = "Dead"; +const LEVY = "Levy"; + +let label_style = window.localStorage['julius-caesar/label-style'] || 'columbia'; +let label_layout = window.localStorage['julius-caesar/label-layout'] || 'spread'; + +function toggle_blocks() { + document.getElementById("blocks").classList.toggle("hide_blocks"); +} + +function set_simple_labels() { + label_style = 'simple'; + document.querySelector(".blocks").classList.remove("columbia-labels"); + document.querySelector(".battle").classList.remove("columbia-labels"); + document.querySelector(".blocks").classList.add("simple-labels"); + document.querySelector(".battle").classList.add("simple-labels"); + window.localStorage['julius-caesar/label-style'] = label_style; + update_map(); +} + +function set_columbia_labels() { + label_style = 'columbia'; + document.querySelector(".blocks").classList.remove("simple-labels"); + document.querySelector(".battle").classList.remove("simple-labels"); + document.querySelector(".blocks").classList.add("columbia-labels"); + document.querySelector(".battle").classList.add("columbia-labels"); + window.localStorage['julius-caesar/label-style'] = label_style; + update_map(); +} + +function set_spread_layout() { + label_layout = 'spread'; + window.localStorage['julius-caesar/label-layout'] = label_layout; + update_map(); +} + +function set_stack_layout() { + label_layout = 'stack'; + window.localStorage['julius-caesar/label-layout'] = label_layout; + update_map(); +} + +// Levy and hit animations for 'simple' blocks. +const step_down_animation = [ + { transform: 'translateY(0px)' }, + { transform: 'translateY(10px)' }, + { transform: 'translateY(0px)' }, +]; +const step_up_animation = [ + { transform: 'translateY(0px)' }, + { transform: 'translateY(-10px)' }, + { transform: 'translateY(0px)' }, +]; + +let game = null; + +let ui = { + spaces: {}, + known: {}, + secret: { + Caesar: { offmap: [] }, + Pompeius: { offmap: [] }, + Cleopatra: { offmap: [] }, + }, + seen: new Set(), + present: new Set(), + battle_block: {}, + battle_menu: {}, + map_steps: {}, + map_location: {}, + battle_steps: {}, + cards: {}, +}; + +const STEPS = [ 0, "I", "II", "III", "IIII" ]; + +function block_description(b) { + let s = ui.map_steps[b] || ui.battle_steps[b]; + let c = BLOCKS[b].initiative + BLOCKS[b].firepower; + return BLOCKS[b].name + " " + STEPS[s] + "-" + c; +} + +function block_name(b) { + return BLOCKS[b].name; +} + +function on_focus_space(evt) { + document.getElementById("status").textContent = evt.target.space; +} + +function on_blur_space(evt) { + document.getElementById("status").textContent = ""; +} + +function on_focus_block(evt) { + document.getElementById("status").textContent = block_description(evt.target.block); +} + +function on_blur_block(evt) { + document.getElementById("status").textContent = ""; +} + +function on_focus_battle_block(evt) { + let b = evt.target.block; + let msg = block_name(b); + if (!evt.target.classList.contains("known")) + document.getElementById("status").textContent = "Reserves"; + + if (game.actions && game.actions.battle_fire && game.actions.battle_fire.includes(b)) + msg = "Fire with " + msg; + else if (game.actions && game.actions.battle_retreat && game.actions.battle_retreat.includes(b)) + msg = "Retreat with " + msg; + else if (game.actions && game.actions.battle_hit && game.actions.battle_hit.includes(b)) + msg = "Take hit on " + msg; + + document.getElementById("status").textContent = msg; +} + +function on_blur_battle_block(evt) { + document.getElementById("status").textContent = ""; +} + +function on_focus_battle_fire(evt) { + document.getElementById("status").textContent = + "Fire with " + block_name(evt.target.block); +} +function on_focus_battle_retreat(evt) { + document.getElementById("status").textContent = + "Retreat with " + block_name(evt.target.block); +} +function on_focus_battle_pass(evt) { + document.getElementById("status").textContent = + "Pass with " + block_name(evt.target.block); +} +function on_focus_battle_hit(evt) { + document.getElementById("status").textContent = + "Take hit on " + block_name(evt.target.block); +} +function on_blur_battle_button(evt) { + document.getElementById("status").textContent = ""; +} + +function build_map() { + // These must match up with the sizes in play.html + const city_size = 60+10; + const sea_size = 70+10; + + for (let s in SPACES) { + let space = SPACES[s]; + let element = document.createElement("div"); + element.classList.add("space"); + let size = (space.type == 'sea') ? sea_size : city_size; + if (space.type == "sea") + element.classList.add("sea"); + else + element.classList.add("city"); + element.setAttribute("draggable", "false"); + element.addEventListener("mouseenter", on_focus_space); + element.addEventListener("mouseleave", on_blur_space); + element.addEventListener("click", select_space); + element.style.left = (space.x - size/2) + "px"; + element.style.top = (space.y - size/2) + "px"; + if (space.type != 'pool') + document.getElementById("spaces").appendChild(element); + element.space = s; + ui.spaces[s] = element; + + ui.secret[CLEOPATRA][s] = []; + ui.secret[CAESAR][s] = []; + ui.secret[POMPEIUS][s] = []; + } + + function build_known_block(b, block, color) { + let element = document.createElement("div"); + element.classList.add("block"); + element.classList.add("known"); + element.classList.add(color); + element.classList.add("block_"+block.label); + element.addEventListener("mouseenter", on_focus_block); + element.addEventListener("mouseleave", on_blur_block); + element.addEventListener("click", select_block); + document.getElementById("known_blocks").appendChild(element); + element.style.visibility = 'hidden'; + element.block = b; + ui.known[b] = element; + } + + function build_secret_block(b, block, color) { + let element = document.createElement("div"); + element.secret_index = ui.secret[color].offmap.length; + element.classList.add("block"); + element.classList.add("secret"); + element.classList.add(color); + element.addEventListener("click", select_secret_block); + document.getElementById("secret_blocks").appendChild(element); + element.style.visibility = 'hidden'; + element.owner = BLOCKS[b].owner; + ui.secret[color].offmap.unshift(element); + } + + function build_battle_button(menu, b, c, click, enter, img_src) { + let img = new Image(); + img.draggable = false; + img.classList.add("action"); + img.classList.add(c); + img.setAttribute("src", img_src); + img.addEventListener("click", click); + img.addEventListener("mouseenter", enter); + img.addEventListener("mouseleave", on_blur_battle_button); + img.block = b; + menu.appendChild(img); + } + + function build_battle_block(b, block, color) { + let element = document.createElement("div"); + element.classList.add("block"); + element.classList.add("known"); + element.classList.add(color); + element.classList.add("block_"+block.label); + element.addEventListener("mouseenter", on_focus_battle_block); + element.addEventListener("mouseleave", on_blur_battle_block); + element.addEventListener("click", select_block); + element.block = b; + ui.battle_block[b] = element; + + let action_list = document.createElement("div"); + action_list.classList.add("battle_menu_list"); + action_list.appendChild(element); + build_battle_button(action_list, b, "hit", + select_battle_hit, on_focus_battle_hit, + "/images/cross-mark.svg"); + build_battle_button(action_list, b, "fire", + select_battle_fire, on_focus_battle_fire, + "/images/pointy-sword.svg"); + build_battle_button(action_list, b, "retreat", + select_battle_retreat, on_focus_battle_retreat, + "/images/flying-flag.svg"); + build_battle_button(action_list, b, "pass", + select_battle_pass, on_focus_battle_pass, + "/images/sands-of-time.svg"); + + let menu = document.createElement("div"); + menu.classList.add("battle_menu"); + menu.appendChild(element); + menu.appendChild(action_list); + ui.battle_menu[b] = menu; + } + + for (let b in BLOCKS) { + let block = BLOCKS[b]; + let color = (block.name == "Cleopatra" ? "Cleopatra" : block.owner); + build_known_block(b, block, color); + build_secret_block(b, block, color); + build_battle_block(b, block, color); + } + + for (let c = 1; c <= 27; ++c) { + ui.cards[c] = document.getElementById("card+" + c); + } +} + +function update_steps(memo, block, steps, element, animate) { + let old_steps = memo[block] || steps; + memo[block] = steps; + + if (label_style == 'simple' && steps != old_steps && animate) { + let options = { duration: 700, easing: 'ease', iterations: Math.abs(steps-old_steps) } + if (steps < old_steps) + element.animate(step_down_animation, options); + if (steps > old_steps) + element.animate(step_up_animation, options); + } + + element.classList.remove("r0"); + element.classList.remove("r1"); + element.classList.remove("r2"); + element.classList.remove("r3"); + element.classList.add("r"+(BLOCKS[block].steps - steps)); +} + +function layout_blocks(location, secret, known) { + if (label_layout == 'spread' || (location == LEVY || location == DEAD)) + layout_blocks_spread(location, secret, known); + else + layout_blocks_stacked(location, secret, known); +} + +function layout_blocks_spread(location, secret, known) { + let wrap = SPACES[location].wrap; + let s = secret.length; + let k = known.length; + let n = s + k; + let row, rows = []; + let i = 0; + + function new_line() { + rows.push(row = []); + i = 0; + } + + new_line(); + + while (secret.length > 0) { + if (i == wrap) + new_line(); + row.push(secret.shift()); + ++i; + } + + // Break early if secret and known fit in exactly two rows, and more than three blocks total + if (s > 0 && s <= wrap && k > 0 && k <= wrap && n > 3) + new_line(); + + while (known.length > 0) { + if (i == wrap) + new_line(); + row.push(known.shift()); + ++i; + } + + if (SPACES[location].layout_minor > 0.5) + rows.reverse(); + + for (let j = 0; j < rows.length; ++j) + for (i = 0; i < rows[j].length; ++i) + position_block_spread(location, j, rows.length, i, rows[j].length, rows[j][i]); +} + +function position_block_spread(location, row, n_rows, col, n_cols, element) { + let space = SPACES[location]; + let block_size = (label_style == 'columbia') ? 56+6 : 48+4; + let padding = (location == LEVY || location == DEAD) ? 6 : 3; + let offset = block_size + padding; + let row_size = (n_rows-1) * offset; + let col_size = (n_cols-1) * offset; + let x = space.x - block_size/2; + let y = space.y - block_size/2; + + if (space.layout_axis == 'X') { + x -= col_size * space.layout_major; + y -= row_size * space.layout_minor; + x += col * offset; + y += row * offset; + } else { + y -= col_size * space.layout_major; + x -= row_size * space.layout_minor; + y += col * offset; + x += row * offset; + } + + element.style.left = (x|0)+"px"; + element.style.top = (y|0)+"px"; +} + +function layout_blocks_stacked(location, secret, known) { + let s = secret.length; + let k = known.length; + let n = s + k; + let i = 0; + while (secret.length > 0) + position_block_stacked(location, i++, n, secret.shift()); + while (known.length > 0) + position_block_stacked(location, i++, n, known.shift()); +} + +function position_block_stacked(location, i, n, element) { + let space = SPACES[location]; + let block_size = (label_style == 'columbia') ? 56+6 : 48+4; + let x = space.x - block_size/2 - (n-1) * 9 + i * 18; + let y = space.y - block_size/2 - (n-1) * 9 + i * 18; + element.style.left = x+"px"; + element.style.top = y+"px"; +} + +function sort_secret(a,b) { + return a.secret_index - b.secret_index; +} + +function update_map() { + let overflow = { Caesar: [], Pompeius: [], Cleopatra: [] }; + let layout = {}; + + for (let s in SPACES) + layout[s] = { secret: [], known: [] }; + + // Move secret blocks to overflow queue if there are too many in a location + for (let s in SPACES) { + for (let color of [CAESAR, POMPEIUS, CLEOPATRA]) { + let max = 0; + if (game.secret[color] && game.secret[color][s]) + max = game.secret[color][s][0]; + while (ui.secret[color][s].length > max) + overflow[color].push(ui.secret[color][s].pop()); + } + } + + // Add secret blocks if there are too few in a location + for (let s in SPACES) { + for (let color of [CAESAR, POMPEIUS, CLEOPATRA]) { + let max = 0; + if (game.secret[color] && game.secret[color][s]) + max = game.secret[color][s][0]; + while (ui.secret[color][s].length < max) { + if (overflow[color].length > 0) { + ui.secret[color][s].push(overflow[color].pop()); + } else { + let element = ui.secret[color].offmap.pop(); + element.style.visibility = 'visible'; + ui.secret[color][s].push(element); + } + } + } + } + + // Remove any blocks left in the overflow queue + for (let color of [CAESAR, POMPEIUS, CLEOPATRA]) { + while (overflow[color].length > 0) { + let element = overflow[color].pop(); + element.style.visibility = 'hidden'; + // Prevent move animation when blocks are revived. + element.style.left = null; + element.style.top = null; + ui.secret[color].offmap.push(element); + } + } + + // Hide formerly known blocks + for (let b in BLOCKS) { + if (!(b in game.known)) { + ui.known[b].style.visibility = 'hidden'; + // Prevent move animation when blocks are revived. + ui.known[b].style.left = null; + ui.known[b].style.top = null; + } + } + + // Add secret blocks to layout + for (let location in SPACES) { + for (let color of [CAESAR, POMPEIUS, CLEOPATRA]) { + if (location != LEVY) { + let i = 0, n = 0, m = 0; + if (game.secret[color] && game.secret[color][location]) { + n = game.secret[color][location][0]; + m = game.secret[color][location][1]; + } + // Preserve block stacking order, but lets the user track block identities... + if (label_layout == 'stack') + ui.secret[color][location].sort((a,b) => a.secret_index - b.secret_index); + for (let element of ui.secret[color][location]) { + if (i++ < n - m) + element.classList.remove('moved'); + else + element.classList.add('moved'); + element.style.visibility = 'visible'; + + layout[location].secret.push(element); + } + } + } + } + + // Add known blocks to layout + for (let block in game.known) { + let location = game.known[block][0]; + let steps = game.known[block][1]; + let moved = game.known[block][2]; + let element = ui.known[block]; + + element.style.visibility = 'visible'; + + layout[location].known.push(element); + + let old_location = ui.map_location[block]; + update_steps(ui.map_steps, block, steps, element, location == old_location); + ui.map_location[block] = location; + + if (moved || (location == DEAD && BLOCKS[block].type != 'leader')) + element.classList.add("moved"); + else + element.classList.remove("moved"); + + ui.seen.add(block); + } + + // Layout blocks on map + for (let location in SPACES) + layout_blocks(location, layout[location].secret, layout[location].known); + + // Mark selections and highlights + + for (let where in SPACES) { + if (ui.spaces[where]) { + ui.spaces[where].classList.remove('highlight'); + ui.spaces[where].classList.remove('where'); + } + } + if (game.actions && game.actions.space) + for (let where of game.actions.space) + ui.spaces[where].classList.add('highlight'); + if (game.where) + ui.spaces[game.where].classList.add('where'); + + for (let b in BLOCKS) { + ui.known[b].classList.remove('highlight'); + ui.known[b].classList.remove('selected'); + } + if (!game.battle) { + if (game.actions && game.actions.block) + for (let b of game.actions.block) + ui.known[b].classList.add('highlight'); + if (game.who) + ui.known[game.who].classList.add('selected'); + } + + for (let o in ui.secret) { + for (let s in ui.secret[o]) { + if (game.actions && game.actions.secret && game.actions.secret.includes(s)) { + for (let e of ui.secret[o][s]) + e.classList.add("highlight"); + } else { + for (let e of ui.secret[o][s]) + e.classList.remove("highlight"); + } + } + } +} + +function update_battle(player) { + function fill_cell(name, list, reserve) { + let cell = window[name]; + + ui.present.clear(); + + for (let [block, steps, moved] of list) { + ui.seen.add(block); + ui.present.add(block); + + if (block == game.who) + ui.battle_menu[block].classList.add("selected"); + else + ui.battle_menu[block].classList.remove("selected"); + + ui.battle_block[block].classList.remove("highlight"); + ui.battle_menu[block].classList.remove('hit'); + ui.battle_menu[block].classList.remove('fire'); + ui.battle_menu[block].classList.remove('retreat'); + ui.battle_menu[block].classList.remove('pass'); + + if (game.actions && game.actions.block && game.actions.block.includes(block)) + ui.battle_block[block].classList.add("highlight"); + if (game.actions && game.actions.battle_fire && game.actions.battle_fire.includes(block)) + ui.battle_menu[block].classList.add('fire'); + if (game.actions && game.actions.battle_retreat && game.actions.battle_retreat.includes(block)) + ui.battle_menu[block].classList.add('retreat'); + if (game.actions && game.actions.battle_pass && game.actions.battle_pass.includes(block)) + ui.battle_menu[block].classList.add('pass'); + if (game.actions && game.actions.battle_hit && game.actions.battle_hit.includes(block)) + ui.battle_menu[block].classList.add('hit'); + + update_steps(ui.battle_steps, block, steps, ui.battle_block[block], true); + if (reserve) + ui.battle_block[block].classList.add("secret"); + else + ui.battle_block[block].classList.remove("secret"); + if (moved || reserve) + ui.battle_block[block].classList.add("moved"); + else + ui.battle_block[block].classList.remove("moved"); + if (reserve) + ui.battle_block[block].classList.remove("known"); + else + ui.battle_block[block].classList.add("known"); + } + + for (let b in BLOCKS) { + if (ui.present.has(b)) { + if (!cell.contains(ui.battle_menu[b])) + cell.appendChild(ui.battle_menu[b]); + } else { + if (cell.contains(ui.battle_menu[b])) + cell.removeChild(ui.battle_menu[b]); + } + } + } + + if (player == CAESAR) { + fill_cell("FR", game.battle.CR, true); + fill_cell("FA", game.battle.CA, false); + fill_cell("FB", game.battle.CB, false); + fill_cell("FC", game.battle.CC, false); + fill_cell("FD", game.battle.CD, false); + fill_cell("EA", game.battle.PA, false); + fill_cell("EB", game.battle.PB, false); + fill_cell("EC", game.battle.PC, false); + fill_cell("ED", game.battle.PD, false); + fill_cell("ER", game.battle.PR, true); + } else { + fill_cell("ER", game.battle.CR, true); + fill_cell("EA", game.battle.CA, false); + fill_cell("EB", game.battle.CB, false); + fill_cell("EC", game.battle.CC, false); + fill_cell("ED", game.battle.CD, false); + fill_cell("FA", game.battle.PA, false); + fill_cell("FB", game.battle.PB, false); + fill_cell("FC", game.battle.PC, false); + fill_cell("FD", game.battle.PD, false); + fill_cell("FR", game.battle.PR, true); + } +} + +function update_card_display(element, card, prior_card) { + if (!card && !prior_card) { + element.className = "small_card card_back"; + } else if (prior_card) { + element.className = "small_card prior card_" + CARDS[prior_card].image; + } else { + element.className = "small_card card_" + CARDS[card].image; + } +} + +function update_cards() { + update_card_display(document.getElementById("caesar_card"), game.c_card, game.prior_c_card); + update_card_display(document.getElementById("pompeius_card"), game.p_card, game.prior_p_card); + + for (let c = 1; c <= 27; ++c) { + let element = ui.cards[c]; + if (game.hand.includes(c)) { + element.classList.add("show"); + if (game.actions && game.actions.card) { + if (game.actions.card.includes(c)) { + element.classList.add("enabled"); + element.classList.remove("disabled"); + } else { + element.classList.remove("enabled"); + element.classList.add("disabled"); + } + } else { + element.classList.remove("enabled"); + element.classList.remove("disabled"); + } + } else { + element.classList.remove("show"); + } + } +} + +function on_update(state, player) { + game = state; + + document.getElementById("turn").className = "year_" + game.year; + document.getElementById("caesar_vp").textContent = game.c_vp + " VP"; + document.getElementById("pompeius_vp").textContent = game.p_vp + " VP"; + + show_action_button("#undo_button", "undo"); + show_action_button("#surprise_button", "surprise"); + show_action_button("#pass_button", "pass", true); + + ui.seen.clear(); + + update_cards(); + update_map(); + + for (let b in BLOCKS) + if (!ui.seen.has(b)) + ui.map_steps[b] = 0; + + ui.seen.clear(); + + if (game.battle) { + document.querySelector(".battle_header").textContent = game.battle.title; + document.querySelector(".battle_message").textContent = game.battle.flash; + document.querySelector(".battle").classList.add("show"); + update_battle(player); + } else { + document.querySelector(".battle").classList.remove("show"); + } + + for (let b in BLOCKS) + if (!ui.seen.has(b)) + ui.battle_steps[b] = 0; + + if (game.spaceList) { + if (game.actionList.includes("secret")) { + for (let o in ui.secret) { + for (let s in ui.secret[o]) { + if (game.spaceList.includes(s)) { + for (let e of ui.secret[o][s]) + e.classList.add("highlight"); + } else { + for (let e of ui.secret[o][s]) + e.classList.remove("highlight"); + } + } + } + for (let s in SPACES) + if (game.spaceList.includes(s)) + ui.spaces[s].classList.remove("highlight"); + } else { + for (let o in ui.secret) + for (let s in ui.secret[o]) + for (let e of ui.secret[o][s]) + e.classList.remove("highlight"); + for (let s in SPACES) { + if (game.spaceList.includes(s)) + ui.spaces[s].classList.add("highlight"); + else + ui.spaces[s].classList.remove("highlight"); + } + } + } + + if (game.blockList) { + for (let b in BLOCKS) { + if (game.blockList.includes(b)) + ui.known[b].classList.add("highlight"); + else + ui.known[b].classList.remove("highlight"); + if (b == game.who) + ui.known[b].classList.add("selected"); + else + ui.known[b].classList.remove("selected"); + } + } +} + +function select_card(c) { + send_action('card', c); +} + +function select_space(evt) { + send_action('space', evt.target.space); +} + +function select_block(evt) { + send_action('block', evt.target.block); +} + +function select_secret_block(evt) { + let element = evt.target; + let owner = null; + let where = null; + for (let o in ui.secret) { + for (let s in ui.secret[o]) { + if (ui.secret[o][s].includes(element)) { + owner = o; + where = s; + break; + } + } + } + if (game.actions && game.actions.secret && game.actions.secret.includes(where)) { + socket.emit('action', 'secret', [where, owner]); + game.actions = null; + } +} + +function select_surprise() { send_action('surprise'); } +function select_pass() { send_action('pass'); } +function select_undo() { send_action('undo'); } + +function select_battle_hit(evt) { send_action('battle_hit', evt.target.block); } +function select_battle_fire(evt) { send_action('battle_fire', evt.target.block); } +function select_battle_retreat(evt) { send_action('battle_retreat', evt.target.block); } +function select_battle_pass(evt) { send_action('battle_pass', evt.target.block); } + +build_map(); + +document.querySelector(".blocks").classList.add(label_style+'-labels'); +document.querySelector(".battle").classList.add(label_style+'-labels'); + +drag_element_with_mouse(".battle", ".battle_header"); +scroll_with_middle_mouse(".grid_center"); + +init_client([ "Caesar", "Pompeius" ]); |