"use strict" /* global data, view, send_action, action_button, player */ const dice_text_map = { "1-3": "1/2/3", "2-4": "2/3/4", "3-5": "3/4/5", "4-6": "4/5/6", "(1-3)": "(1/2/3)", "(2-4)": "(2/3/4)", "(3-5)": "(3/4/5)", "(4-6)": "(4/5/6)", } const wing_name = [ "red", "pink", "blue", "dkblue" ] const side_color = [ "red", "red", "blue", "blue" ] const reactions = [ "Screen", "Counterattack", "Absorb" ] let ui = { main: document.querySelector("main"), role_panel: [ document.getElementById("role_First"), document.getElementById("role_Second") ], role_name: [ document.getElementById("role1"), document.getElementById("role2") ], role_stat: [ document.getElementById("stat1"), document.getElementById("stat2") ], name: [ document.getElementById("name1"), document.getElementById("name2") ], front: [ document.getElementById("front1"), document.getElementById("front2") ], morale: [ document.getElementById("morale1"), document.getElementById("morale2") ], pool: [ document.getElementById("pool1"), document.getElementById("pool2") ], reserve: [ document.getElementById("reserve1"), document.getElementById("reserve2") ], tooltip: document.getElementById("tooltip"), cards: {}, cards_tip: {}, slots: {}, slot_sticks: {}, slot_shift: {}, slot_cubes: {}, slot_dice: {}, dice: [], sticks: [], hit_sticks: [], mcubes: [], ccubes: [], } let animation_registry = [] function register_animation(e, duration) { animation_registry.push(e) e.my_duration = duration } function remember_position(e) { if (e.parentElement) { let rect = e.getBoundingClientRect() let prect = e.parentElement.getBoundingClientRect() e.my_parent = e.parentElement e.my_x = rect.x e.my_y = rect.y e.my_p_x = prect.x e.my_p_y = prect.y } else { e.my_parent = null e.my_x = 0 e.my_y = 0 } } function animate_position(e) { if (e.parentElement && e.my_parent) { let dx = 0, dy = 0 let rect = e.getBoundingClientRect() if (e.parentElement === e.my_parent) { let prect = e.parentElement.getBoundingClientRect() dx = (e.my_x - e.my_p_x) - (rect.x - prect.x) dy = (e.my_y - e.my_p_y) - (rect.y - prect.y) } else { dx = e.my_x - rect.x dy = e.my_y - rect.y } if (dx !== 0 || dy !== 0) { e.animate( [ { transform: `translate(${dx}px, ${dy}px)`, }, { transform: "translate(0, 0)", }, ], { duration: e.my_duration, easing: "ease" } ) } } } let action_registry = [] function register_action(e, action, id, null_action=null) { e.my_id = id e.my_action = action e.my_null_action = null_action e.onclick = on_click_action action_registry.push(e) return e } function on_click_action(evt) { if (evt.button === 0) { if (send_action(evt.target.my_action, evt.target.my_id)) evt.stopPropagation() if (evt.target.my_null_action) if (send_action(evt.target.my_null_action, evt.target.my_id)) evt.stopPropagation() } } function is_action(action, arg) { if (arg === undefined) return !!(view.actions && view.actions[action] === 1) return !!(view.actions && view.actions[action] && view.actions[action].includes(arg)) } function create_div(className, text) { let e = document.createElement("div") e.className = className if (text) e.innerHTML = text return e } function append_div(parent, className, text) { let e = create_div(className, text) parent.appendChild(e) return e } function create_formation_slot(id) { let card = data.cards[id] let e = create_div("slot " + wing_name[card.wing]) e.dataset.card = card.number register_animation(e, 250) if (card.name === "The Fog" || card.name === "Retreat to Nivelles") { ui.slot_cubes[id] = append_div(e, "slot_cubes") ui.slot_sticks[id] = append_div(e, "slot_sticks") } else { if (card.special) ui.slot_cubes[id] = append_div(e, "slot_cubes") else ui.slot_sticks[id] = append_div(e, "slot_sticks") } e.appendChild(ui.cards[id]) if (card.infantry || card.cavalry) ui.slot_shift[id] = append_div(e, "slot_shift") ui.slot_dice[id] = append_div(e, "slot_dice") return e } function nbsp_target(target) { return target .split(", ") .map(x=>x.replaceAll(" ", "\xa0")) .join(", ") .replaceAll("\xa0or\xa0", " or ") .replaceAll("\xa0OR\xa0", " OR ") } function create_formation_card(id, tip=false) { let card = data.cards[id] let e = create_div("card formation " + wing_name[card.wing]) if (card.squeeze) e.classList.add("squeeze") if (!tip) register_action(e, "card", id) if (card.infantry || card.cavalry) append_div(e, "name with_symbol " + wing_name[card.wing], card.name) else append_div(e, "name " + wing_name[card.wing], card.name) if (card.infantry) append_div(e, "symbol infantry") if (card.cavalry) append_div(e, "symbol cavalry") if (card.special === 1) append_div(e, "strength", "I") else if (card.special === 2) append_div(e, "strength", "II") else if (card.special === 3) append_div(e, "strength", "III") else if (card.special === 4) append_div(e, "strength", "IV") else if (card.special === 5) append_div(e, "strength", "V") else if (card.special === -1) append_div(e, "strength", "?") else append_div(e, "strength", card.strength) if (card.link) { if (set_has(card.link, id - 1)) append_div(e, "link left") if (set_has(card.link, id + 1)) append_div(e, "link right") } if (card.dice) { let dice_text = dice_text_map[card.dice] || card.dice dice_text = dice_text.replaceAll("-", " – ") dice_text = dice_text.replaceAll("/", " / ") dice_text = dice_text.replace("(", "( ") dice_text = dice_text.replace(")", " )") if (card.morale === 2) append_div(e, "star", '★') append_div(e, "dice_area", dice_text) } function create_action(a, ix) { let ee = append_div(e, "action_row") let et if (reactions.includes(a.type)) et = append_div(ee, "action_type reaction", a.type) else et = append_div(ee, "action_type", a.type) if (!tip) register_action(et, "a" + ix, id, "n" + ix) append_div(ee, "action_requirement", a.requirement) if (a.target) append_div(ee, "action_target", nbsp_target(a.target)) else append_div(ee, "action_target", "") if (a.effect) { if (a.short) append_div(ee, "action_effect short", a.effect_text) else append_div(ee, "action_effect", a.effect_text) } return et } if (card.actions.length >= 1) create_action(card.actions[0], 1) if (card.rule_text_1) append_div(e, "rule_text", card.rule_text_1) if (card.actions.length >= 2) create_action(card.actions[1], 2) if (card.rule_text_2) append_div(e, "rule_text", card.rule_text_2) if (card.lore_text) append_div(e, "lore_text", card.lore_text) let reserve = null if (card.reserve) { if (Array.isArray(card.reserve) && card.reserve.length > 0) { reserve = card.reserve.map(c => data.cards[c].alias || data.cards[c].name).join(" or ") } else { reserve = card.reserve } } if (card.retire) { let ee if (card.pursuit) ee = append_div(e, "retire", "RETIRE, PURSUIT") else if (card.reserve) ee = append_div(e, "retire", `RETIRE, RESERVE (${reserve})`) else ee = append_div(e, "retire", "RETIRE") if (!tip) register_action(ee, "retire", id) } else if (card.pursuit) { append_div(e, "reserve", "PURSUIT") } else if (card.reserve) { append_div(e, "reserve", `IN RESERVE (${reserve})`) } append_div(e, "number", card.number) return e } function fill_card_row(parent, list) { parent.replaceChildren() for (let id of list) { let n, x if (!ui.cards[id]) ui.cards[id] = create_formation_card(id) if (!ui.slots[id]) ui.slots[id] = create_formation_slot(id) parent.append(ui.slots[id]) ui.cards[id].classList.toggle("selected", view.selected === id) ui.cards[id].classList.toggle("target", view.target === id) n = map_get(view.cubes, id, 0) for (let i = 0; i < n; ++i) add_ccube(ui.slot_cubes[id]) n = map_get(view.sticks, id, 0) x = 0 if (view.selected === id) x = view.self if (view.target === id) x = view.hits if (view.target2 === id) x = view.hits2 for (let i = 0; i < x && i < n; ++i) add_hit_stick(ui.slot_sticks[id]) for (let i = x; i < n; ++i) add_stick(ui.slot_sticks[id]) if (view.shift) { n = map_get(view.shift, id, 0) for (let i = 0; i < n; ++i) add_stick(ui.slot_shift[id]) } } } function add_mcube(parent) { for (let i = 0; i < 10; ++i) { if (ui.mcubes[i].parentElement === null) { parent.appendChild(ui.mcubes[i]) return } } throw Error("OUT OF CUBES ERROR") } function add_ccube(parent) { for (let i = 0; i < 10; ++i) { if (ui.ccubes[i].parentElement === null) { parent.appendChild(ui.ccubes[i]) return } } throw Error("OUT OF CUBES ERROR") } function add_stick(parent) { for (let i = 0; i < 80; ++i) { if (ui.sticks[i].parentElement === null) { parent.appendChild(ui.sticks[i]) return } } throw Error("OUT OF CUBES ERROR") } function add_hit_stick(parent) { for (let i = 0; i < 80; ++i) { if (ui.hit_sticks[i].parentElement === null) { parent.appendChild(ui.hit_sticks[i]) return } } throw Error("OUT OF CUBES ERROR") } function get_player_color(p) { let c = data.scenarios[view.scenario].players[p].cards[0] return data.cards[c].wing } function on_update() { let p1 = 0, p2 = 1 if (player === "First") p1 = 1, p2 = 0 ui.main.dataset.scenario = data.scenarios[view.scenario].number ui.name[p1].textContent = data.scenarios[view.scenario].players[0].name ui.name[p2].textContent = data.scenarios[view.scenario].players[1].name let w1 = get_player_color(0) let w2 = get_player_color(1) ui.role_panel[0].classList.add(side_color[w1]) ui.role_panel[0].classList.remove(side_color[w2]) ui.role_panel[1].classList.add(side_color[w2]) ui.role_panel[1].classList.remove(side_color[w1]) ui.role_name[0].textContent = data.scenarios[view.scenario].players[0].name ui.role_name[1].textContent = data.scenarios[view.scenario].players[1].name if (data.scenarios[view.scenario].players[0].tactical) ui.role_stat[0].textContent = `${view.morale[0]} (${view.tv1})` else ui.role_stat[0].textContent = view.morale[0] if (data.scenarios[view.scenario].players[1].tactical) ui.role_stat[1].textContent = `${view.morale[1]} (${view.tv2})` else ui.role_stat[1].textContent = view.morale[1] for (let e of animation_registry) remember_position(e) for (let e of ui.mcubes) e.remove() for (let e of ui.ccubes) e.remove() for (let e of ui.sticks) e.remove() for (let e of ui.hit_sticks) e.remove() for (let i = 0; i < view.morale[0]; ++i) add_mcube(ui.morale[p1]) for (let i = 0; i < view.morale[1]; ++i) add_mcube(ui.morale[p2]) fill_card_row(ui.reserve[p1], view.reserve[0]) fill_card_row(ui.front[p1], view.front[0]) fill_card_row(ui.reserve[p2], view.reserve[1]) fill_card_row(ui.front[p2], view.front[1]) function update_die(d, p) { let v = view.dice[d * 2 + 0] let c = view.dice[d * 2 + 1] ui.dice[d].className = "die d" + v if (c < 0) ui.pool[p].appendChild(ui.dice[d]) else ui.slot_dice[c].appendChild(ui.dice[d]) } for (let i = 0; i < 6; ++i) { update_die(i, p1) update_die(i+6, p2) } for (let e of action_registry) { if (e.my_null_action) { e.classList.toggle("action", is_action(e.my_action, e.my_id) || is_action(e.my_null_action, e.my_id)) e.classList.toggle("null", is_action(e.my_null_action, e.my_id)) } else { e.classList.toggle("action", is_action(e.my_action, e.my_id)) } } for (let e of animation_registry) animate_position(e) action_button("attack", "Attack") action_button("bombard", "Bombard") action_button("command", "Command") action_button("screen", "Screen") action_button("counterattack", "Counterattack") action_button("absorb", "Absorb") action_button("shift", "Shift") action_button("roll", "Roll") action_button("pass", "Pass") action_button("next", "Next") action_button("end_turn", "End turn") action_button("undo", "Undo") } for (let i = 0; i < 10; ++i) { ui.ccubes[i] = create_div("cube") ui.mcubes[i] = create_div("cube") } for (let i = 0; i < 80; ++i) { ui.sticks[i] = create_div("stick") ui.hit_sticks[i] = create_div("stick hit") } for (let i = 0; i < 12; ++i) { ui.dice[i] = register_action(create_div("die d0"), "die", i) register_animation(ui.dice[i], 250) } const ICONS = { D0: '', D1: '', D2: '', D3: '', D4: '', D5: '', D6: '', } function on_click_tip(id) { scroll_into_view_if_needed(ui.cards[id]) } function on_focus_tip(id) { if (!ui.cards_tip[id]) ui.cards_tip[id] = create_formation_card(id, true) ui.tooltip.replaceChildren(ui.cards_tip[id]) } function on_blur_tip() { ui.tooltip.replaceChildren() } function sub_icon(match) { return ICONS[match] } function sub_card(match) { let c = parseInt(match.substring(1)) let wing = wing_name[data.cards[c].wing] let name = data.cards[c].name return `${name}` } function on_log(text) { let p = document.createElement("div") if (text.match(/^\.h1 /)) { text = text.substring(4) p.className = "h1" let w = data.scenarios[view.scenario].wikipedia if (w) { p.innerHTML = `${text}` return p } } else if (text.match(/^\.h2 /)) { text = text.substring(4) p.className = "h2" } else if (text.match(/^\.p1/)) { let w = data.cards[data.scenarios[view.scenario].players[0].cards[0]].wing text = data.scenarios[view.scenario].players[0].name p.className = "h3 " + side_color[w] } else if (text.match(/^\.p2/)) { let w = data.cards[data.scenarios[view.scenario].players[1].cards[0]].wing text = data.scenarios[view.scenario].players[1].name p.className = "h3 " + side_color[w] } else if (text.match(/^\.rule /)) { text = text.substring(6) p.className = "rule" } else if (text.match(/^\.lore /)) { text = text.substring(6) p.className = "lore" } else if (text.match(/^>>/)) { text = text.substring(2) p.className = "i2" } else if (text.match(/^>/)) { text = text.substring(1) p.className = "i1" } else if (text.match(/^.img/)) { p.className = "img" let img = document.createElement("img") img.src = "paintings/" + data.scenarios[view.scenario].name.toLowerCase().replaceAll(" ", "_") + ".jpg" img.onerror = function (evt) { this.style.display="none" } let w = data.scenarios[view.scenario].wikipedia if (w) { let a = document.createElement("a") a.href = w p.appendChild(a) a.appendChild(img) } else { p.appendChild(img) } return p } text = text.replace(/\bC\d+\b/g, sub_card) text = text.replace(/\b[DPRBK]\d\b/g, sub_icon) p.innerHTML = text return p } function set_has(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else return true } return false } function map_get(map, key, missing) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else return map[(m<<1)+1] } return missing }