diff options
-rw-r--r-- | play.html | 6 | ||||
-rw-r--r-- | play.js | 56 | ||||
-rw-r--r-- | rules.js | 382 |
3 files changed, 229 insertions, 215 deletions
@@ -542,6 +542,12 @@ svg .hex.tip { <div class="debug menu_item" onclick="send_restart('1941-42')">⚠ Restart 1941-42</div> </div> </div> + <div class="menu"> + <div class="menu_title"><img src="/images/wooden-sign.svg"></div> + <div class="menu_popup"> + <div class="menu_item" onclick="send_query('supply')">Supply lines</div> + </div> + </div> <div class="icon_button" onclick="toggle_units()"><img src="/images/earth-africa-europe.svg"></div> <div class="icon_button" onclick="toggle_zoom()"><img src="/images/magnifying-glass.svg"></div> <div class="icon_button" onclick="toggle_log()"><img src="/images/scroll-quill.svg"></div> @@ -152,7 +152,7 @@ function is_unit_action(unit) { } function is_unit_selected(unit) { - return !!(view.selected && view.selected.includes(unit)) + return view.selected === unit } function is_artillery_unit(u) { @@ -227,16 +227,18 @@ function on_blur(evt) { function on_click_hex(evt) { if (evt.button === 0) { - send_action('hex', evt.target.hex) + hide_supply() + if (send_action('hex', evt.target.hex)) + evt.stopPropagation() } } function on_click_unit(evt) { if (evt.button === 0) { + hide_supply() evt.stopPropagation() if (focus_stack(evt.target.stack)) send_action('unit', evt.target.unit) - return true } } @@ -248,6 +250,7 @@ function on_click_battle_unit(evt) { document.getElementById("map").addEventListener("mousedown", function (evt) { if (evt.button === 0) { + hide_supply() blur_stack() } }) @@ -277,6 +280,38 @@ function toggle_units() { document.getElementById("units").classList.toggle("hide") } +let showing_supply = false + +function show_supply(reply) { + showing_supply = true + view.axis_supply = reply.axis_supply + view.axis_supply_line = reply.axis_supply_line + view.allied_supply = reply.allied_supply + view.allied_supply_line = reply.allied_supply_line + for (let x of all_hexes) { + ui.hexes[x].classList.toggle("axis_supply", is_hex_axis_supply(x)) + for (let s = 0; s < 3; ++s) + ui.sides[x*3+s].classList.toggle("axis_supply", is_side_axis_supply_line(x*3+s)) + ui.hexes[x].classList.toggle("allied_supply", is_hex_allied_supply(x)) + for (let s = 0; s < 3; ++s) + ui.sides[x*3+s].classList.toggle("allied_supply", is_side_allied_supply_line(x*3+s)) + } +} + +function hide_supply() { + if (showing_supply) { + showing_supply = false + for (let x of all_hexes) { + ui.hexes[x].classList.toggle("axis_supply", false) + ui.hexes[x].classList.toggle("allied_supply", false) + for (let s = 0; s < 3; ++s) { + ui.sides[x*3+s].classList.toggle("axis_supply", false) + ui.sides[x*3+s].classList.toggle("allied_supply", false) + } + } + } +} + const CLEAR = 2 const PASS = 1 const ROUGH = 0 @@ -487,16 +522,6 @@ function update_map() { ui.hexes[hex].classList.toggle("to", hex === view.to1 || hex === view.to2) ui.hexes[hex].classList.toggle("axis_control", is_hex_axis_controlled(hex)) ui.hexes[hex].classList.toggle("allied_control", is_hex_allied_controlled(hex)) - if (view.axis_supply) { - ui.hexes[hex].classList.toggle("axis_supply", is_hex_axis_supply(hex)) - for (let s = 0; s < 3; ++s) - ui.sides[hex*3+s].classList.toggle("axis_supply", is_side_axis_supply_line(hex*3+s)) - } - if (view.allied_supply) { - ui.hexes[hex].classList.toggle("allied_supply", is_hex_allied_supply(hex)) - for (let s = 0; s < 3; ++s) - ui.sides[hex*3+s].classList.toggle("allied_supply", is_side_allied_supply_line(hex*3+s)) - } for (let s = 0; s < 3; ++s) { ui.sides[hex*3+s].classList.toggle("axis_control", is_side_axis_controlled(hex*3+s)) ui.sides[hex*3+s].classList.toggle("allied_control", is_side_allied_controlled(hex*3+s)) @@ -623,6 +648,11 @@ function on_update() { action_button("undo", "Undo") } +function on_reply(q, params) { + if (q === 'supply') + show_supply(params) +} + function on_focus_hex_tip(x) { ui.hexes[x].classList.add("tip") } @@ -231,6 +231,19 @@ function is_hex_or_adjacent_to(x, where) { // === UNIT STATE === +function apply_select(u) { + if (game.selected === u) + game.selected = -1 + else + game.selected = u +} + +function pop_selected() { + let u = game.selected + game.selected = -1 + return u +} + const UNIT_DISRUPTED_SHIFT = 0 const UNIT_DISRUPTED_MASK = 1 << UNIT_DISRUPTED_SHIFT @@ -592,6 +605,13 @@ function for_each_undisrupted_friendly_unit_in_hex(x, fn) { fn(u) } +function for_each_undisrupted_friendly_unit_in_hex_or_adjacent(x, fn) { + // TODO: first/last_enemy_unit + for (let u = 0; u < units.length; ++u) + if (is_friendly_unit(u) && !is_unit_disrupted(u) && is_hex_or_adjacent_to(unit_hex(u), x)) + fn(u) +} + function for_each_enemy_unit_in_hex(x, fn) { // TODO: first/last_enemy_unit for (let u = 0; u < units.length; ++u) @@ -1478,100 +1498,12 @@ function goto_move() { game.state = 'move_who' } -function slowest_speed_of_selected_units() { - let r = 4 - for (let u of game.selected) { - let s = unit_speed(u) - if (s < r) - r = s - } - return r -} -function gen_move_who() { +function gen_move(search_fn) { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 - - // Deselect - for (let u of game.selected) - gen_action_unit(u) - - // Select Group Move 1 - if (!game.to1 && game.from1) { - if (game.selected.length === 0 || unit_hex(game.selected[0]) === game.from1) { - if (!is_battle_hex(game.from1)) { - for_each_friendly_unit(u => { - if (!is_unit_disrupted(u) && !is_unit_moved(u) && !set_has(game.selected, u)) - if (unit_hex(u) === game.from1) - gen_action_unit(u) - }) - } - } - } - - // Select Group Move 2 - if (!game.to2 && game.from2) { - if (game.selected.length === 0 || unit_hex(game.selected[0]) === game.from2) { - if (!is_battle_hex(game.from2)) { - for_each_friendly_unit(u => { - if (!is_unit_disrupted(u) && !is_unit_moved(u) && !set_has(game.selected, u)) - if (unit_hex(u) === game.from2) - gen_action_unit(u) - }) - } - } - } - - function overlap(x, f, t) { - return t ? is_hex_or_adjacent_to(x, f) : x === f - } - - // Select Regroup Move 1 - if (game.to1) { - // Multi-select if it does not engage and is an unambigously regroup move 1. - if (game.selected.length === 0 || (!has_enemy_unit(game.to1) && overlap(unit_hex(game.selected[0]), game.from1, game.to1) && !overlap(unit_hex(game.selected[0]), game.from2, game.to2))) { - search_move(game.to1, 4 + rommel1) - for_each_friendly_unit(u => { - if (!is_unit_disrupted(u) && !is_unit_moved(u) && !set_has(game.selected, u)) { - let x = unit_hex(u) - if (is_hex_or_adjacent_to(x, game.from1)) - if (game.selected.length === 0 || !overlap(x, game.from2, game.to2)) - if (can_move_to(game.to1, unit_speed(u) + rommel1)) - if (!is_battle_hex(x)) - gen_action_unit(u) - } - }) - } - } - - // Select Regroup Move 2 - if (game.to2) { - // Multi-select if it does not engage and is unambigously regroup move 2. - if (game.selected.length === 0 || (!has_enemy_unit(game.to2) && !overlap(unit_hex(game.selected[0]), game.from1, game.to1) && overlap(unit_hex(game.selected[0]), game.from2, game.to2))) { - search_move(game.to2, 4 + rommel2) - for_each_friendly_unit(u => { - if (!is_unit_disrupted(u) && !is_unit_moved(u) && !set_has(game.selected, u)) { - let x = unit_hex(u) - if (is_hex_or_adjacent_to(x, game.from2)) - if (game.selected.length === 0 || !overlap(x, game.from1, game.to1)) - if (can_move_to(game.to2, unit_speed(u) + rommel2)) - if (!is_battle_hex(x)) - gen_action_unit(u) - } - }) - } - } -} - -function gen_move_to(search_fn) { - let rommel1 = (game.rommel === 1) ? 1 : 0 - let rommel2 = (game.rommel === 2) ? 1 : 0 - let speed = slowest_speed_of_selected_units() - - // NOTE: During normal regroup moves, we check that we can reach - // the destination before allowing multi-select. - // During retreats, we only move from one hex at a time. - let from = unit_hex(game.selected[0]) + let speed = unit_speed(game.selected) + let from = unit_hex(game.selected) search_fn(from, max(speed + rommel1, speed + rommel2)) @@ -1579,9 +1511,7 @@ function gen_move_to(search_fn) { if (from === game.from1) { for (let to of all_hexes) if (to != from && can_move_to(to, speed + rommel1)) - // no multi-unit engagement moves - if (game.selected.length === 1 || !has_enemy_unit(to)) - gen_action_hex(to) + gen_action_hex(to) } } @@ -1589,9 +1519,7 @@ function gen_move_to(search_fn) { if (from === game.from2) { for (let to of all_hexes) if (to != from && can_move_to(to, speed + rommel2)) - // no multi-unit engagement moves - if (game.selected.length === 1 || !has_enemy_unit(to)) - gen_action_hex(to) + gen_action_hex(to) } } @@ -1611,11 +1539,9 @@ function gen_move_to(search_fn) { function apply_move(to) { let rommel1 = (game.rommel === 1) ? 1 : 0 let rommel2 = (game.rommel === 2) ? 1 : 0 - let speed = slowest_speed_of_selected_units() - let from = unit_hex(game.selected[0]) - - let list = game.selected - game.selected = [] + let who = pop_selected() + let from = unit_hex(who) + let speed = unit_speed(who) push_undo() @@ -1625,28 +1551,28 @@ function apply_move(to) { search_move(from, max(speed + rommel1, speed + rommel2)) - for (let who of list) { - if (!game.to1 && game.from1) { - if (from === game.from1) + if (!game.to1 && game.from1) { + if (from === game.from1) + if (can_move_to(to, speed + rommel1)) move_unit(rommel1, who, from, to) - } + } - if (!game.to2 && game.from2) { - if (from === game.from2) + if (!game.to2 && game.from2) { + if (from === game.from2) + if (can_move_to(to, speed + rommel2)) move_unit(rommel2, who, from, to) - } + } - if (game.to1) { - if (is_hex_or_adjacent_to(from, game.from1)) - if (can_move_to(game.to1, speed + rommel1)) - move_unit(rommel1, who, from, to) - } + if (game.to1) { + if (is_hex_or_adjacent_to(from, game.from1)) + if (can_move_to(game.to1, speed + rommel1)) + move_unit(rommel1, who, from, to) + } - if (game.to2) { - if (is_hex_or_adjacent_to(from, game.from2)) - if (can_move_to(game.to2, speed + rommel2)) - move_unit(rommel2, who, from, to) - } + if (game.to2) { + if (is_hex_or_adjacent_to(from, game.from2)) + if (can_move_to(game.to2, speed + rommel2)) + move_unit(rommel2, who, from, to) } } @@ -1681,39 +1607,88 @@ states.move_who = { prompt() { view.prompt = `Move: Select unit to move.` - gen_move_who() + let rommel1 = (game.rommel === 1) ? 1 : 0 + let rommel2 = (game.rommel === 2) ? 1 : 0 - if (game.selected.length > 0) { + if (game.selected < 0) { + + // Select Group Move 1 + if (!game.to1 && game.from1) { + if (!is_battle_hex(game.from1)) { + for_each_undisrupted_friendly_unit_in_hex(game.from1, u => { + if (!is_unit_moved(u)) + gen_action_unit(u) + }) + } + } + + // Select Group Move 2 + if (!game.to2 && game.from2) { + if (!is_battle_hex(game.from2)) { + for_each_undisrupted_friendly_unit_in_hex(game.from2, u => { + if (!is_unit_moved(u)) + gen_action_unit(u) + }) + } + } + + // Select Regroup Move 1 + if (game.to1) { + search_move(game.to1, 4 + rommel1) + for_each_undisrupted_friendly_unit_in_hex_or_adjacent(game.from1, u => { + if (!is_unit_moved(u) && can_move_to(game.to1, unit_speed(u) + rommel1)) + if (!is_battle_hex(unit_hex(u))) + gen_action_unit(u) + }) + } + + // Select Regroup Move 2 + if (game.to1) { + search_move(game.to2, 4 + rommel1) + for_each_undisrupted_friendly_unit_in_hex_or_adjacent(game.from2, u => { + if (!is_unit_moved(u) && can_move_to(game.to2, unit_speed(u) + rommel2)) + if (!is_battle_hex(unit_hex(u))) + gen_action_unit(u) + }) + } + + // Retreat + let has_retreat_hex = false + if (!game.to1 && game.from1 && is_battle_hex(game.from1)) + has_retreat_hex = true + if (!game.to2 && game.from2 && is_battle_hex(game.from2)) + has_retreat_hex = true + if (game.to1 && !has_enemy_unit(game.to1)) { + for_each_hex_and_adjacent_hex(game.from1, x => { + if (has_undisrupted_friendly_unit(x) && is_battle_hex(x)) + has_retreat_hex = true + }) + } + if (game.to2 && !has_enemy_unit(game.to2)) { + for_each_hex_and_adjacent_hex(game.from2, x => { + if (has_undisrupted_friendly_unit(x) && is_battle_hex(x)) + has_retreat_hex = true + }) + } + if (has_retreat_hex) + gen_action('retreat') + + gen_action('end_move') + } else { + + // Deselect + gen_action_unit(game.selected) + + // Move if (game.turn_option === 'pass') - gen_move_to(search_withdraw_normal) + gen_move(search_withdraw) else - gen_move_to(search_move) - } + gen_move(search_move) - let has_retreat_hex = false - if (!game.to1 && game.from1 && is_battle_hex(game.from1)) - has_retreat_hex = true - if (!game.to2 && game.from2 && is_battle_hex(game.from2)) - has_retreat_hex = true - if (game.to1 && !has_enemy_unit(game.to1)) { - for_each_hex_and_adjacent_hex(game.from1, x => { - if (has_undisrupted_friendly_unit(x) && is_battle_hex(x)) - has_retreat_hex = true - }) } - if (game.to2 && !has_enemy_unit(game.to2)) { - for_each_hex_and_adjacent_hex(game.from2, x => { - if (has_undisrupted_friendly_unit(x) && is_battle_hex(x)) - has_retreat_hex = true - }) - } - if (has_retreat_hex) - gen_action('retreat') - - gen_action('end_move') }, unit(who) { - set_toggle(game.selected, who) + apply_select(who) }, hex(to) { apply_move(to) @@ -1871,9 +1846,9 @@ states.retreat_who = { if (game.selected.length > 0) { if (game.turn_option === 'pass') - gen_move_to(search_withdraw_retreat) + gen_move(search_withdraw_retreat) else - gen_move_to(search_move_retreat) + gen_move(search_move_retreat) } }, unit(who) { @@ -1933,20 +1908,20 @@ states.refuse_battle = { } } -states.refuse_battle_who = { +states.refuse_battle_withdraw = { inactive: "refuse battle (withdraw group move)", prompt() { view.prompt = `Withdraw: Select unit to move.` - let done = true - for_each_undisrupted_friendly_unit_in_hex(game.from1, u => { - gen_action_unit(u) - done = false - }) - if (done) - gen_action('end_retreat') - - if (game.selected.length > 0) { - let speed = slowest_speed_of_selected_units() + if (game.selected < 0) { + let done = true + for_each_undisrupted_friendly_unit_in_hex(game.from1, u => { + gen_action_unit(u) + done = false + }) + if (done) + gen_action('end_retreat') + } else { + let speed = unit_speed(game.selected) search_withdraw_retreat(game.from1, speed) for (let to of all_hexes) if (to != game.from1 && can_move_to(to, speed)) @@ -1954,17 +1929,14 @@ states.refuse_battle_who = { } }, unit(who) { - set_toggle(game.selected, who) + apply_select(who) }, hex(to) { - let list = game.selected - game.selected = [] + let who = pop_selected() push_undo() - for (let who of list) { - log(`>to #${to}`) - set_unit_hex(who, to) - set_unit_disrupted(who) - } + log(`>to #${to}`) + set_unit_hex(who, to) + set_unit_disrupted(who) }, end_retreat() { clear_undo() @@ -2453,7 +2425,7 @@ function end_pursuit_fire() { if (game.retreat) { game.state = 'retreat_who' } else { - game.state = 'refuse_battle_who' + game.state = 'refuse_battle_withdraw' } } @@ -2464,52 +2436,39 @@ states.free_deployment = { prompt() { let scenario = SCENARIOS[game.scenario] let deploy = hexdeploy + scenario.start + let axis = (game.active === AXIS) let done = true - if (game.active === AXIS) - view.prompt = "Axis Free Deployment" - else - view.prompt = "Allied Free Deployment" + view.prompt = axis ? "Axis Free Deployment" : "Allied Free Deployment" - // TODO: first/last_axis_unit - for (let u = 0; u < units.length; ++u) { - if (is_friendly_unit(u)) { - if (unit_hex(u) === deploy) { - done = false - gen_action_unit(u) + if (game.selected < 0) { + for_each_friendly_unit_in_hex(deploy, u => { + gen_action_unit(u) + done = false + }) + } else { + for (let x of (axis ? scenario.axis_deployment : scenario.allied_deployment)) { + if (!is_enemy_hex(x)) { + let limit = scenario.deployment_limit[x] | 0 + if (!limit || count_friendly_units_in_hex(x) < limit) + gen_action_hex(x) } } - } - - if (game.selected.length > 0) { - let list - if (game.active === AXIS) - list = scenario.axis_deployment - else - list = scenario.allied_deployment - // TODO: scenario.deployment_limit - for (let i = 0; i < list.length; ++i) - if (!is_enemy_hex(list[i])) - gen_action_hex(list[i]) + gen_action_unit(game.selected) + done = false } if (done) gen_action_next() }, unit(u) { - if (set_has(game.selected, u)) - set_delete(game.selected, u) - else - set_add(game.selected, u) + apply_select(u) }, - hex(x) { + hex(to) { + let who = pop_selected() push_undo() - log(`Deployed ${game.selected.length} at #${x}.`) - for (let i = 0; i < game.selected.length; ++i) { - let u = game.selected[i] - set_unit_hex(u, x) - } - set_clear(game.selected) + log(`Deployed at #${to}.`) + set_unit_hex(who, to) }, next() { clear_undo() @@ -2593,6 +2552,7 @@ const SCENARIOS = { allied_deployment: region_egypt, axis_initial_supply: 6, allied_initial_supply: 3, + deployment_limit: {}, special: { no_rommel_bonus: true, only_one_die_for_buildup: true, @@ -2606,6 +2566,7 @@ const SCENARIOS = { allied_deployment: region_egypt_and_libya, axis_initial_supply: 6, allied_initial_supply: 6, + deployment_limit: {}, }, "Crusader": { year: 1941, @@ -2656,6 +2617,7 @@ const SCENARIOS = { allied_deployment: regions["East Line"], axis_initial_supply: 10, allied_initial_supply: 12, + deployment_limit: {}, special: { gazala_pre_build: true, } @@ -2668,6 +2630,7 @@ const SCENARIOS = { allied_deployment: regions["Egypt"], axis_initial_supply: 8, allied_initial_supply: 8, + deployment_limit: {}, }, "1941-42": { year: 1941, @@ -2677,6 +2640,7 @@ const SCENARIOS = { allied_deployment: region_egypt_and_libya, axis_initial_supply: 6, allied_initial_supply: 6, + deployment_limit: {}, }, } @@ -2979,7 +2943,7 @@ function setup(name) { log_h2("Axis Deployment") game.active = 'Axis' game.state = 'free_deployment' - game.selected = [] + game.selected = -1 } // === PUBLIC FUNCTIONS === @@ -2997,7 +2961,7 @@ exports.setup = function (seed, scenario, options) { state: null, phasing: AXIS, active: AXIS, - selected: null, + selected: -1, scenario: scenario, month: 0, @@ -3098,6 +3062,20 @@ exports.view = function(state, current) { return common_view(current) } +exports.query = function (state, current, q) { + if (q === 'supply') { + game = state + return { + axis_supply: game.axis_supply, + axis_supply_line: game.axis_supply_line, + allied_supply: game.allied_supply, + allied_supply_line: game.allied_supply_line, + } + } + return null +} + + function gen_action_next() { gen_action('next') } |