diff options
-rw-r--r-- | play.html | 97 | ||||
-rw-r--r-- | play.js | 154 | ||||
-rw-r--r-- | rules.js | 200 |
3 files changed, 414 insertions, 37 deletions
@@ -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"> @@ -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") @@ -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) } |