diff options
-rw-r--r-- | play.js | 2 | ||||
-rw-r--r-- | rules.js | 294 |
2 files changed, 238 insertions, 58 deletions
@@ -629,6 +629,7 @@ function on_update() { action_button("overrun", "Overrun") action_button("rommel", "Rommel") + action_button("eliminate", "Eliminate") action_button("retreat", "Retreat") action_button("probe", "Probe") @@ -643,6 +644,7 @@ function on_update() { action_button("next", "Next") action_button("end_move", "End move") + action_button("end_rout", "End rout") action_button("end_retreat", "End retreat") action_button("end_combat", "End combat") action_button("end_turn", "End turn") @@ -1656,39 +1656,29 @@ function apply_move(to) { push_undo() + console.log("apply_move", hex_name[to]) + if (has_enemy_unit(to)) { // TODO: pick hex-side manually when engaging } search_move(from, max(speed + rommel1, speed + rommel2)) - if (!game.to1 && game.from1) { - if (from === game.from1) { - for (let to of all_hexes) - if (to != from && can_move_to(to, speed + rommel1)) - return move_unit(who, to, speed + rommel1) - } - } + if (!game.to1 && game.from1 === from) + if (can_move_to(to, speed + rommel1)) + return move_unit(who, to, speed + rommel1) - if (!game.to2 && game.from2) { - if (from === game.from2) { - for (let to of all_hexes) - if (to != from && can_move_to(to, speed + rommel2)) - return move_unit(who, to, speed + rommel2) - } - } + if (!game.to2 && game.from2 === from) + if (can_move_to(to, speed + rommel2)) + return move_unit(who, to, speed + rommel2) - if (game.to1 === to) { - if (is_hex_or_adjacent_to(from, game.from1)) - if (can_move_to(game.to1, speed + rommel1)) - return move_unit(who, to, speed + rommel1) - } + if (game.to1 === to && is_hex_or_adjacent_to(from, game.from1)) + if (can_move_to(to, speed + rommel1)) + return move_unit(who, to, speed + rommel1) - if (game.to2 === to) { - if (is_hex_or_adjacent_to(from, game.from2)) - if (can_move_to(game.to2, speed + rommel2)) - return move_unit(who, to, speed + rommel2) - } + if (game.to2 === to && is_hex_or_adjacent_to(from, game.from2)) + if (can_move_to(to, speed + rommel2)) + return move_unit(who, to, speed + rommel2) } function move_unit(who, to, speed) { @@ -1752,6 +1742,8 @@ states.retreat_from = { view.prompt = `Retreat: Select hex to retreat from.` if (game.from1) { if (game.to1) { + // TODO: only hexes with units that can reach regroup destination + // TODO: if full retreat required, only allow hexes where all can reach regroup destination for_each_hex_and_adjacent_hex(game.from1, x => { if (is_battle_hex(x) && !set_has(game.partial_retreats, x)) gen_action_hex(x) @@ -1763,6 +1755,8 @@ states.retreat_from = { } if (game.from2) { if (game.to2) { + // TODO: only hexes with units that can reach regroup destination + // TODO: if full retreat required, only allow hexes where all can reach regroup destination for_each_hex_and_adjacent_hex(game.from2, x => { if (is_battle_hex(x) && !set_has(game.partial_retreats, x)) gen_action_hex(x) @@ -1786,6 +1780,7 @@ states.retreat_who = { view.prompt = `Retreat: Select units to retreat.` let full_retreat = true for_each_undisrupted_friendly_unit_in_hex(game.retreat, u => { + // TODO: can reach regroup destination? if (!set_has(game.retreat_units, u)) full_retreat = false gen_action_unit(u) @@ -1944,7 +1939,7 @@ states.refuse_battle = { states.refuse_battle_move = { inactive: "refuse battle (withdraw group move)", prompt() { - view.prompt = `Withdraw: Select unit to move.` + view.prompt = `Refuse Battle: Withdraw units.` if (game.selected < 0) { let done = true for_each_undisrupted_friendly_unit_in_hex(game.from1, u => { @@ -1955,6 +1950,7 @@ states.refuse_battle_move = { gen_action('end_retreat') } else { let speed = unit_speed(game.selected) + gen_action_unit(game.selected) search_withdraw_retreat(game.from1, speed) for (let to of all_hexes) if (to != game.from1 && can_move_to(to, speed)) @@ -1979,6 +1975,102 @@ states.refuse_battle_move = { } } +// === ROUT === + +// rout attrition +// pursuit fire +// withdraw by group move +// eliminated if cannot + +function goto_rout(from, side) { + // save old state so we can recover + game.rout = { + state: game.state, + active: game.active, + from: from, + attrition: [], + } + + if (side !== game.active) + set_enemy_player() + + game.pursuit = from + game.state = 'rout_attrition' +} + +states.rout_attrition = { + prompt() { + view.prompt = "Rout: Suffer attrition!" + for_each_friendly_unit_in_hex(game.rout.from, u => { + if (!set_has(game.rout.attrition, u)) + gen_action_unit(u) + }) + }, + unit(who) { + reduce_unit(who) + set_add(game.rout.attrition, who) + let done = true + for_each_friendly_unit_in_hex(game.rout.from, u => { + if (!set_has(game.rout.attrition, u)) + done = false + }) + if (done) { + goto_pursuit_fire_during_rout() + delete game.rout.attrition + } + }, +} + +states.rout_move = { + prompt() { + view.prompt = `Rout: Withdraw units.` + if (game.selected < 0) { + let done = true + for_each_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) + let eliminate = true + search_withdraw_retreat(game.rout.from, speed) + for (let to of all_hexes) { + if (to != game.rout.from && can_move_to(to, speed)) { + gen_action_hex(to) + eliminate = false + } + } + if (eliminate) + gen_action('eliminate') + } + }, + unit(who) { + apply_select(who) + }, + eliminate() { + let who = pop_selected() + push_undo() + log(`>eliminated`) + eliminate_unit(who) + }, + hex(to) { + let who = pop_selected() + push_undo() + log(`>to #${to}`) + set_unit_hex(who, to) + set_unit_disrupted(who) + }, + end_rout() { + clear_undo() + game.state = game.rout.state + if (game.active !== game.rout.active) + set_active_enemy() + delete game.rout + } +} + // ==== COMBAT PHASE === function goto_combat_phase() { @@ -2375,8 +2467,17 @@ states.probe_hits = { // routing apply hits // routing moves +function goto_rout_fire(where) { + clear_undo() + game.hits = 0 + game.pursuit = where + if (can_rout_fire(true)) + game.state = 'rout_fire' + else + goto_rout_hits() +} + function goto_pursuit_fire_during_retreat(where) { -console.log("goto_pursuit_fire_during_retreat", where) clear_undo() set_passive_player() game.hits = 0 @@ -2398,16 +2499,30 @@ function goto_pursuit_fire_during_refuse_battle(where) { goto_pursuit_hits() } +function goto_rout_hits() { + set_enemy_player() + if (game.hits > 0) + game.state = 'rout_hits' + else + end_rout_fire() +} + function goto_pursuit_hits() { set_enemy_player() - if (game.hits > 0) { - let hp = count_hp_in_pursuit() - if (game.hits > hp) - game.hits = hp + if (game.hits > 0) game.state = 'pursuit_hits' - } else { + else end_pursuit_fire() - } +} + +function slowest_enemy_unit_speed(where) { + let r = 4 + for_each_enemy_unit_in_hex(where, u => { + let s = unit_speed(u) + if (s < r) + r = s + }) + return r } function slowest_undisrupted_enemy_unit_speed(where) { @@ -2420,6 +2535,18 @@ function slowest_undisrupted_enemy_unit_speed(where) { return r } +function can_rout_fire(verbose) { + let result = false + let slowest = slowest_enemy_unit_speed(game.pursuit) + if (verbose) + log(`Slowest routing unit was ${speed_name[slowest]} ${game.active} at #${game.pursuit}.`) + for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { + if (unit_speed(u) >= slowest && !is_unit_fired(u)) + result = true + }) + return result +} + function can_pursuit_fire(verbose) { let result = false let slowest = slowest_undisrupted_enemy_unit_speed(game.pursuit) @@ -2432,7 +2559,7 @@ function can_pursuit_fire(verbose) { return result } -function roll_pursuit_fire(who, n) { +function roll_pursuit_fire_imp(who, n, hp) { if (n === 2) { let a = random(6) + 1 let b = random(6) + 1 @@ -2450,13 +2577,19 @@ function roll_pursuit_fire(who, n) { if (a >= 4) game.hits++ } - - let hp = count_hp_in_pursuit() if (game.hits > hp) game.hits = hp return game.hits === hp } +function roll_pursuit_fire(who, n) { + return roll_pursuit_fire_imp(who, n, count_hp_in_pursuit()) +} + +function roll_rout_fire(who, n) { + return roll_pursuit_fire_imp(who, n, count_hp_in_rout()) +} + states.pursuit_fire = { inactive: "pursuit fire (fire)", prompt() { @@ -2481,36 +2614,75 @@ states.pursuit_fire = { } } +states.rout_fire = { + inactive: "rout fire (fire)", + prompt() { + view.prompt = `Pursuit Fire (Rout).` + let slowest = slowest_enemy_unit_speed(game.pursuit) + for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { + if (unit_speed(u) >= slowest && !is_unit_fired(u)) + gen_action_unit(u) + }) + }, + unit(who) { + let slowest = slowest_enemy_unit_speed(game.pursuit) + set_unit_fired(who) + let done = roll_rout_fire(who, (unit_speed(who) > slowest ? 2 : 1)) + if (done || !can_rout_fire(false)) + goto_rout_hits() + }, +} + +function gen_pursuit_hits(normal_steps, elite_steps, iterate) { + let done = true + iterate(game.pursuit, u => { + if (is_unit_elite(u)) { + if (game.hits >= 2) { + gen_action_unit(u) + done = false + } + } else { + if (game.hits >= 1) { + // If mixed elite and non-elite: must assign ALL damage. + if (elite_steps > 0 && normal_steps === 1 && (game.hits & 1) === 0) { + // Eliminating the last non-elite must not leave an odd + // number of hits remaining. + } else { + gen_action_unit(u) + done = false + } + } + } + }) + if (done) + gen_action('next') +} + states.pursuit_hits = { inactive: "pursuit fire (hits)", prompt() { view.prompt = `Pursuit Fire: Apply ${game.hits} hits.` - let normal_steps = count_normal_steps_in_pursuit() let elite_steps = count_elite_steps_in_pursuit() + gen_pursuit_hits(normal_steps, elite_steps, for_each_undisrupted_friendly_unit_in_hex) + }, + unit(who) { + push_undo() + game.hits -= reduce_unit(who) + }, + next() { + clear_undo() + end_pursuit_fire() + }, +} - let done = true - for_each_undisrupted_friendly_unit_in_hex(game.pursuit, u => { - if (is_unit_elite(u)) { - if (game.hits >= 2) { - gen_action_unit(u) - done = false - } - } else { - if (game.hits >= 1) { - // If mixed elite and non-elite: must assign ALL damage. - if (elite_steps > 0 && normal_steps === 1 && (game.hits & 1) === 0) { - // Eliminating the last non-elite must not leave an odd - // number of hits remaining. - } else { - gen_action_unit(u) - done = false - } - } - } - }) - if (done) - gen_action('next') +states.rout_hits = { + inactive: "rout fire (hits)", + prompt() { + view.prompt = `Pursuit Fire (Rout): Apply ${game.hits} hits.` + let normal_steps = count_normal_steps_in_rout() + let elite_steps = count_elite_steps_in_rout() + gen_pursuit_hits(normal_steps, elite_steps, for_each_friendly_unit_in_hex) }, unit(who) { push_undo() @@ -2532,6 +2704,12 @@ function end_pursuit_fire() { } } +function end_rout_fire() { + game.from1 = game.pursuit + game.pursuit = 0 + game.state = 'rout_move' +} + // === DEPLOYMENT === states.free_deployment = { |