summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--play.html97
-rw-r--r--play.js154
-rw-r--r--rules.js200
3 files changed, 414 insertions, 37 deletions
diff --git a/play.html b/play.html
index 9b888f5..5df888b 100644
--- a/play.html
+++ b/play.html
@@ -31,6 +31,80 @@ header.your_turn { background-color: orange; }
cursor: pointer;
}
+/* BATTLE DIALOG */
+
+#battle_header { background-color: brown; color: gold }
+#battle_hits { background-color: #c4ab8b; }
+#battle_line_1, #battle_line_2 { background-color: #d6c4a9; background: url(texture_clear.png); }
+#battle_buttons { background-color: #c4ab8b; }
+#battle_message { background-color: #d6c4a9; }
+
+#battle {
+ position: fixed;
+ min-width: 524px; /* 6 blocks wide */
+ left: 12px;
+ top: 56px;
+ z-index: 100;
+ box-shadow: 0px 5px 10px 0px rgba(0,0,0,0.5);
+ border: 1px solid black;
+ user-select: none;
+}
+
+#battle_header {
+ cursor: move;
+ padding: 2px 8px;
+ line-height: 24px;
+ min-height: 24px;
+ text-align: center;
+ font-weight: bold;
+ border-bottom: 1px solid black;
+}
+
+#battle_message {
+ padding: 2px 8px;
+ line-height: 24px;
+ min-height: 24px;
+ text-align: center;
+ border-top: 1px solid black;
+}
+
+#battle_hits {
+ padding: 4px;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 6px;
+ border-bottom: 1px solid black;
+}
+
+#battle_hits .hits_text {
+ width: 24px;
+}
+
+#battle_hits .hits_icon {
+ display: block;
+ vertical-align: middle
+}
+
+#battle_line_1, #battle_line_2 {
+ padding: 20px;
+ min-height: 60px;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 16px;
+}
+
+#battle_buttons {
+ padding: 12px;
+ min-height: 28px;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 12px;
+ border-top: 1px solid black;
+}
+
/* TABLES */
table { border-collapse: collapse; font-size: 12px; }
@@ -361,6 +435,29 @@ svg .hex.allied_control {
</head>
<body>
+ <div id="battle" class="hide">
+ <div id="battle_header"></div>
+ <div id="battle_hits">
+ <img class="hits_icon" src="icons/armor.svg">
+ <div class="hits_text" id="hits_armor">0</div>
+ <img class="hits_icon" src="icons/infantry.svg">
+ <div class="hits_text" id="hits_infantry">0</div>
+ <img class="hits_icon" src="icons/motorized_antitank_old.svg">
+ <div class="hits_text" id="hits_antitank">0</div>
+ <img class="hits_icon" src="icons/artillery.svg">
+ <div class="hits_text" id="hits_artillery">0</div>
+ </div>
+ <div id="battle_line_1"></div>
+ <div id="battle_line_2"></div>
+ <div id="battle_buttons">
+ <button id="target_armor_button" onclick="send_action('armor')">Armor</button>
+ <button id="target_infantry_button" onclick="send_action('infantry')">Infantry</button>
+ <button id="target_antitank_button" onclick="send_action('antitank')">Anti-tank</button>
+ <button id="target_artillery_button" onclick="send_action('artillery')">Artillery</button>
+ </div>
+ <div id="battle_message"></div>
+ </div>
+
<header>
<div id="toolbar">
<div class="menu">
diff --git a/play.js b/play.js
index 125bb19..615c25a 100644
--- a/play.js
+++ b/play.js
@@ -6,7 +6,7 @@ const svgNS = "http://www.w3.org/2000/svg"
const round = Math.round
const sqrt = Math.sqrt
-function set_index(set, item) {
+function set_has(set, item) {
let a = 0
let b = set.length - 1
while (a <= b) {
@@ -17,13 +17,9 @@ function set_index(set, item) {
else if (item > x)
a = m + 1
else
- return m
+ return true
}
- return -1
-}
-
-function set_has(set, item) {
- return set_index(set, item) >= 0
+ return false
}
let ui = {
@@ -32,10 +28,31 @@ let ui = {
hex_x: [],
hex_y: [],
units: [],
+ battle_units: [],
+ battle: document.getElementById("battle"),
+ battle_hits: [
+ document.getElementById("hits_armor"),
+ document.getElementById("hits_infantry"),
+ document.getElementById("hits_antitank"),
+ document.getElementById("hits_artillery")
+ ],
+ battle_buttons: [
+ document.getElementById("target_armor_button"),
+ document.getElementById("target_infantry_button"),
+ document.getElementById("target_antitank_button"),
+ document.getElementById("target_artillery_button")
+ ],
+ battle_header: document.getElementById("battle_header"),
+ battle_message: document.getElementById("battle_message"),
+ battle_line_1: document.getElementById("battle_line_1"),
+ battle_line_2: document.getElementById("battle_line_2"),
onmap: document.getElementById("units"),
focus: null,
}
+const AXIS = 'Axis'
+const ALLIED = 'Allied'
+
function unit_hex(u) {
return view.units[u] >>> 5
}
@@ -56,6 +73,10 @@ function is_unit_moved(u) {
return (view.units[u] & 16) === 16
}
+function is_unit_fired(u) {
+ return set_has(view.fired, u)
+}
+
function is_unit_action(unit) {
return !!(view.actions && view.actions.unit && view.actions.unit.includes(unit))
}
@@ -64,6 +85,14 @@ function is_unit_selected(unit) {
return !!(view.selected && view.selected.includes(unit))
}
+function is_allied_unit(u) {
+ return units[u].nationality === 'allied'
+}
+
+function is_axis_unit(u) {
+ return units[u].nationality !== 'allied'
+}
+
function is_hex_action(hex) {
return !!(view.actions && view.actions.hex && view.actions.hex.includes(hex))
}
@@ -137,6 +166,12 @@ function on_click_unit(evt) {
}
}
+function on_click_battle_unit(evt) {
+ if (evt.button === 0) {
+ send_action('unit', evt.target.unit)
+ }
+}
+
document.getElementById("map").addEventListener("mousedown", function (evt) {
if (evt.button === 0) {
blur_stack()
@@ -158,6 +193,12 @@ function on_focus_unit(evt) {
document.getElementById("status").textContent = `(${u}) ${data.nationality} ${data.elite ? "elite " : ""}${data.type} - ${data.steps} - ${data.name}`
}
+function on_focus_battle_unit(evt) {
+ let u = evt.target.unit
+ let data = units[u]
+ document.getElementById("status").textContent = `(${u}) ${data.nationality} ${data.elite ? "elite " : ""}${data.type} - ${data.steps} - ${data.name}`
+}
+
function toggle_units() {
document.getElementById("units").classList.toggle("hide")
}
@@ -281,11 +322,18 @@ function build_hexes() {
function build_units() {
function build_unit(u, data) {
let elt = ui.units[u] = document.createElement("div")
- elt.className = `unit ${data.nationality} u${u} r0`
+ elt.className = `unit ${data.nationality} u${u} r0 m`
elt.addEventListener("mousedown", on_click_unit)
elt.addEventListener("mouseenter", on_focus_unit)
elt.addEventListener("mouseleave", on_blur)
elt.unit = u
+
+ elt = ui.battle_units[u] = document.createElement("div")
+ elt.className = `unit ${data.nationality} u${u} r0`
+ elt.addEventListener("mousedown", on_click_battle_unit)
+ elt.addEventListener("mouseenter", on_focus_battle_unit)
+ elt.addEventListener("mouseleave", on_blur)
+ elt.unit = u
}
for (let u = 0; u < units.length; ++u) {
build_unit(u, units[u])
@@ -322,19 +370,22 @@ function update_map() {
let x, y, z
if (stack[hex] === ui.focus) {
- x = ui.hex_x[hex] - 30
- y = ui.hex_y[hex] - 30 + i * 64
+ x = ui.hex_x[hex] - 25
+ y = ui.hex_y[hex] - 25 + i * 54
z = 100
} else {
- if (stack[hex].length <= 4) {
- x = ui.hex_x[hex] - 30 + i * 13
- y = ui.hex_y[hex] - 30 + i * 16
+ if (stack[hex].length <= 1) {
+ x = ui.hex_x[hex] - 25 + i * 11
+ y = ui.hex_y[hex] - 25 + i * 14
+ } else if (stack[hex].length <= 4) {
+ x = ui.hex_x[hex] - 30 + i * 11
+ y = ui.hex_y[hex] - 30 + i * 14
} else if (stack[hex].length <= 8) {
- x = ui.hex_x[hex] - 30 + i * 8
- y = ui.hex_y[hex] - 30 + i * 8
+ x = ui.hex_x[hex] - 35 + i * 4
+ y = ui.hex_y[hex] - 35 + i * 4
} else {
- x = ui.hex_x[hex] - 30 + i * 3
- y = ui.hex_y[hex] - 30 + i * 3
+ x = ui.hex_x[hex] - 35 + i * 2
+ y = ui.hex_y[hex] - 35 + i * 2
}
z = 1 + i
}
@@ -349,12 +400,13 @@ function update_map() {
e.classList.toggle("r2", r === 2)
e.classList.toggle("r3", r === 3)
- e.classList.toggle("action", is_unit_action(u))
- e.classList.toggle("selected", is_unit_selected(u))
+ e.classList.toggle("action", !view.battle && is_unit_action(u))
+ e.classList.toggle("selected", !view.battle && is_unit_selected(u))
e.classList.toggle("disrupted", is_unit_disrupted(u))
e.classList.toggle("moved", is_unit_moved(u))
// e.classList.toggle("unsupplied", !is_unit_supplied(u))
}
+
if (ui.hexes[hex]) {
ui.hexes[hex].classList.toggle("action", is_hex_action(hex))
ui.hexes[hex].classList.toggle("from", hex === view.from1 || hex === view.from2)
@@ -379,13 +431,74 @@ function update_map() {
}
}
+function update_battle_line(line, test) {
+ for (let u = 0; u < units.length; ++u) {
+ let e = ui.battle_units[u]
+ if (unit_hex(u) === view.battle && test(u)) {
+ if (!line.contains(e))
+ line.appendChild(e)
+
+ let r = unit_lost_steps(u)
+ e.classList.toggle("r0", r === 0)
+ e.classList.toggle("r1", r === 1)
+ e.classList.toggle("r2", r === 2)
+ e.classList.toggle("r3", r === 3)
+
+ e.classList.toggle("action", is_unit_action(u))
+ e.classList.toggle("selected", is_unit_selected(u))
+ e.classList.toggle("disrupted", is_unit_disrupted(u))
+ e.classList.toggle("fire", is_unit_fired(u))
+ } else {
+ if (line.contains(e))
+ line.removeChild(e)
+ }
+ }
+}
+
+function update_battle() {
+ ui.battle.classList.remove("hide")
+ ui.battle_header.textContent = hex_name[view.battle]
+ ui.battle_message.textContent = view.flash
+ if (player === ALLIED) {
+ update_battle_line(ui.battle_line_1, is_axis_unit)
+ update_battle_line(ui.battle_line_2, is_allied_unit)
+ } else {
+ update_battle_line(ui.battle_line_1, is_allied_unit)
+ update_battle_line(ui.battle_line_2, is_axis_unit)
+ }
+ target_button("target_armor")
+ target_button("target_infantry")
+ target_button("target_antitank")
+ target_button("target_artillery")
+}
+
+function target_button(action) {
+ let button = document.getElementById(action + "_button")
+ if (view.actions) {
+ button.classList.remove("hide")
+ if (view.actions[action])
+ button.disabled = false
+ else
+ button.disabled = true
+ } else {
+ button.classList.add("hide")
+ }
+}
+
function on_update() {
update_map()
+ if (view.battle)
+ update_battle()
+ else
+ ui.battle.classList.add("hide")
+
action_button("overrun", "Overrun")
action_button("rommel", "Rommel")
- action_button("end_move", "End move")
action_button("stop", "Stop")
+ action_button("end_move", "End move")
+
+ action_button("end_combat", "End combat")
action_button("group", "Group")
action_button("regroup", "Regroup")
@@ -400,4 +513,5 @@ function on_update() {
action_button("undo", "Undo")
}
+drag_element_with_mouse("#battle", "#battle_header")
scroll_with_middle_mouse("main")
diff --git a/rules.js b/rules.js
index bc6c618..4edfa17 100644
--- a/rules.js
+++ b/rules.js
@@ -159,6 +159,22 @@ function unit_speed(u) {
return units[u].speed
}
+function is_artillery_unit(u) {
+ return units[u].class === 'artillery'
+}
+
+function is_armor_unit(u) {
+ return units[u].class === 'armor'
+}
+
+function is_infantry_unit(u) {
+ return units[u].class === 'infantry'
+}
+
+function is_antitank_unit(u) {
+ return units[u].class === 'antitank'
+}
+
function unit_hex(u) {
return game.units[u] >>> 5
}
@@ -211,6 +227,14 @@ function clear_unit_moved(u) {
game.units[u] &= ~16
}
+function is_unit_fired(u) {
+ return set_has(game.fired, u)
+}
+
+function set_unit_fired(u) {
+ set_add(game.fired, u)
+}
+
function unit_steps(u) {
return units[u].steps - unit_lost_steps(u)
}
@@ -643,7 +667,7 @@ function clear_supply_networks() {
game.allied_supply_line = null
}
-// === MOVEMENT ===
+// === PATHING ===
const path_from = [ new Array(hexcount), new Array(hexcount), new Array(hexcount), null, new Array(hexcount) ]
const path_cost = [ new Array(hexcount), new Array(hexcount), new Array(hexcount), null, new Array(hexcount) ]
@@ -851,6 +875,17 @@ function find_valid_regroup_destinations(from, rommel) {
// === TURN ===
+function set_active_player() {
+ game.active = game.phasing
+}
+
+function set_passive_player() {
+ if (game.phasing === AXIS)
+ game.active = ALLIED
+ else
+ game.active = AXIS
+}
+
// Supply check
// Turn option
// Movement
@@ -866,14 +901,17 @@ function end_player_turn() {
game.phasing = ALLIED
else
game.phasing = AXIS
- game.active = game.phasing
+ set_active_player()
goto_player_turn()
}
function goto_player_turn() {
+ // paranoid resetting of state
+ game.side_limit = {}
game.rommel = 0
game.from1 = game.from2 = 0
game.to1 = game.to2 = 0
+
goto_supply_check()
}
@@ -922,6 +960,8 @@ states.turn_option = {
},
}
+// ==== MOVEMENT PHASE ===
+
function goto_move_phase() {
game.state = 'select_moves'
if (game.active === AXIS) {
@@ -1133,17 +1173,11 @@ states.move_who = {
},
end_move() {
clear_supply_networks()
+ // TODO
+ goto_combat_phase()
}
}
-function rommel_group_move_bonus(from) {
- if (game.rommel === 1 && from === game.from1 && !game.to1)
- return 1
- if (game.rommel === 2 && from === game.from2 && !game.to2)
- return 1
- return 0
-}
-
function print_path(who, from, to, road) {
let p = [ hex_name[to] ]
while (to && to !== from) {
@@ -1190,7 +1224,7 @@ function apply_move(move, who, from, to) {
claim_hexside_control(side)
if (is_new_battle_hex(to)) {
claim_hex_control_for_defender(to)
- game.battles.push(to)
+ set_add(game.active_battles, to)
}
return true
}
@@ -1363,6 +1397,127 @@ function stop_move(who) {
game.state = 'move_who'
}
+// ==== COMBAT PHASE ===
+
+function goto_combat_phase() {
+ game.state = 'select_active_battles'
+}
+
+states.select_active_battles = {
+ inactive: "combat phase (select active battles)",
+ prompt() {
+ view.prompt = `Select active battles.`
+ for (let x = first_hex; x <= last_hex; ++x)
+ if (hex_exists[x])
+ if (!set_has(game.active_battles, x) && is_battle_hex(x))
+ gen_action_hex(x)
+ gen_action('next')
+ },
+ hex(x) {
+ push_undo()
+ set_add(game.active_battles, x)
+ },
+ next() {
+ push_undo()
+ if (game.turn_option === 'assault')
+ game.state = 'select_assault_battles'
+ else
+ game.state = 'select_battle'
+ }
+}
+
+states.select_assault_battles = {
+ inactive: "combat phase (select assault battles)",
+ prompt() {
+ view.prompt = `Select assault battles.`
+ for (let x of game.active_battles)
+ if (!set_has(game.assault_battles, x))
+ gen_action_hex(x)
+ gen_action_next()
+ },
+ hex(x) {
+ push_undo()
+ set_add(game.assault_battles, x)
+ },
+ next() {
+ push_undo()
+ game.state = 'select_battle'
+ }
+}
+
+states.select_battle = {
+ inactive: "combat phase (select next battle)",
+ prompt() {
+ view.prompt = `Select next battle to resolve.`
+ for (let x of game.active_battles)
+ gen_action_hex(x)
+ if (game.active_battles.length === 0)
+ gen_action('end_combat')
+ },
+ hex(x) {
+ push_undo()
+ game.battle = x
+ goto_defensive_fire()
+ },
+}
+
+function goto_defensive_fire() {
+ set_passive_player()
+ game.fired = []
+ game.state = 'defensive_fire'
+}
+
+function goto_offensive_fire() {
+ set_active_player()
+ game.fired = []
+ game.state = 'offensive_fire'
+}
+
+const xxx_fire = {
+ prompt() {
+ view.prompt = `Fire!`
+
+ let arty = false
+ for (let u = 0; u < units.length; ++u) {
+ if (is_friendly_unit(u) && !is_unit_fired(u) && unit_hex(u) === game.battle) {
+ if (is_artillery_unit(u)) {
+ gen_action_unit(u)
+ arty = true
+ }
+ }
+ }
+
+ if (!arty) {
+ for (let u = 0; u < units.length; ++u) {
+ if (is_friendly_unit(u) && !is_unit_fired(u) && unit_hex(u) === game.battle) {
+ gen_action_unit(u)
+ }
+ }
+ }
+ },
+ unit(who) {
+ clear_undo()
+ set_unit_fired(who)
+
+ let done = true
+ for (let u = 0; u < units.length; ++u)
+ if (is_friendly_unit(u) && !is_unit_fired(u) && unit_hex(u) === game.battle)
+ done = false
+ if (done)
+ end_fire()
+ },
+}
+
+function end_fire() {
+ if (game.state === 'defensive_fire')
+ goto_offensive_fire()
+ else
+ end_combat_phase()
+}
+
+states.defensive_fire = xxx_fire
+states.offensive_fire = xxx_fire
+
// === DEPLOYMENT ===
states.free_deployment = {
@@ -1403,10 +1558,10 @@ states.free_deployment = {
gen_action_next()
},
unit(u) {
- if (game.selected.includes(u))
- remove_from_array(game.selected, u)
+ if (set_has(game.selected, u))
+ set_delete(game.selected, u)
else
- game.selected.push(u)
+ set_add(game.selected, u)
},
hex(x) {
push_undo()
@@ -1414,7 +1569,7 @@ states.free_deployment = {
let u = game.selected[i]
set_unit_hex(u, x)
}
- game.selected.length = 0
+ set_clear(game.selected)
},
next() {
clear_undo()
@@ -1910,10 +2065,9 @@ exports.setup = function (seed, scenario, options) {
axis_sides: [],
allied_sides: [],
- // current turn option and moves
+ // current turn option and selected moves
turn_option: null,
side_limit: {},
- battles: [],
rommel: 0,
from1: 0,
to1: 0,
@@ -1924,6 +2078,14 @@ exports.setup = function (seed, scenario, options) {
move_from: 0,
move_used: 0,
move_road: 4,
+
+ // combat
+ active_battles: [],
+ assault_battles: [],
+ battle: 0,
+ fired: [],
+ hits: null,
+ flash: null,
}
setup(scenario)
@@ -1956,6 +2118,10 @@ exports.view = function(state, current) {
if (game.to1) view.to1 = game.to1
if (game.to2) view.to2 = game.to2
+ if (game.battle) view.battle = game.battle
+ if (game.fired) view.fired = game.fired
+ if (game.flash) view.flash = game.flash
+
return common_view(current)
}