"use strict"
/* global data, view, action_button, send_action, scroll_with_middle_mouse */
/*
* CONSTANTS AND LISTS
*/
const DICE = {
D0: '',
D1: '',
D2: '',
D3: '',
D4: '',
D5: '',
D6: '',
}
const yoff = 1555
const xoff = 36
const hex_dx = 58.67
const hex_dy = 68
const hex_r = 56 >> 1
const TURN_X = 20 - 70 + 35 + 8 + 20
const TURN_Y = 1745 + 20
const TURN_DX = 70
const REINF_OFFSET = {
1015: [ hex_dx/2, hex_dy * 3/4 ],
1017: [ hex_dx/2, hex_dy * 3/4 ],
1018: [ -hex_dx/2, hex_dy * 3/4 ],
1020: [ -hex_dx/2, hex_dy * 3/4 ],
3000: [ -hex_dx/2, 0 ],
3241: [ hex_dx/2, 0 ],
4015: [ 0, -hex_dy * 3/8 ],
}
const P1 = "French"
const P2 = "Coalition"
const ELIMINATED = 0
const REINFORCEMENTS = 100
const AVAILABLE_P1 = 101
const AVAILABLE_P2 = 102
const BLOWN = 103
const last_corps = 22
const piece_count = 39
const first_hex = 1000
const last_hex = 4041
const adjacent_x1 = [
[-101,-100,-1,1,99,100],
[-100,-99,-1,1,100,101]
]
const adjacent_cn = [ "r3", "r2", "r4", "r1", "r5", "r0" ]
const data_rivers = []
const data_bridges = []
for (let [a, b] of data.map.rivers) {
set_add(data_rivers, a * 10000 + b)
set_add(data_rivers, b * 10000 + a)
}
for (let [a, b] of data.map.bridges) {
set_delete(data_rivers, a * 10000 + b)
set_delete(data_rivers, b * 10000 + a)
set_add(data_bridges, a * 10000 + b)
set_add(data_bridges, b * 10000 + a)
}
function make_piece_list(f) {
let list = []
for (let p = 0; p < data.pieces.length; ++p)
if (f(data.pieces[p]))
list.push(p)
return list
}
const p1_corps = make_piece_list(p => p.side === P1 && (p.type === "inf" || p.type === "cav"))
const p2_corps = make_piece_list(p => p.side !== P1 && (p.type === "inf" || p.type === "cav"))
const p1_det = make_piece_list(p => p.side === P1 && p.type === "det")
const p2_det = make_piece_list(p => p.side !== P1 && p.type === "det")
function find_piece(name) {
let id = data.pieces.findIndex(pc => pc.name === name)
if (id < 0)
throw new Error("PIECE NOT FOUND: " + name)
return id
}
const OLD_GUARD = find_piece("Old Guard")
const GRAND_BATTERY = find_piece("Grand Battery")
const IMPERIAL_GUARD = find_piece("Guard Corps (Drouot)")
const IMPERIAL_GUARD_CAV = find_piece("Guard Cav Corps (Guyot)")
for (let info of data.reinforcements)
info.list = info.list.map(name => find_piece(name))
/*
* INIT UI
*/
let ui = {
header: document.querySelector("header"),
arrow: document.getElementById("arrow"),
hexes: new Array(last_hex+1).fill(null),
hex_x: new Array(last_hex+1).fill(0),
hex_y: new Array(last_hex+1).fill(0),
pieces: [
document.getElementById("french_hq_1"),
document.getElementById("french_hq_2"),
document.getElementById("french_hq_3"),
document.getElementById("anglo_hq_1"),
document.getElementById("prussian_hq_1"),
document.getElementById("french_corps_1"),
document.getElementById("french_corps_2"),
document.getElementById("french_corps_3"),
document.getElementById("french_corps_4"),
document.getElementById("french_corps_5"),
document.getElementById("french_corps_6"),
document.getElementById("french_corps_7"),
document.getElementById("french_corps_8"),
document.getElementById("anglo_corps_1"),
document.getElementById("anglo_corps_2"),
document.getElementById("anglo_corps_3"),
document.getElementById("anglo_corps_4"),
document.getElementById("anglo_corps_5"),
document.getElementById("prussian_corps_1"),
document.getElementById("prussian_corps_2"),
document.getElementById("prussian_corps_3"),
document.getElementById("prussian_corps_4"),
document.getElementById("prussian_corps_5"),
document.getElementById("french_detachment_1"),
document.getElementById("french_detachment_2"),
document.getElementById("french_detachment_3"),
document.getElementById("french_detachment_4"),
document.getElementById("french_detachment_5"),
document.getElementById("french_detachment_6"),
document.getElementById("anglo_detachment_1"),
document.getElementById("anglo_detachment_2"),
document.getElementById("anglo_detachment_3"),
document.getElementById("anglo_detachment_4"),
document.getElementById("prussian_detachment_1"),
document.getElementById("prussian_detachment_2"),
document.getElementById("prussian_detachment_3"),
document.getElementById("prussian_detachment_4"),
document.getElementById("prussian_detachment_5"),
document.getElementById("prussian_detachment_6"),
],
stack: new Array(last_hex+1).fill(0),
turn: document.getElementById("marker_turn"),
remain: document.getElementById("marker_remain"),
french_moves: document.getElementById("marker_french_moves"),
prussian_moves: document.getElementById("marker_prussian_moves"),
french_vp: document.querySelector("#role_French .role_vp"),
coalition_vp: document.querySelector("#role_Coalition .role_vp"),
}
for (let row = 0; row < data.map.rows; ++row) {
for (let col = 0; col < data.map.cols; ++col) {
let hex_id = first_hex + 100 * row + col
let hex_x = ui.hex_x[hex_id] = Math.floor(xoff + hex_dx * (col + (row & 1) * 0.5 + 0.5))
let hex_y = ui.hex_y[hex_id] = Math.floor(yoff - hex_dy * 3 / 4 * row + hex_dy/2)
let hex = ui.hexes[hex_id] = document.createElement("div")
hex.className = "hex"
hex.style.left = (hex_x - hex_r) + "px"
hex.style.top = (hex_y - hex_r) + "px"
hex.style.width = (hex_r * 2) + "px"
hex.style.height = (hex_r * 2) + "px"
hex.onmousedown = on_click_action
hex.onmouseenter = on_focus_hex
hex.onmouseleave = on_blur_hex
hex.my_action = "hex"
hex.my_action_2 = "stop_hex"
hex.my_id = hex_id
if (data.map.names[hex_id])
hex.my_name = String(hex_id) + " (" + data.map.names[hex_id] + ")"
else
hex.my_name = String(hex_id)
document.getElementById("hexes").appendChild(hex)
}
}
for (let p = 0; p < ui.pieces.length; ++p) {
ui.pieces[p].onmousedown = on_click_action
ui.pieces[p].onmouseenter = on_focus_piece
ui.pieces[p].onmouseleave = on_blur_piece
ui.pieces[p].my_action = "piece"
ui.pieces[p].my_id = p
ui.pieces[p].my_name = data.pieces[p].name
}
scroll_with_middle_mouse("main")
/*
* TOOLTIPS & ACTIONS
*/
function toggle_pieces() {
document.getElementById("pieces").classList.toggle("hide")
}
function toggle_zoc() {
document.getElementById("hexes").classList.toggle("zoc")
}
function is_action(action, arg) {
if (arg === undefined)
return !!(view.actions && view.actions[action] === 1)
return !!(view.actions && view.actions[action] && set_has(view.actions[action], arg))
}
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_action_2)
if (send_action(evt.target.my_action_2, evt.target.my_id))
evt.stopPropagation()
}
}
function on_focus_hex_tip(id) {
ui.hexes[id].classList.add("tip")
}
function on_blur_hex_tip(id) {
ui.hexes[id].classList.remove("tip")
}
function on_click_hex_tip(id) {
ui.hexes[id].scrollIntoView({ block:"center", inline:"center", behavior:"smooth" })
}
function on_focus_piece_tip(id) {
ui.pieces[id].classList.add("tip")
}
function on_blur_piece_tip(id) {
ui.pieces[id].classList.remove("tip")
}
function on_click_piece_tip(id) {
ui.pieces[id].scrollIntoView({ block:"center", inline:"center", behavior:"smooth" })
}
function on_blur() {
document.getElementById("status").textContent = ""
}
var focused_piece = -1
function on_focus_piece(evt) {
let p = evt.target.my_id
document.getElementById("status").textContent = evt.target.my_name
if (data.pieces[p].type === "hq") {
focused_piece = p
show_hq_range(p)
}
}
function on_blur_piece(evt) {
let p = evt.target.my_id
on_blur()
if (data.pieces[p].type === "hq") {
focused_piece = -1
hide_hq_range(p)
}
}
function on_focus_hex(evt) {
document.getElementById("status").textContent = "Hex " + evt.target.my_name
if (view && view.move_from)
show_move_path(evt.target.my_id)
}
function on_blur_hex() {
on_blur()
hide_move_path()
}
/*
* SHOW PATH (FOR MOVE ACTIONS)
*/
var _move_path = []
function hide_move_path() {
if (_move_path) {
for (let x of _move_path) {
ui.hexes[x].classList.remove("move")
ui.hexes[x].classList.remove("road")
}
_move_path = null
}
}
function show_move_path(x) {
if (_move_path)
hide_move_path()
if (!is_action("hex", x) && !is_action("stop_hex", x))
return
if (view.move_from && map_get(view.move_from, x, 0)) {
_move_path = []
for (let i = 0; x && i < 100; ++i) {
_move_path.push(x)
x = map_get(view.move_from, x, 0)
}
for (let x of _move_path)
ui.hexes[x].classList.add("move")
}
else
if (view.move_from_road && map_get(view.move_from_road, x, 0)) {
_move_path = []
for (let i = 0; x && i < 100; ++i) {
_move_path.push(x)
x = map_get(view.move_from_road, x, 0)
}
for (let x of _move_path)
ui.hexes[x].classList.add("road")
}
}
/*
* SHOW HQ RANGE (FOR HQ MOUSEOVER)
*/
function is_in_range(x, hq) {
let hq_x = view.pieces[hq] >> 1
if (hq_x >= 1000) {
let hq_m = view.pieces[hq] & 1
let hq_r = hq_m ? data.pieces[hq].range2 : data.pieces[hq].range1
return calc_distance(x, hq_x) === hq_r
}
return false
}
function show_hq_range(hq) {
for (let row = 0; row < data.map.rows; ++row) {
for (let col = 0; col < data.map.cols; ++col) {
let id = first_hex + row * 100 + col
ui.hexes[id].classList.toggle("range", is_in_range(id, hq))
}
}
}
function hide_hq_range() {
for (let row = 0; row < data.map.rows; ++row) {
for (let col = 0; col < data.map.cols; ++col) {
let id = first_hex + row * 100 + col
ui.hexes[id].classList.remove("range")
}
}
}
/*
* UPDATE UI
*/
function is_piece_support(id) {
if (view.support)
return view.support & (1 << id)
return false
}
function on_update() {
ui.stack.fill(0)
update_zoc()
if (search_brussels_path())
ui.french_vp.textContent = count_french_vp() + " + 5 VP"
else
ui.french_vp.textContent = count_french_vp() + " + 0 VP"
ui.coalition_vp.textContent = count_coalition_vp() + " VP"
if (!view.move_path)
hide_move_path()
for (let row = 0; row < data.map.rows; ++row) {
for (let col = 0; col < data.map.cols; ++col) {
let id = first_hex + row * 100 + col
ui.hexes[id].classList.toggle("action", is_action("hex", id) || is_action("stop_hex", id))
ui.hexes[id].classList.toggle("stop", is_action("stop_hex", id))
}
}
if (focused_piece >= 0)
show_hq_range(focused_piece)
if (view.who >= 0 && view.target >= 0) {
let wx = view.pieces[view.who] >> 1
let tx = view.pieces[view.target] >> 1
if (wx >= 1000 && tx >= 1000 && calc_distance(wx, tx) === 1) {
ui.arrow.style.left = (ui.hex_x[wx] - 25) + "px"
ui.arrow.style.top = (ui.hex_y[wx] - 50) + "px"
for (let i = 0; i < 6; ++i) {
let dx = adjacent_x1[wx / 100 & 1][i]
if (tx - wx === dx)
ui.arrow.className = adjacent_cn[i]
}
} else {
ui.arrow.className = "hide"
}
} else {
ui.arrow.className = "hide"
}
for (let id = 0; id < piece_count; ++id) {
let hex = view.pieces[id] >> 1
let z = 0
let s = 0
if (hex > BLOWN && hex < BLOWN + 20)
hex -= BLOWN
if (hex >= first_hex || hex === REINFORCEMENTS) {
// ON MAP
ui.pieces[id].classList.remove("hide")
ui.pieces[id].classList.toggle("flip", (view.pieces[id] & 1) === 1)
let x, y
if (hex === REINFORCEMENTS) {
hex = find_reinforcement_hex(id)
if (typeof hex !== "number")
hex = hex[0]
s = find_reinforcement_z(id)
z = 4 - s
x = ui.hex_x[hex] + s * 24
y = ui.hex_y[hex] + s * 18
if (REINF_OFFSET[hex]) {
x += REINF_OFFSET[hex][0]
y += REINF_OFFSET[hex][1]
}
} else {
s = z = ui.stack[hex]++
x = ui.hex_x[hex] - s * 18
y = ui.hex_y[hex] + s * 12
}
if (id <= last_corps) {
x -= (46>>1)
y -= (46>>1)
} else {
x -= (38>>1)
y -= (38>>1)
}
ui.pieces[id].style.top = y + "px"
ui.pieces[id].style.left = x + "px"
ui.pieces[id].style.zIndex = z
} else if (hex >= AVAILABLE_P1 && hex <= BLOWN) {
// OFF MAP DETACHMENTS / LEADERS / REINFORCEMENTS
ui.pieces[id].classList.remove("hide")
ui.pieces[id].classList.toggle("flip", (view.pieces[id] & 1) === 1)
let x = 600 + 40 + ui.stack[hex] * 60 + 40
let y = 1650 + 40 + 60 * (hex-AVAILABLE_P1) + 20
ui.stack[hex] += 1
ui.pieces[id].style.top = y + "px"
ui.pieces[id].style.left = x + "px"
ui.pieces[id].style.zIndex = 0
} else if (hex >= 1 && hex <= 20) {
// ON TURN TRACK
ui.pieces[id].classList.remove("hide")
ui.pieces[id].classList.remove("flip")
let x = TURN_X + hex * TURN_DX - ui.stack[hex] * 18
let y = TURN_Y + ui.stack[hex] * 12
ui.stack[hex] += 1
if (id <= last_corps) {
x -= (46>>1)
y -= (46>>1)
} else {
x -= (38>>1)
y -= (38>>1)
}
ui.pieces[id].style.top = y + "px"
ui.pieces[id].style.left = x + "px"
} else {
// ELIMINATED or SWAPPED
ui.pieces[id].classList.add("hide")
}
//if (is_action("piece", id)) z = 101
if (view.target === id) z = 102
if (view.who === id) z = 103
ui.pieces[id].style.zIndex = z
ui.pieces[id].classList.toggle("action", is_action("piece", id))
ui.pieces[id].classList.toggle("selected", view.who === id)
ui.pieces[id].classList.toggle("target", view.target === id)
ui.pieces[id].classList.toggle("support", is_piece_support(id))
}
ui.turn.style.left = (40 + TURN_X + (view.turn-1) * TURN_DX) + "px"
ui.turn.classList.toggle("flip", view.rain === 2)
if (view.remain > 0) {
ui.remain.style.left = (20 + 109 + (view.remain % 10) * 47.5 | 0) + "px"
ui.remain.classList.toggle("flip", view.remain > 9)
ui.remain.classList.remove("hide")
} else {
ui.remain.classList.add("hide")
}
if (view.french_moves !== undefined) {
let x = (20 + 109 + (view.french_moves % 10) * 47.5 | 0)
ui.french_moves.style.left = x + "px"
ui.french_moves.classList.toggle("flip", view.french_moves > 9)
ui.french_moves.classList.remove("hide")
} else {
ui.french_moves.classList.add("hide")
}
if (view.prussian_moves !== undefined) {
let x = (20 + 109 + (view.prussian_moves % 10) * 47.5 | 0)
let y = 1857
if (view.prussian_moves === view.french_moves) {
x += 12
y -= 12
}
ui.prussian_moves.style.left = x + "px"
ui.prussian_moves.style.top = y + "px"
ui.prussian_moves.classList.toggle("flip", view.prussian_moves > 9)
ui.prussian_moves.classList.remove("hide")
} else {
ui.prussian_moves.classList.add("hide")
}
action_button("blow", "Blow")
action_button("roll", "Roll")
action_button("next", "Next")
action_button("end_step", "End step")
action_button("end_turn", "End turn")
action_button("pass", "Pass")
action_button("undo", "Undo")
}
/*
* LOG
*/
function sub_hex(match, p1) {
let x = p1 | 0
let n = data.map.names[x]
if (n)
n = x + " " + n
else
n = x
return `${n}`
}
function sub_piece(match, p1) {
let x = p1 | 0
let n = data.pieces[x].name
let c = "piece"
if (data.pieces[x].side === "Anglo")
c = "tip anglo"
else if (data.pieces[x].side === "Prussian")
c = "tip prussian"
else if (data.pieces[x].side === "French")
c = "tip french"
return `${n}`
}
function on_log(text) {
let p = document.createElement("div")
if (text.match(/^>/)) {
text = text.substring(1)
p.className = 'i'
}
text = text.replace(/&/g, "&")
text = text.replace(//g, ">")
text = text.replace(/\b(\d\d\d\d)\b/g, sub_hex)
text = text.replace(/P(\d+)/g, sub_piece)
text = text.replace(/\bD\d\b/g, match => DICE[match])
text = text.replace(/^French/g, 'French')
text = text.replace(/^Coalition/g, 'Coalition')
if (text.match(/^\.h1 /)) {
text = text.substring(4)
p.className = "h1"
}
else if (text.match(/^\.h2/)) {
text = text.substring(4)
p.className = "h2"
}
else if (text.match(/^\.h3/)) {
text = text.substring(4)
p.className = "h3"
}
p.innerHTML = text
return p
}
/*
* COPIED FROM RULES
*/
function array_remove(array, index) {
let n = array.length
for (let i = index + 1; i < n; ++i)
array[i - 1] = array[i]
array.length = n - 1
}
function array_insert(array, index, item) {
for (let i = array.length; i > index; --i)
array[i] = array[i - 1]
array[index] = item
}
function set_has(set, item) {
if (!set)
return false
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 set_add(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
}
array_insert(set, a, item)
}
function set_delete(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 {
array_remove(set, m)
return
}
}
}
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
}
function calc_distance(a, b) {
let ac = a % 100
let bc = b % 100
let ay = a / 100 | 0
let by = b / 100 | 0
let ax = ac - (ay >> 1)
let bx = bc - (by >> 1)
let az = -ax - ay
let bz = -bx - by
return Math.max(Math.abs(bx-ax), Math.abs(by-ay), Math.abs(bz-az))
}
var move_seen = new Array(last_hex - 999).fill(0)
var zoc_cache = new Array(data.map.rows * 100).fill(0)
function is_map_hex(x) {
if (x >= 1000 && x <= 4041)
return x % 100 <= 41
return false
}
function is_river(a, b) {
return set_has(data_rivers, a * 10000 + b)
}
function is_bridge(a, b) {
return set_has(data_bridges, a * 10000 + b)
}
function piece_hex(p) {
return view.pieces[p] >> 1
}
function for_each_adjacent(x, f) {
for (let dx of adjacent_x1[x / 100 & 1]) {
let nx = x + dx
if (is_map_hex(nx))
f(nx)
}
}
function update_zoc_imp(zoc, zoi, units) {
for (let p of units) {
let a = piece_hex(p)
zoc_cache[a - 1000] |= zoc
for_each_adjacent(a, b => {
if (!is_river(a, b)) {
zoc_cache[b - 1000] |= zoc
if (zoi) {
for_each_adjacent(b, c => {
if (!is_bridge(b, c))
zoc_cache[c - 1000] |= zoi
})
}
}
})
}
}
function update_zoc() {
zoc_cache.fill(0)
update_zoc_imp(1, 4, p1_corps)
update_zoc_imp(1, 0, p1_det)
update_zoc_imp(16, 64, p2_corps)
update_zoc_imp(16, 0, p2_det)
}
function is_p1_zoc(x) { return (zoc_cache[x-1000] & 1) > 0 }
function is_p1_zoi(x) { return (zoc_cache[x-1000] & 4) > 0 }
function is_p2_zoc(x) { return (zoc_cache[x-1000] & 16) > 0 }
function is_p2_zoi(x) { return (zoc_cache[x-1000] & 64) > 0 }
function search_brussels_path() {
move_seen.fill(0)
move_seen[1017-1000] = 1
move_seen[1018-1000] = 1
let queue = []
if (!is_p2_zoc(1017))
queue.push(1017)
if (!is_p2_zoc(1018))
queue.push(1018)
while (queue.length > 0) {
let here = queue.shift()
for_each_adjacent(here, next => {
if (move_seen[next-1000])
return
if (is_p2_zoc(next))
return
if (is_river(here, next))
return
move_seen[next-1000] = 1
queue.push(next)
})
}
if (move_seen[4006-1000] || move_seen[4015-1000] || move_seen[4025-1000])
return true
return false
}
function find_reinforcement_hex(who) {
for (let info of data.reinforcements)
for (let p of info.list)
if (p === who)
return info.hex
return REINFORCEMENTS
}
function find_reinforcement_z(who) {
for (let info of data.reinforcements) {
let n = 0
for (let p of info.list) {
if (p === who)
return n
if ((view.pieces[p] >> 1) === REINFORCEMENTS)
++n
}
}
return 0
}
function count_french_vp() {
let vp = 0
for (let p of p2_corps)
if (piece_hex(p) === ELIMINATED)
vp += 3
for (let p of p2_det)
if (piece_hex(p) === ELIMINATED)
vp += 1
return vp
}
function count_coalition_vp() {
let vp = 0
for (let p of p1_corps) {
if (piece_hex(p) === ELIMINATED) {
if (p === IMPERIAL_GUARD || p === IMPERIAL_GUARD_CAV)
vp += 5
else
vp += 3
}
}
for (let p of p1_det) {
if (piece_hex(p) === ELIMINATED) {
if (p === GRAND_BATTERY || p === OLD_GUARD)
vp += 2
else
vp += 1
}
}
return vp
}