"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")
register_animation(ui.mcubes[i], 750)
}
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
}