summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2023-01-08 00:38:45 +0100
committerTor Andersson <tor@ccxvii.net>2023-02-18 13:02:38 +0100
commit3d98a7e7f83d06b4a2c45cb76510064e93af413e (patch)
treef791838a1bb45a58735e98f1de94b87dcbf926fa
parent828518a70b392b7e1c60fb4e50af3553bd3bd155 (diff)
downloadnevsky-3d98a7e7f83d06b4a2c45cb76510064e93af413e.tar.gz
Total Strike rewrite.
-rw-r--r--play.html4
-rw-r--r--play.js12
-rw-r--r--rules.js1375
3 files changed, 707 insertions, 684 deletions
diff --git a/play.html b/play.html
index 23ab115..1f81845 100644
--- a/play.html
+++ b/play.html
@@ -110,6 +110,10 @@ body.Teutons #plan_actions .russian { display: none }
/* BATTLE GRID WITH LORD MATS */
+#battle_panel {
+ width: 1275px;
+}
+
#battle_grid {
display: grid;
gap: 18px;
diff --git a/play.js b/play.js
index 0f1720a..bdc9a8a 100644
--- a/play.js
+++ b/play.js
@@ -1,6 +1,9 @@
"use strict"
-// TODO: Feed x2 markers on lord mats with >6 units
+// Feed x2 on lords with >6 units
+// End marker on Calendar
+// Remove battle mat.
+// Siege/Walls/Garrison display in battle array.
// TODO: show sg and hg highlighting on battle mat (separate from view.group)
@@ -1508,7 +1511,12 @@ function on_update() {
if (view.battle && view.battle.array) {
ui.battle_panel.classList.remove("hide")
- ui.battle_header.textContent = "~ Battle at " + data.locales[view.battle.where].name + " ~"
+ if (view.battle.storm)
+ ui.battle_header.textContent = "Storm at " + data.locales[view.battle.where].name
+ else if (view.battle.sally)
+ ui.battle_header.textContent = "Sally at " + data.locales[view.battle.where].name
+ else
+ ui.battle_header.textContent = "Battle at " + data.locales[view.battle.where].name
if (view.battle.attacker === player) {
ui.battle_grid.className = "attacker"
} else {
diff --git a/rules.js b/rules.js
index 3d7fe47..2d0f505 100644
--- a/rules.js
+++ b/rules.js
@@ -1,6 +1,5 @@
"use strict"
-// TODO: Strike hit overflow
// TODO: Bridge - kn, sgt, 1x lh, maa, militia, serf, lh, ah
// TODO: Lodya capability during supply!
// TODO: 2nd edition supply rule - no reuse of transports
@@ -26,11 +25,6 @@
const data = require("./data.js")
-// Packed strike and hit group lookup table.
-const GROUPS = [[[0,0,0,0,0,0,0,0,0,[[8,1]],[[8,2]],[[8,3]],[[8,4]],[[8,5]],[[8,2]],[[8,7]],0,[[16,1]],[[16,2]],[[16,3]],[[16,4]],[[16,1]],[[16,6]],[[16,7]],0,[[24,1]],[[24,2]],[[8,1],[16,2]],[[24,4]],[[24,1]],[[24,2]],[[8,1],[16,6]],0,[[32,1]],[[32,2]],[[32,2]],[[32,4]],[[32,5]],[[32,6]],[[32,7]],0,[[40,1]],[[40,2]],[[8,3],[32,2]],[[40,4]],[[8,1],[32,4]],[[8,2],[32,6]],[[8,3],[32,6]],0,[[48,1]],[[48,2]],[[48,2]],[[48,4]],[[16,1],[32,4]],[[16,2],[32,4]],[[16,3],[32,4]],0,[[56,1]],[[56,2]],[[8,1],[48,2]],[[56,4]],[[24,1],[32,4]],[[24,2],[32,4]],[[8,1],[16,2],[32,4]]],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[[16,4]],0,0,0,0,0,0,0,[[8,1],[16,4]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[[48,4]],0,0,0,0,0,0,0,[[8,1],[48,4]],0,0]],[[0,0,0,0,0,0,0,0,0,[[1,8]],[[2,8]],[[3,8]],[[4,8]],[[5,8]],[[6,8]],[[7,8]],0,[[1,16]],[[2,16]],[[3,16]],[[4,16]],[[5,16]],[[6,16]],[[7,16]],0,[[1,24]],[[2,24]],[[1,8],[2,16]],[[4,16]],[[1,24],[4,16]],[[6,16]],[[1,8],[6,16]],0,[[1,32]],[[2,32]],[[3,32]],[[4,32]],[[5,32]],[[6,32]],[[7,32]],0,[[1,40]],[[2,8]],[[3,8]],[[4,40]],[[1,8],[4,32]],[[2,8],[4,32]],[[3,8],[4,32]],0,[[1,16]],[[2,48]],[[3,16]],[[4,48]],[[1,16],[4,48]],[[2,16],[4,32]],[[3,16],[4,32]],0,[[1,56]],[[2,56]],[[1,8],[2,48]],[[4,56]],[[1,24],[4,48]],[[2,24],[4,32]],[[1,8],[2,16],[4,32]]],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[[2,32]],[[1,8],[2,32]],0,0,[[6,32]],[[1,8],[6,32]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]]
-
-const TODO = false
-
const BOTH = "Both"
const TEUTONS = "Teutons"
const RUSSIANS = "Russians"
@@ -81,9 +75,7 @@ const scenario_last_turn = {
}
function should_remove_no_event_card() {
- if (game.scenario === "Crusade on Novgorod")
- return current_turn() >= 9
- return true
+ return game.scenario !== "Crusade on Novgorod"
}
// unit types
@@ -124,11 +116,11 @@ const RD1 = 9 // relief sally: reserve defenders
const RD2 = 10
const RD3 = 11
-const battle_array_name = [
- "A1", "A2", "A3",
- "D1", "D2", "D3",
- "SA1", "SA2", "SA3",
- "RD1", "RD2", "RD3",
+const ARRAY_FLANKS = [
+ [ A2, A3 ], [ A1, A3 ], [ A1, A2 ],
+ [ D2, D3 ], [ D1, D3 ], [ D1, D2 ],
+ [ SA2, SA3 ], [ SA1, SA3 ], [ SA1, SA2 ],
+ [ RD2, RD3 ], [ RD1, RD3 ], [ RD1, RD2 ],
]
function find_card(name) {
@@ -1901,6 +1893,14 @@ function setup_test() {
setup_pleskau_quickstart()
set_add(game.capabilities, AOW_TEUTONIC_RANSOM)
set_add(game.capabilities, AOW_RUSSIAN_RANSOM)
+
+ set_lord_locale(LORD_KNUD_ABEL, LOC_ODENPAH)
+ set_lord_locale(LORD_HERMANN, LOC_ODENPAH)
+ set_lord_locale(LORD_RUDOLF, LOC_ODENPAH)
+
+ set_lord_locale(LORD_DOMASH, LOC_PSKOV)
+ set_lord_locale(LORD_VLADISLAV, LOC_IZBORSK)
+
for (let c = first_p1_card; c <= last_p1_card; ++c)
if (data.cards[c].when === "hold")
game.hand1.push(c)
@@ -1930,7 +1930,7 @@ states.setup_lords = {
push_undo()
log(`L${lord} at %${get_lord_locale(lord)}`)
- // TODO: clean up these transitions
+ // FIXME: clean up these transitions
push_state("muster_lord_transport")
set_lord_moved(lord, 1)
game.who = lord
@@ -3399,7 +3399,7 @@ states.muster_lord_at_seat = {
push_undo()
logii(`at %${loc}`)
- // TODO: clean up these transitions
+ // FIXME: clean up these transitions
set_lord_moved(game.who, 1)
muster_lord(game.who, loc)
game.state = "muster_lord_transport"
@@ -3692,7 +3692,7 @@ states.papal_legate_active = {
log(`Mustered L${lord}`)
logii(`at %${here}`)
- // TODO: clean up these transitions
+ // FIXME: clean up these transitions
muster_lord(lord, here)
push_state("muster_lord_transport")
game.who = lord
@@ -3880,6 +3880,8 @@ states.novgorod_veche = {
view.actions.delay = 1
}
+ // TODO: 2E 3.5.2 Shift all Russian cylinders at once
+
if (game.pieces.veche_vp > 0) {
for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
if (no_muster_of_or_by_lord(lord))
@@ -4682,7 +4684,7 @@ function select_all_lords(here) {
game.group = []
for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
if (get_lord_locale(lord) === here)
- game.group.push(lord)
+ set_add(game.group, lord)
}
function goto_avoid_battle() {
@@ -4841,7 +4843,7 @@ function avoid_battle_2() {
log(`Avoided Battle to %${to}.`)
for (let lord of game.group) {
- game.march.ambush_lords.push(lord)
+ set_add(game.march.ambush_lords, lord)
set_lord_locale(lord, to)
set_lord_moved(lord, 1)
}
@@ -5494,7 +5496,7 @@ function can_action_forage() {
return false
if (is_summer())
return true
- if (is_friendly_stronghold_locale(here)) // TODO: simpler check?
+ if (is_friendly_stronghold_locale(here)) // FIXME: simpler check?
return true
return false
}
@@ -6018,7 +6020,6 @@ function init_battle(here, is_storm, is_sally) {
reserves: [],
retreated: 0,
strikers: 0,
- targets: 0,
warrior_monks: 0,
hits: 0,
xhits: 0,
@@ -6077,7 +6078,6 @@ function start_storm() {
init_battle(here, 1, 0)
- // TODO: 2nd edition garrison forces
if (here === LOC_NOVGOROD)
init_garrison(0, 3)
else if (is_city(here))
@@ -6391,7 +6391,7 @@ function end_defender_events() {
function resume_battle_events() {
game.what = -1
- if (game.active === game.battle.attacker)
+ if (is_attacker())
goto_attacker_events()
else
goto_defender_events()
@@ -6423,7 +6423,7 @@ function can_play_battle_events() {
if (game.active === TEUTONS) {
if (could_play_card(EVENT_TEUTONIC_AMBUSH))
return true
- if (game.active !== game.battle.attacker) {
+ if (is_defender()) {
if (could_play_card(EVENT_TEUTONIC_HILL))
return true
if (!is_winter())
@@ -6438,7 +6438,7 @@ function can_play_battle_events() {
if (game.active === RUSSIANS) {
if (could_play_card(EVENT_RUSSIAN_AMBUSH))
return true
- if (game.active !== game.battle.attacker) {
+ if (is_defender()) {
if (could_play_card(EVENT_RUSSIAN_HILL))
return true
if (!is_winter())
@@ -6546,7 +6546,7 @@ function action_battle_events(c) {
case EVENT_TEUTONIC_AMBUSH:
case EVENT_RUSSIAN_AMBUSH:
log(`Played E${c}.`)
- if (game.active === game.battle.attacker)
+ if (is_attacker())
game.battle.ambush |= 2
else
game.battle.ambush |= 1
@@ -6567,7 +6567,7 @@ states.bridge = {
prompt() {
view.prompt = "Bridge: Play on a Center Lord."
let array = game.battle.array
- if (game.active === game.battle.attacker) {
+ if (is_attacker()) {
if (array[D2] !== NOBODY)
gen_action_lord(array[D2])
if (array[RD2] !== NOBODY)
@@ -6591,7 +6591,7 @@ states.field_organ = {
prompt() {
view.prompt = "Field Organ: Play on a Lord."
let array = game.battle.array
- if (game.active === game.battle.attacker) {
+ if (is_attacker()) {
for (let pos of battle_attacking_positions)
if (array[pos] !== NOBODY)
gen_action_lord(array[pos])
@@ -6621,7 +6621,7 @@ function goto_concede() {
if (game.battle.storm) {
log_h3(`Storm Round ${game.battle.round} / ${count_siege_markers(game.battle.where)}`)
if (game.battle.round === 1)
- goto_start_strike()
+ goto_first_strike()
else
game.state = "concede_storm"
} else {
@@ -6643,7 +6643,7 @@ states.concede_battle = {
},
battle() {
set_active_enemy()
- if (game.active === game.battle.attacker)
+ if (is_attacker())
goto_reposition_battle()
},
}
@@ -6683,35 +6683,49 @@ function goto_reposition_battle() {
// If all SA routed, send RD to reserve (end relief sally)
if (array[SA1] === NOBODY && array[SA2] === NOBODY && array[SA3] === NOBODY) {
- send_to_reserve(RD1)
- send_to_reserve(RD2)
- send_to_reserve(RD3)
+ if (array[RD1] !== NOBODY || array[RD2] !== NOBODY || array[RD3] !== NOBODY) {
+ log("Ended relief sally.")
+ send_to_reserve(RD1)
+ send_to_reserve(RD2)
+ send_to_reserve(RD3)
+ }
}
// If all D routed, advance RD to front
if (array[D1] === NOBODY && array[D2] === NOBODY && array[D3] === NOBODY) {
- slide_array(RD1, D1)
- slide_array(RD2, D2)
- slide_array(RD3, D3)
+ if (array[RD1] !== NOBODY || array[RD2] !== NOBODY || array[RD3] !== NOBODY) {
+ log("Reserve defenders to front.")
+ slide_array(RD1, D1)
+ slide_array(RD2, D2)
+ slide_array(RD3, D3)
+ }
}
// If all A routed, flip the battle field around:
if (array[A1] === NOBODY && array[A2] === NOBODY && array[A3] === NOBODY) {
- // Become a regular sally situation (siegeworks still count for defender)
- game.battle.sally = 1
- // Advance SA to front (to regular sally)
- slide_array(SA1, A1)
- slide_array(SA2, A2)
- slide_array(SA3, A3)
- // then D back to reserve
- send_to_reserve(D1)
- send_to_reserve(D2)
- send_to_reserve(D3)
- // then RD to D
- slide_array(RD1, D1)
- slide_array(RD2, D2)
- slide_array(RD3, D3)
- // and during the advance D may come back out from reserve
+ if (array[SA1] !== NOBODY || array[SA2] !== NOBODY || array[SA3] !== NOBODY) {
+ log("Attackers routed.")
+
+ // Become a regular sally situation (siegeworks still count for defender)
+ game.battle.sally = 1
+
+ // Advance SA to front (to regular sally)
+ slide_array(SA1, A1)
+ slide_array(SA2, A2)
+ slide_array(SA3, A3)
+
+ // then D back to reserve
+ send_to_reserve(D1)
+ send_to_reserve(D2)
+ send_to_reserve(D3)
+
+ // then RD to D
+ slide_array(RD1, D1)
+ slide_array(RD2, D2)
+ slide_array(RD3, D3)
+
+ // and during the advance D may come back out from reserve
+ }
}
set_active_attacker()
@@ -6728,7 +6742,7 @@ function goto_reposition_advance() {
function end_reposition_advance() {
game.who = NOBODY
set_active_enemy()
- if (game.active === game.battle.attacker)
+ if (is_attacker())
goto_reposition_center()
else
goto_reposition_advance()
@@ -6744,8 +6758,8 @@ function goto_reposition_center() {
function end_reposition_center() {
game.who = NOBODY
set_active_enemy()
- if (game.active === game.battle.attacker)
- goto_start_strike()
+ if (is_attacker())
+ goto_first_strike()
else
goto_reposition_center()
}
@@ -6753,10 +6767,9 @@ function end_reposition_center() {
function can_reposition_advance() {
if (has_reserves()) {
let array = game.battle.array
- if (game.active === game.battle.attacker) {
+ if (is_attacker()) {
if (array[A1] === NOBODY || array[A2] === NOBODY || array[A3] === NOBODY)
return true
- // TODO: sally more lords from castle?
} else {
if (array[D1] === NOBODY || array[D2] === NOBODY || array[D3] === NOBODY)
return true
@@ -6778,7 +6791,7 @@ states.reposition_advance = {
gen_action_lord(lord)
if (game.who !== NOBODY) {
- if (game.active === game.battle.attacker) {
+ if (is_attacker()) {
if (array[A1] === NOBODY) gen_action_array(A1)
if (array[A2] === NOBODY) gen_action_array(A2)
if (array[A3] === NOBODY) gen_action_array(A3)
@@ -6798,6 +6811,7 @@ states.reposition_advance = {
game.who = lord
},
array(pos) {
+ set_delete(game.battle.reserves, game.who)
game.battle.array[pos] = game.who
game.who = NOBODY
goto_reposition_advance()
@@ -6806,7 +6820,7 @@ states.reposition_advance = {
function can_reposition_center() {
let array = game.battle.array
- if (game.active === game.battle.attacker) {
+ if (is_attacker()) {
if (array[A2] === NOBODY && (array[A1] !== NOBODY || array[A3] !== NOBODY))
return true
if (array[SA2] === NOBODY && (array[SA1] !== NOBODY || array[SA3] !== NOBODY))
@@ -6825,7 +6839,7 @@ states.reposition_center = {
view.prompt = "Reposition: Slide to Center."
let array = game.battle.array
- if (game.active === game.battle.attacker) {
+ if (is_attacker()) {
if (array[A2] === NOBODY) {
if (array[A1] !== NOBODY) gen_action_lord(game.battle.array[A1])
if (array[A3] !== NOBODY) gen_action_lord(game.battle.array[A3])
@@ -6880,8 +6894,8 @@ function goto_reposition_storm() {
function end_reposition_storm() {
game.who = NOBODY
set_active_enemy()
- if (game.active === game.battle.attacker)
- goto_start_strike()
+ if (is_attacker())
+ goto_first_strike()
else
goto_reposition_storm()
}
@@ -6897,7 +6911,7 @@ states.reposition_storm = {
lord(lord) {
log(`Swapped in L${lord}.`)
set_delete(game.battle.reserves, lord)
- if (game.active === game.battle.attacker) {
+ if (is_attacker()) {
if (game.battle.array[A2] !== NOBODY)
set_add(game.battle.reserves, game.battle.array[A2])
game.battle.array[A2] = lord
@@ -6913,7 +6927,20 @@ states.reposition_storm = {
},
}
-// === BATTLE: TOTAL HITS ===
+// === BATTLE: STRIKE ===
+
+/*
+
+Strike groups:
+ Strike opposing lord
+ Strike closest flanked lord (choice left/right) if not directly opposed
+ Combine strikes with lords targeting same position
+
+Target groups:
+ If any striker is flanking target, single target.
+ If any other lords flank all strikers, add them to target group.
+
+*/
function get_battle_array(pos) {
if (game.battle.ambush & 1)
@@ -6937,19 +6964,19 @@ const battle_defending_positions = [ D1, D2, D3, RD1, RD2, RD3 ]
const battle_attacking_positions = [ A1, A2, A3, SA1, SA2, SA3 ]
const battle_steps = [
- { name: "Defending Archery", hits: count_archery_hits, xhits: count_archery_xhits, archery: 1 },
- { name: "Attacking Archery", hits: count_archery_hits, xhits: count_archery_xhits, archery: 1 },
- { name: "Defending Horse", hits: count_horse_hits, xhits: count_zero_hits, archery: 0 },
- { name: "Attacking Horse", hits: count_horse_hits, xhits: count_zero_hits, archery: 0 },
- { name: "Defending Foot", hits: count_foot_hits, xhits: count_zero_hits, archery: 0 },
- { name: "Attacking Foot", hits: count_foot_hits, xhits: count_zero_hits, archery: 0 },
+ { name: "Defending Archery", hits: count_archery_hits, xhits: count_archery_xhits },
+ { name: "Attacking Archery", hits: count_archery_hits, xhits: count_archery_xhits },
+ { name: "Defending Horse", hits: count_horse_hits, xhits: count_zero_hits },
+ { name: "Attacking Horse", hits: count_horse_hits, xhits: count_zero_hits },
+ { name: "Defending Foot", hits: count_foot_hits, xhits: count_zero_hits },
+ { name: "Attacking Foot", hits: count_foot_hits, xhits: count_zero_hits },
]
const storm_steps = [
- { name: "Defending Archery", hits: count_archery_hits, xhits: count_archery_xhits, archery: 1 },
- { name: "Attacking Archery", hits: count_archery_hits, xhits: count_archery_xhits, archery: 1 },
- { name: "Defending Melee", hits: count_melee_hits, xhits: count_zero_hits, archery: 0 },
- { name: "Attacking Melee", hits: count_melee_hits, xhits: count_zero_hits, archery: 0 },
+ { name: "Defending Archery", hits: count_archery_hits, xhits: count_archery_xhits },
+ { name: "Attacking Archery", hits: count_archery_hits, xhits: count_archery_xhits },
+ { name: "Defending Melee", hits: count_melee_hits, xhits: count_zero_hits },
+ { name: "Attacking Melee", hits: count_melee_hits, xhits: count_zero_hits },
]
function count_zero_hits(_) {
@@ -7014,13 +7041,13 @@ function count_foot_hits(lord) {
}
function count_garrison_xhits() {
- if (storm_steps[game.battle.step].archery)
+ if (is_archery_step())
return game.battle.garrison.men_at_arms
return 0
}
function count_garrison_hits() {
- if (!storm_steps[game.battle.step].archery)
+ if (is_melee_step())
return (game.battle.garrison.knights << 1) + (game.battle.garrison.men_at_arms << 1)
return 0
}
@@ -7037,35 +7064,232 @@ function count_lord_hits(lord) {
return battle_steps[game.battle.step].hits(lord)
}
-function has_lord_strike(lord) {
- if (lord !== NOBODY)
- return count_lord_hits(lord) + count_lord_xhits(lord) > 0
+function is_battle_over() {
+ set_active_attacker()
+ if (has_no_unrouted_forces())
+ return true
+ set_active_defender()
+ if (has_no_unrouted_forces())
+ return true
return false
}
-function has_center_strike(AX, DX) {
- if (game.active === game.battle.attacker)
- return has_lord_strike(get_battle_array(AX))
+function has_no_unrouted_forces() {
+ // All unrouted lords are either in battle array or in reserves
+ for (let p = 0; p < 12; ++p)
+ if (is_friendly_lord(game.battle.array[p]))
+ return false
+ for (let lord of game.battle.reserves)
+ if (is_friendly_lord(lord))
+ return false
+ if (game.battle.storm && is_defender())
+ if (game.battle.garrison)
+ return false
+ return true
+}
+
+function is_attacker() {
+ return game.active === game.battle.attacker
+}
+
+function is_defender() {
+ return game.active !== game.battle.attacker
+}
+
+function is_attacker_step() {
+ return (game.battle.step & 1) === 1
+}
+
+function is_defender_step() {
+ return (game.battle.step & 1) === 0
+}
+
+function is_archery_step() {
+ return game.battle.step < 2
+}
+
+function is_melee_step() {
+ return game.battle.step >= 2
+}
+
+function did_concede() {
+ return game.active === game.battle.conceded
+}
+
+function did_not_concede() {
+ return game.active !== game.battle.conceded
+}
+
+function has_strike(pos) {
+ return game.battle.ah[pos] + game.battle.ahx[pos] > 0
+}
+
+function current_strike_positions() {
+ if (game.battle.storm)
+ return is_attacker_step() ? [ A2 ] : [ D2 ]
else
- return has_lord_strike(get_battle_array(DX))
+ return is_attacker_step() ? battle_attacking_positions : battle_defending_positions
}
-function has_sa_strike() {
- return (
- has_lord_strike(get_battle_array(SA1)) ||
- has_lord_strike(get_battle_array(SA2)) ||
- has_lord_strike(get_battle_array(SA3))
- )
+function find_closest_target(A, B, C) {
+ if (filled(A)) return A
+ if (filled(B)) return B
+ if (filled(C)) return C
+ return -1
+}
+
+function find_closest_target_center(T2) {
+ if (game.battle.fc < 0) throw Error("unset front l/r choice")
+ if (game.battle.rc < 0) throw Error("unset rear l/r choice")
+ if (filled(T2))
+ return T2
+ if (T2 >= A1 && T2 <= D3)
+ return game.battle.fc
+ return game.battle.rc
+}
+
+function find_strike_target(S) {
+ switch (S) {
+ case A1: return find_closest_target(D1, D2, D3)
+ case A2: return find_closest_target_center(D2)
+ case A3: return find_closest_target(D3, D2, D1)
+ case D1: return find_closest_target(A1, A2, A3)
+ case D2: return find_closest_target_center(A2)
+ case D3: return find_closest_target(A3, A2, A1)
+ case SA1: return find_closest_target(RD1, RD2, RD3)
+ case SA2: return find_closest_target_center(RD2)
+ case SA3: return find_closest_target(RD3, RD2, RD1)
+ case RD1: return find_closest_target(SA1, SA2, SA3)
+ case RD2: return find_closest_target_center(SA2)
+ case RD3: return find_closest_target(SA3, SA2, SA1)
+ }
+}
+
+function has_strike_target(S) {
+ if (is_attacker_step() && game.battle.storm && game.battle.garrison)
+ return true
+ if (S === A1 || S === A2 || S === A3)
+ return filled(D1) || filled(D2) || filled(D3)
+ if (S === D1 || S === D2 || S === D3)
+ return filled(A1) || filled(A2) || filled(A3)
+ if (S === SA1 || S === SA2 || S === SA3)
+ return filled(RD1) || filled(RD2) || filled(RD3) || filled(D1) || filled(D2) || filled(D3)
+ if (S === RD1 || S === RD2 || S === RD3)
+ return filled(SA1) || filled(SA2) || filled(SA3)
+}
+
+function has_no_strike_targets() {
+ for (let striker of game.battle.strikers)
+ if (has_strike_target(striker))
+ return false
+ return true
+}
+
+function has_no_strikers_and_strike_targets() {
+ for (let pos of current_strike_positions())
+ if (has_strike(pos) && has_strike_target(pos))
+ return false
+ return true
+}
+
+function create_strike_group(start) {
+ let strikers = [ start ]
+ let target = find_strike_target(start)
+ for (let pos of current_strike_positions())
+ if (pos !== start && filled(pos) && find_strike_target(pos) === target)
+ set_add(strikers, pos)
+ return strikers
+}
+
+function flanks_position_row(S, T, S1, S2, S3, T1, T2, T3) {
+ // S and T are not empty
+ switch (S) {
+ case S1:
+ switch (T) {
+ case T1: return false
+ case T2: return empty(T1)
+ case T3: return empty(T1) && empty(T2)
+ }
+ break
+ case S2:
+ return empty(T2)
+ case S3:
+ switch (T) {
+ case T1: return empty(T3) && empty(T2)
+ case T2: return empty(T3)
+ case T3: return false
+ }
+ break
+ }
+ return false
+}
+
+function flanks_position(S, T) {
+ if (S === A1 || S === A2 || S === A3)
+ return flanks_position_row(S, T, A1, A2, A3, D1, D2, D3)
+ if (S === D1 || S === D2 || S === D3)
+ return flanks_position_row(S, T, D1, D2, D3, A1, A2, A3)
+ if (S === SA1 || S === SA2 || S === SA3)
+ return flanks_position_row(S, T, SA1, SA2, SA3, RD1, RD2, RD3)
+ if (S === RD1 || S === RD2 || S === RD3)
+ return flanks_position_row(S, T, RD1, RD2, RD3, SA1, SA2, SA3)
+}
+
+function flanks_all_positions(S, TT) {
+ for (let T of TT)
+ if (!flanks_position(S, T))
+ return false
+ return true
+}
+
+function strike_left_or_right(gate, S2, T1, T2, T3) {
+ if (gate(S2)) {
+ if (filled(T2))
+ return T2
+ let has_t1 = filled(T1)
+ let has_t3 = filled(T3)
+ if (has_t1 && has_t3)
+ return -1
+ if (has_t1)
+ return T1
+ if (has_t3)
+ return T3
+ }
+ return T2 // No target - safe default
+}
+
+function strike_reserve_defender() {
+ let has_d1 = filled(D1)
+ let has_d2 = filled(D2)
+ let has_d3 = filled(D3)
+ if (has_d1 && !has_d2 && !has_d3) return D1
+ if (!has_d1 && has_d2 && !has_d3) return D2
+ if (!has_d1 && !has_d2 && has_d3) return D3
+ return -1
}
// === BATTLE: STRIKE ===
-// Segment strikers and targets into groups according to flanking situation (front/rear choice).
-// S picks group to strike.
-// Roll for walls or siegeworks.
-// T applies hits.
-// Rolls for armor.
-// If any routed, recalculate target group for current strike group (front/rear choice again).
+/*
+
+for each battle step:
+ generate strikes for each lord
+ while strikes remain:
+ create list of strike groups (choose left/right both rows)
+ select strike group
+ create target group (choose if sally)
+ total strikes and roll for walls
+ while hits remain:
+ assign hit to unit in target group
+ if lord routs:
+ forget choice of left/right strike group in current row
+ create new target group (choose if left/right/sally)
+
+*/
+
+function format_group(g) {
+ return g.map(p=>lord_name[game.battle.array[p]]).join(", ")
+}
function format_strike_step() {
// TODO: format strike group and target groups too?
@@ -7074,7 +7298,16 @@ function format_strike_step() {
return battle_steps[game.battle.step].name
}
-function goto_start_strike() {
+function format_hits() {
+ if (game.battle.xhits > 0 && game.battle.hits > 0)
+ return `${game.battle.xhits} crossbow hits and ${game.battle.hits} hits`
+ else if (game.battle.xhits > 0)
+ return `${game.battle.xhits} crossbow hits`
+ else
+ return `${game.battle.hits} hits`
+}
+
+function goto_first_strike() {
game.battle.step = 0
goto_strike()
}
@@ -7091,534 +7324,257 @@ function goto_next_strike() {
function goto_strike() {
// Exit early if one side is completely routed
if (is_battle_over()) {
- goto_next_strike()
+ end_battle_round()
return
}
- if (game.battle.step & 1)
+ if (is_attacker_step())
set_active_attacker()
else
set_active_defender()
+ if (game.battle.storm)
+ log_h4(storm_steps[game.battle.step].name)
+ else
+ log_h4(battle_steps[game.battle.step].name)
+
// Once per Archery and once per Melee.
if (game.battle.step === 0 || game.battle.step === 2) {
game.battle.warrior_monks = 0
for (let p = 0; p < 12; ++p) {
let lord = game.battle.array[p]
if (lord !== NOBODY && lord_has_capability(lord, AOW_TEUTONIC_WARRIOR_MONKS))
- game.battle.warrior_monks |= (1 << lord)
+ game.battle.warrior_monks |= 1 << lord
}
}
- if (game.battle.storm)
- goto_strike_storm()
- else
- goto_strike_battle()
-}
-
-function is_battle_over() {
- set_active_attacker()
- if (has_no_unrouted_forces())
- return true
- set_active_defender()
- if (has_no_unrouted_forces())
- return true
- return false
-}
-
-function has_no_unrouted_forces() {
- // All unrouted lords are either in battle array or in reserves
- for (let p = 0; p < 12; ++p)
- if (is_friendly_lord(game.battle.array[p]))
- return false
- for (let lord of game.battle.reserves)
- if (is_friendly_lord(lord))
- return false
- if (game.battle.storm && game.active !== game.battle.attacker)
- if (game.battle.garrison)
- return false
- return true
-}
+ if (is_marsh_in_play())
+ log("Marsh")
-function has_sa_without_rd() {
- if (game.active === game.battle.attacker)
- if (filled(SA1) || filled(SA2) || filled(SA3))
- if (empty(RD1) && empty(RD2) && empty(RD3))
- return true
- return false
-}
+ if (is_archery_step() && is_hill_in_play())
+ log("Hill")
-function select_lone_defender() {
- let has_d1 = filled(D1)
- let has_d2 = filled(D2)
- let has_d3 = filled(D3)
- if (has_d1 && !has_d2 && !has_d3)
- return D1
- if (!has_d1 && has_d2 && !has_d3)
- return D2
- if (!has_d1 && !has_d2 && has_d3)
- return D3
- if (!has_d1 && !has_d2 && !has_d3)
- return 0 // no target!
- return -1 // choice of target
-}
-
-function pack_battle_array_front() {
- let x = 0
- for (let i = 0; i < 6; ++i)
- if (filled(i))
- x |= (1 << i)
- return x
-}
-
-function pack_battle_array_rear() {
- let x = 0
- for (let i = 0; i < 6; ++i)
- if (filled(i+6))
- x |= (1 << i)
- return x
-}
-
-function front_strike_choice() {
- let s = game.battle.step & 1
- let x = pack_battle_array_front()
- if (GROUPS[s][1][x] !== 0) {
- // Choice only matters if the center Lord has strikes this step
- if (has_center_strike(A2, D2))
- game.battle.fc = -1
- else
- game.battle.fc = 0
- } else {
- game.battle.fc = 0
- }
-}
+ if (is_melee_step() && game.battle.bridge)
+ log("TODO: Bridge")
-function rear_strike_choice() {
- if (has_sa_without_rd()) {
- if (has_sa_strike())
- game.battle.rc = select_lone_defender()
- else
- game.battle.rc = 0
+ // Generate hits
+ if (!game.battle.storm) {
+ game.battle.ah = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+ game.battle.ahx = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
} else {
- let s = game.battle.step & 1
- let x = pack_battle_array_rear()
- if (GROUPS[s][1][x] !== 0) {
- // Choice only matters if the center Lord has strikes this step
- if (has_center_strike(SA2, RD2))
- game.battle.fc = -1
- else
- game.battle.rc = 0
- } else {
- game.battle.rc = 0
- }
+ game.battle.ah = [ 0, 0, 0, 0, 0, 0 ]
+ game.battle.ahx = [ 0, 0, 0, 0, 0, 0 ]
}
-}
-function unpack_group(g, offset) {
- let list = []
- for (let i = 0; i < 6; ++i)
- if ((g >> i) & 1)
- list.push(i + offset)
- return list
-}
+ for (let pos of current_strike_positions()) {
+ let lord = get_battle_array(pos)
+ if (lord !== NOBODY) {
+ let hits = (game.battle.ah[pos] = count_lord_hits(lord))
+ let xhits = (game.battle.ahx[pos] = count_lord_xhits(lord))
-function create_battle_group(list, targets) {
- let strikers = []
- let hits = 0
- let xhits = 0
+ // STORM: Max 6 hits per lord in melee (12 since we count half-hits).
+ if (game.battle.storm) {
+ if (is_melee_step() && hits > 12)
+ hits = 12
+ }
- for (let pos of list) {
- let lord = game.battle.array[pos]
- let lord_hits = count_lord_hits(lord)
- let lord_xhits = count_lord_xhits(lord)
- if (lord_hits + lord_xhits > 0) {
- strikers.push(pos)
- if (lord_xhits > 0)
- log(`L${lord} ${lord_xhits/2} crossbow hits`)
- if (lord_hits > 0)
- log(`L${lord} ${lord_hits/2} hits`)
- hits += lord_hits
- xhits += lord_xhits
- }
- }
-
- if (strikers.length > 0) {
- // Round in favor of crossbow hits.
- if (xhits & 1) {
- hits = (hits >> 1)
- xhits = (xhits >> 1) + 1
- } else {
- if (hits & 1)
- hits = (hits >> 1) + 1
- else
- hits = (hits >> 1)
- xhits = (xhits >> 1)
+ if (xhits > 0)
+ log(`L${lord} ${xhits / 2} crossbow hits`)
+ if (hits > 0)
+ log(`L${lord} ${hits / 2} hits`)
}
+ }
- // Conceding side halves its total Hits, rounded up.
- if (game.active === game.battle.conceded) {
- hits = (hits + 1) >> 1
- xhits = (xhits + 1) >> 1
- }
+ if (did_concede())
+ log("Pursuit halved hits")
+
+ // Strike left or right or reserve defender
+ if (is_attacker_step())
+ game.battle.fc = strike_left_or_right(has_strike, A2, D1, D2, D3)
+ else
+ game.battle.fc = strike_left_or_right(has_strike, D2, A1, A2, A3)
- game.battle.groups.push([ strikers, targets, hits, xhits ])
+ if (is_sa_without_rd()) {
+ // NOTE: striking reserve defenders is handled in strike_group and assign_hits
+ game.battle.rc = RD2
+ } else {
+ if (is_attacker_step())
+ game.battle.rc = strike_left_or_right(has_strike, SA2, RD1, RD2, RD3)
+ else
+ game.battle.rc = strike_left_or_right(has_strike, RD2, SA1, SA2, SA3)
}
-}
-function create_battle_groups_from_array(list, offset) {
- if (list)
- for (let [ s, t ] of list)
- create_battle_group(unpack_group(s, offset), unpack_group(t, offset))
+ if (game.battle.storm) {
+ if (is_attacker_step())
+ game.battle.strikers = [ A2 ]
+ else
+ game.battle.strikers = [ D2 ]
+ goto_strike_total_hits()
+ } else {
+ if (has_no_strikers_and_strike_targets())
+ log("No targets.")
+ resume_strike()
+ }
}
-function format_group(g) {
- return g.map(p=>lord_name[game.battle.array[p]]).join(", ")
+function resume_strike() {
+ if (game.battle.storm)
+ goto_next_strike() // NOTE: only one strike group in a storm
+ else if (has_no_strikers_and_strike_targets())
+ goto_next_strike()
+ else if (game.battle.fc < 0 || game.battle.rc < 0)
+ game.state = "strike_left_right"
+ else
+ // TODO: auto-strike if only one group?
+ game.state = "strike_group"
}
-function debug_group(g) {
- return g.map(p=>battle_array_name[p]).join("+")
+function prompt_target_2(S1, T1, T3) {
+ view.who = game.battle.array[S1]
+ gen_action_lord(game.battle.array[T1])
+ gen_action_lord(game.battle.array[T3])
}
-function debug_group_list(list) {
- for (let [strikers,targets,hits,xhits] of list)
- console.log(debug_group(strikers), "strike", debug_group(targets), "for", hits, "+", xhits)
+function is_sa_without_rd() {
+ return (filled(SA1) || filled(SA2) || filled(SA3)) && empty(RD1) && empty(RD2) && empty(RD3)
}
-function debug_battle_array(f, r) {
- for (let row = 0; row < 6; row += 3) {
- let x = ""
- for (let col = 0; col < 3; ++col) {
- let b = row + col
- if ((f >> b) & 1)
- x += battle_array_name[b] + " "
- else
- x += "--- "
- }
- console.log(x)
- }
- for (let row = 3; row >= 0; row -= 3) {
- let x = ""
- for (let col = 0; col < 3; ++col) {
- let b = row + col
- if ((r >> b) & 1)
- x += battle_array_name[b+6] + " "
+function prompt_left_right() {
+ if (game.battle.fc < 0) {
+ view.prompt = `${format_strike_step()}: Strike left or right?`
+ if (is_attacker_step())
+ prompt_target_2(A2, D1, D3)
+ else
+ prompt_target_2(D2, A1, A3)
+ } else {
+ if (is_sa_without_rd()) {
+ view.prompt = `${format_strike_step()}: Strike which reserve defender?`
+ view.group = []
+ if (filled(SA1)) view.group.push(SA1)
+ if (filled(SA2)) view.group.push(SA2)
+ if (filled(SA3)) view.group.push(SA3)
+ if (filled(D1)) gen_action_lord(game.battle.array[D1])
+ if (filled(D2)) gen_action_lord(game.battle.array[D2])
+ if (filled(D3)) gen_action_lord(game.battle.array[D3])
+ } else {
+ view.prompt = `${format_strike_step()}: Strike left or right?`
+ if (is_attacker_step())
+ prompt_target_2(SA2, RD1, RD3)
else
- x += "--- "
- }
- console.log(x)
- }
-}
-
-function goto_strike_storm() {
- log_h4(storm_steps[game.battle.step].name)
-
- let strikers = []
- let targets = []
- let hits = 0
- let xhits = 0
-
- function add_lord_strike(pos) {
- let lord = game.battle.array[pos]
- let lord_hits = count_lord_hits(lord)
- let lord_xhits = count_lord_xhits(lord)
-
- // Max 6 hits per lord in melee (12 since we count half-hits).
- if (game.battle.step >= 2)
- if (lord_hits > 12)
- lord_hits = 12
-
- if (lord_hits + lord_xhits > 0) {
- strikers.push(pos)
- if (lord_xhits > 0)
- log(`L${lord} ${lord_xhits/2} crossbow hits`)
- if (lord_hits > 0)
- log(`L${lord} ${lord_hits/2} hits`)
- hits += lord_hits
- xhits += lord_xhits
+ prompt_target_2(RD2, SA1, SA3)
}
}
-
- if (game.active === game.battle.attacker) {
- add_lord_strike(A2)
- if (game.battle.array[D2] !== NOBODY)
- targets.push(D2)
- } else {
- if (game.battle.garrison) {
- let garr_hits = count_garrison_hits()
- let garr_xhits = count_garrison_xhits()
- if (garr_xhits > 0)
- log(`Garrison ${garr_xhits/2} crossbow hits`)
- if (garr_hits > 0)
- log(`Garrison ${garr_hits/2} hits`)
- hits += garr_hits
- xhits += garr_xhits
- }
- add_lord_strike(D2)
- targets.push(A2)
- }
-
- if (hits + xhits === 0) {
- log("No hits.")
- goto_next_strike()
- return
- }
-
- // Round in favor of crossbow hits.
- if (xhits & 1) {
- hits = (hits >> 1)
- xhits = (xhits >> 1) + 1
- } else {
- if (hits & 1)
- hits = (hits >> 1) + 1
- else
- hits = (hits >> 1)
- xhits = (xhits >> 1)
- }
-
- game.battle.groups = []
- game.battle.strikers = strikers
- game.battle.targets = targets
- game.battle.hits = hits
- game.battle.xhits = xhits
-
- // TODO: "select_strike_group" w/ garrison?
-
- goto_assign_hits()
}
-function goto_strike_battle() {
- log_h4(battle_steps[game.battle.step].name)
-
- if (is_marsh_in_play())
- log("Marsh")
-
- if (is_hill_in_play() && game.battle.step < 2)
- log("Hill")
-
- if (game.battle.step >= 2) // Melee Step
- if (game.battle.bridge)
- log("TODO: Bridge")
-
- front_strike_choice()
- rear_strike_choice()
-
- goto_strike_choice()
-}
-
-function goto_strike_choice() {
- if (game.battle.fc === -1)
- game.state = "front_strike_choice"
- else if (game.battle.rc === -1)
- game.state = "rear_strike_choice"
+function action_left_right(lord) {
+ log(`Targeted L${lord}`)
+ let pos = get_lord_array_position(lord)
+ if (game.battle.fc < 0)
+ game.battle.fc = pos
else
- end_strike_choice()
+ game.battle.rc = pos
}
-function prompt_strike_choice(X1, X2, X3, Y2) {
- if (empty(X2) && filled(Y2)) {
- view.who = game.battle.array[Y2]
- if (filled(X1))
- gen_action_lord(game.battle.array[X1])
- if (filled(X3))
- gen_action_lord(game.battle.array[X3])
- }
+states.strike_left_right = {
+ prompt: prompt_left_right,
+ lord(lord) {
+ action_left_right(lord)
+ resume_strike()
+ },
}
-states.front_strike_choice = {
- prompt() {
- view.prompt = `${format_strike_step()}: Strike who?`
- if (game.active === game.battle.attacker)
- prompt_strike_choice(D1, D2, D3, A2)
- else
- prompt_strike_choice(A1, A2, A3, D2)
- },
+states.assign_left_right = {
+ prompt: prompt_left_right,
lord(lord) {
- let pos = get_lord_array_position(lord)
- if (pos === A1 || pos === D1)
- game.battle.fc = 0
- if (pos === A3 || pos === D3)
- game.battle.fc = 1
- goto_strike_choice()
+ action_left_right(lord)
+ set_active_enemy()
+ goto_assign_hits()
},
}
-states.rear_strike_choice = {
+states.strike_group = {
prompt() {
- view.prompt = `${format_strike_step()}: Strike who?`
- if (has_sa_without_rd()) {
- if (has_sa_strike()) {
- view.group = []
- if (filled(SA1)) view.group.push(SA1)
- if (filled(SA2)) view.group.push(SA2)
- if (filled(SA3)) view.group.push(SA3)
- if (filled(D1)) gen_action_lord(game.battle.array[D1])
- if (filled(D2)) gen_action_lord(game.battle.array[D2])
- if (filled(D3)) gen_action_lord(game.battle.array[D3])
- }
- } else {
- if (game.active === game.battle.attacker)
- prompt_strike_choice(RD1, RD2, RD3, SA2)
- else
- prompt_strike_choice(SA1, SA2, SA3, RD2)
- }
+ view.prompt = `${format_strike_step()}: Strike with a Lord.`
+ for (let pos of current_strike_positions())
+ if (has_strike(pos))
+ gen_action_lord(game.battle.array[pos])
},
lord(lord) {
let pos = get_lord_array_position(lord)
- if (has_sa_without_rd()) {
- game.battle.rc = pos
+ if ((pos === SA1 || pos === SA2 || pos === SA3) && is_sa_without_rd()) {
+ game.battle.strikers = [ SA1, SA2, SA3 ]
+ game.battle.rc = strike_reserve_defender()
} else {
- if (pos === SA1 || pos === RD1)
- game.battle.rc = 0
- if (pos === SA3 || pos === RD3)
- game.battle.rc = 1
+ game.battle.strikers = create_strike_group(pos)
}
- goto_strike_choice()
+ goto_strike_total_hits()
},
}
-function end_strike_choice() {
- let s = game.battle.step & 1
- let front = pack_battle_array_front()
- let rear = pack_battle_array_rear()
+// === BATTLE: TOTAL HITS (ROUND UP) ===
- console.log("STRIKE")
+function goto_strike_total_hits() {
+ let hits = 0
+ let xhits = 0
- debug_battle_array(front, rear)
+ // STORM: Garrison strikes
+ if (is_defender_step() && game.battle.storm && game.battle.garrison) {
+ let garr_hits = count_garrison_hits()
+ let garr_xhits = count_garrison_xhits()
+ if (garr_xhits > 0)
+ log(`Garrison ${garr_xhits/2} crossbow hits`)
+ if (garr_hits > 0)
+ log(`Garrison ${garr_hits/2} hits`)
+ hits += garr_hits
+ xhits += garr_xhits
+ }
- game.battle.groups = []
+ // Total hits
+ for (let pos of game.battle.strikers) {
+ hits += game.battle.ah[pos]
+ xhits += game.battle.ahx[pos]
+ game.battle.ah[pos] = 0
+ game.battle.ahx[pos] = 0
+ }
- create_battle_groups_from_array(GROUPS[s][game.battle.fc][front], 0)
- if (has_sa_without_rd()) {
- if (game.battle.rc === D1 || game.battle.rc === D2 || game.battle.rc === D3)
- create_battle_group([ SA1, SA2, SA3 ], [ game.battle.rc ])
+ // Round in favor of crossbow hits.
+ if (xhits & 1) {
+ hits = (hits >> 1)
+ xhits = (xhits >> 1) + 1
} else {
- create_battle_groups_from_array(GROUPS[s][game.battle.rc][rear], 6)
+ if (hits & 1)
+ hits = (hits >> 1) + 1
+ else
+ hits = (hits >> 1)
+ xhits = (xhits >> 1)
}
- if (game.active === game.battle.conceded)
- log("Pursuit halved hits")
-
- debug_group_list(game.battle.groups)
-
- goto_select_strike_group()
-}
-
+ // Conceding side halves its total Hits, rounded up.
+ if (did_concede()) {
+ hits = (hits + 1) >> 1
+ xhits = (xhits + 1) >> 1
+ }
-function goto_select_strike_group() {
- if (game.battle.groups.length === 0)
- goto_next_strike()
- /* TODO auto select is too abrupt, maybe option?
- else if (game.battle.groups.length === 1)
- select_strike_group(0)
- */
- else
- game.state = "select_strike_group"
-}
+ game.battle.hits = hits
+ game.battle.xhits = xhits
-states.select_strike_group = {
- prompt() {
- view.prompt = `${format_strike_step()}: Select Striking Lord or Group.`
- for (let [strikers] of game.battle.groups) {
- for (let p of strikers)
- gen_action_lord(game.battle.array[p])
- }
- },
- lord(lord) {
- for (let i = 0; i < game.battle.groups.length; ++i) {
- for (let p of game.battle.groups[i][0])
- if (game.battle.array[p] === lord)
- select_strike_group(i)
- }
- },
-}
+ log(format_group(game.battle.strikers) + " struck")
-function select_strike_group(i) {
- ;[ game.battle.strikers, game.battle.targets, game.battle.hits, game.battle.xhits ] = game.battle.groups[i]
- array_remove(game.battle.groups, i)
- goto_assign_hits()
+ goto_strike_roll_walls()
}
-// === BATTLE: APPLY HITS / PROTECTION / ROLL WALLS ===
+// === BATTLE: ROLL WALLS ===
-function format_hits() {
- if (game.battle.xhits > 0 && game.battle.hits > 0)
- return `${game.battle.xhits} crossbow hits and ${game.battle.hits} hits`
- else if (game.battle.xhits > 0)
- return `${game.battle.xhits} crossbow hits`
- else
- return `${game.battle.hits} hits`
-}
-
-function has_unrouted_forces_in_target() {
- if (game.battle.storm && game.active !== game.battle.attacker)
- if (game.battle.garrison)
- return true
- for (let p of game.battle.targets) {
- let lord = game.battle.array[p]
- if (lord_has_unrouted_units(lord))
- return true
- }
- return false
-}
-
-function is_flanked_target() {
- if (game.battle.targets.length === 1) {
- let pos = game.battle.targets[0]
- let has_d1 = filled(D1) && empty(A1)
- let has_d2 = filled(D2) && empty(A2)
- let has_d3 = filled(D3) && empty(A3)
- let has_a2 = filled(A2)
- switch (pos) {
- case A1:
- return has_d2 || (has_d3 && !has_a2)
- case A2:
- return has_d1 || has_d3
- case A3:
- return has_d2 || (has_d1 && !has_a2)
- }
- }
- return false
-}
-
-function has_valid_target() {
- if (game.battle.storm && game.active !== game.battle.attacker && game.battle.garrison)
- return true
- return game.battle.targets.length > 0
-}
-
-function goto_assign_hits() {
+function goto_strike_roll_walls() {
set_active_enemy()
- if (!has_valid_target()) {
- end_assign_hits()
- return
- }
-
- if (game.battle.storm && game.active !== game.battle.attacker && game.battle.garrison)
- log("Garrison")
- else
- log(`${format_group(game.battle.targets)}`)
-
- if (has_sa_without_rd()) {
- console.log("SA without RD (getting hit)")
- if (!is_flanked_target()) {
- console.log(" unflanked, SA added to hit group")
- if (filled(SA1))
- game.battle.targets.push(SA1)
- if (filled(SA2))
- game.battle.targets.push(SA2)
- if (filled(SA3))
- game.battle.targets.push(SA3)
- }
- }
-
if (game.battle.storm) {
- if (game.active === game.battle.attacker)
- roll_for_siegeworks()
- else
+ if (is_attacker_step())
roll_for_walls()
+ else
+ roll_for_siegeworks()
} else if (game.battle.sally) {
- if (game.active !== game.battle.attacker) {
+ if (is_attacker_step()) {
if (is_ravens_rock_in_play() && count_siege_markers(game.battle.where) < 2)
roll_for_ravens_rock()
else
@@ -7641,36 +7597,13 @@ function goto_assign_hits() {
}
if (game.battle.xhits > 0)
- logi(`Took ${game.battle.xhits} crossbow hits`)
+ logi(`${game.battle.xhits} crossbow hits`)
if (game.battle.hits > 0)
- logi(`Took ${game.battle.hits} hits`)
+ logi(`${game.battle.hits} hits`)
if (game.battle.hits + game.battle.xhits === 0)
- logi(`Took no hits`)
-
- resume_assign_hits()
-}
-
-function resume_assign_hits() {
- if (game.battle.hits + game.battle.xhits === 0) {
- end_assign_hits()
- } else if (!has_unrouted_forces_in_target()) {
- log("TODO: remaining hits!")
- // TODO: calculate new hit group for the current striking group, and resume or end if no valid targets
- end_assign_hits()
- } else {
- game.state = "assign_hits"
- }
-}
-
-function end_assign_hits() {
- game.battle.strikers = 0
- game.battle.targets = 0
+ logi(`No hits`)
- set_active_enemy()
- if (game.battle.storm)
- goto_next_strike()
- else
- goto_select_strike_group()
+ goto_assign_hits()
}
function enemy_has_trebuchets() {
@@ -7749,120 +7682,80 @@ function roll_for_protection(name, prot, n) {
// === BATTLE: ASSIGN HITS TO UNITS / ROLL BY HIT / ROUT ===
-function rout_lord(lord) {
- log(`L${lord} routed!`)
-
- let p = get_lord_array_position(lord)
-
- // remove from battle array
- game.battle.array[p] = NOBODY
-
- // FIXME cleanup TODO, removing from which groups
-
- // remove from current hit group
- array_remove_item(game.battle.targets, p)
-
- for (let i = 0; i < game.battle.groups; ) {
- let targets = game.battle.groups[i][1]
-
- // remove from other hit groups
- array_remove_item(targets, p)
+function goto_assign_hits() {
+ if (game.battle.hits + game.battle.xhits === 0)
+ return end_assign_hits()
- // remove strike groups with no remaining targets
- if (targets.length === 0)
- array_remove(game.battle.groups, i)
- else
- ++i
+ if (has_no_strike_targets()) {
+ log("Lost " + format_hits())
+ return end_assign_hits()
}
-}
-function rout_unit(lord, type) {
- if (lord === GARRISON) {
- if (type === KNIGHTS)
- game.battle.garrison.knights--
- if (type === MEN_AT_ARMS)
- game.battle.garrison.men_at_arms--
- if (game.battle.garrison.knights + game.battle.garrison.men_at_arms === 0) {
- log("Garrison routed.")
- game.battle.garrison = 0
- }
+ if (is_attacker_step()) {
+ if (game.battle.fc < 0 && set_has(game.battle.strikers, A2))
+ return goto_assign_left_right()
+ if (game.battle.rc < 0 && set_has(game.battle.strikers, SA2))
+ return goto_assign_left_right()
} else {
- add_lord_forces(lord, type, -1)
- add_lord_routed_forces(lord, type, 1)
+ if (game.battle.fc < 0 && set_has(game.battle.strikers, D2))
+ return goto_assign_left_right()
+ if (game.battle.rc < 0 && set_has(game.battle.strikers, RD2))
+ return goto_assign_left_right()
}
+
+ game.state = "assign_hits"
}
-function use_warrior_monks(lord, type) {
- if (type === KNIGHTS) {
- let bit = 1 << lord
- if (game.battle.warrior_monks & bit) {
- game.battle.warrior_monks ^= bit
- return true
- }
- }
- return false
+function goto_assign_left_right() {
+ set_active_enemy()
+ game.state = "assign_left_right"
}
-function action_assign_hits(lord, type) {
- let protection = FORCE_PROTECTION[type]
- let evade = FORCE_EVADE[type]
+function end_assign_hits() {
+ game.battle.strikers = 0
+ game.battle.hits = 0
+ game.battle.xhits = 0
+ set_active_enemy()
+ if (game.battle.storm)
+ goto_next_strike()
+ else
+ resume_strike()
+}
- // TODO: manual choice of hit type
- let ap = (is_armored_force(type) && game.battle.xhits > 0) ? 2 : 0
+function for_each_target(fn) {
+ let start = game.battle.strikers[0]
- // TODO: which lord? summarize?
+ // SA without RD striking D, target is always flanked
+ if ((start === SA1 || start === SA2 || start === SA3) && is_sa_without_rd()) {
+ fn(game.battle.array[game.battle.rc])
+ return
+ }
- if (type === SERGEANTS || type === MEN_AT_ARMS)
- if (lord_has_capability(lord, AOW_TEUTONIC_HALBBRUDER))
- protection += 1
+ let target = find_strike_target(start)
- // Evade only in Battle Melee steps
- if (evade > 0 && !game.battle.storm && game.battle.step >= 2) {
- let die = roll_die()
- if (die <= evade) {
- logi(`${FORCE_TYPE_NAME[type]} ${die} <= ${evade}`)
- } else {
- logi(`${FORCE_TYPE_NAME[type]} ${die} > ${evade}`)
- rout_unit(lord, type)
- }
- } else if (protection > 0) {
- let die = roll_die()
- if (die <= protection - ap) {
- logi(`${FORCE_TYPE_NAME[type]} ${die} <= ${protection - ap}`)
- } else {
- logi(`${FORCE_TYPE_NAME[type]} ${die} > ${protection - ap}`)
- if (use_warrior_monks(lord, type)) {
- die = roll_die()
- if (die <= protection - ap) {
- logi(`Warrior Monks ${die} <= ${protection - ap}`)
- } else {
- logi(`Warrior Monks ${die} > ${protection - ap}`)
- rout_unit(lord, type)
- }
- } else {
- rout_unit(lord, type)
- }
- }
- } else {
- logi(`${FORCE_TYPE_NAME[type]} unprotected`)
- rout_unit(lord, type)
- }
+ fn(game.battle.array[target])
- if (game.battle.xhits)
- game.battle.xhits--
- else
- game.battle.hits--
+ // If strikers flank target, target must take all hits
+ for (let striker of game.battle.strikers)
+ if (flanks_position(striker, target))
+ return
- if (!lord_has_unrouted_units(lord))
- rout_lord(lord)
+ // If other lord flanks all strikers, he may take hits instead
+ for (let flanker of ARRAY_FLANKS[target])
+ if (filled(flanker) && flanks_all_positions(flanker, game.battle.strikers))
+ fn(game.battle.array[flanker])
- resume_assign_hits()
+ // SA without RD flank all D equally closely
+ if ((target === A1 || target === A2 || target === A3) && is_sa_without_rd()) {
+ if (filled(SA1)) fn(game.battle.array[SA1])
+ if (filled(SA2)) fn(game.battle.array[SA2])
+ if (filled(SA3)) fn(game.battle.array[SA3])
+ }
}
function prompt_hit_armored_forces() {
let has_armored = false
- for (let p of game.battle.targets) {
- let lord = game.battle.array[p]
+ for_each_target(lord => {
if (get_lord_forces(lord, KNIGHTS) > 0) {
gen_action_knights(lord)
has_armored = true
@@ -7875,13 +7768,12 @@ function prompt_hit_armored_forces() {
gen_action_men_at_arms(lord)
has_armored = true
}
- }
+ })
return has_armored
}
function prompt_hit_unarmored_forces() {
- for (let p of game.battle.targets) {
- let lord = game.battle.array[p]
+ for_each_target(lord => {
if (get_lord_forces(lord, LIGHT_HORSE) > 0)
gen_action_light_horse(lord)
if (get_lord_forces(lord, ASIATIC_HORSE) > 0)
@@ -7890,12 +7782,11 @@ function prompt_hit_unarmored_forces() {
gen_action_militia(lord)
if (get_lord_forces(lord, SERFS) > 0)
gen_action_serfs(lord)
- }
+ })
}
function prompt_hit_forces() {
- for (let p of game.battle.targets) {
- let lord = game.battle.array[p]
+ for_each_target(lord => {
if (get_lord_forces(lord, KNIGHTS) > 0)
gen_action_knights(lord)
if (get_lord_forces(lord, SERGEANTS) > 0)
@@ -7910,7 +7801,7 @@ function prompt_hit_forces() {
gen_action_militia(lord)
if (get_lord_forces(lord, SERFS) > 0)
gen_action_serfs(lord)
- }
+ })
}
states.assign_hits = {
@@ -7922,7 +7813,7 @@ states.assign_hits = {
// TODO: hits or xhits choice
if (game.battle.storm) {
- if (game.active === game.battle.attacker) {
+ if (is_attacker()) {
// Storm - attacker must apply hits to armored first
let has_armored = prompt_hit_armored_forces()
if (!has_armored)
@@ -7938,7 +7829,6 @@ states.assign_hits = {
prompt_hit_forces()
}
}
-
} else {
prompt_hit_forces()
}
@@ -7966,6 +7856,126 @@ states.assign_hits = {
},
}
+function is_striking(pos) {
+ // Future strikes
+ if (has_strike(pos))
+ return true
+ // Remaining hits in current group
+ if (set_has(game.battle.strikers, pos) && game.battle.hits + game.battle.xhits > 0) {
+ let lord = game.battle.array[pos]
+ return count_lord_hits(lord) + count_lord_xhits(lord) > 0
+ }
+ return false
+}
+
+function rout_lord(lord) {
+ log(`L${lord} routed!`)
+
+ let pos = get_lord_array_position(lord)
+
+ // Remove from battle array
+ game.battle.array[pos] = NOBODY
+
+ // Strike left or right or reserve defender
+ if (pos >= A1 && pos <= D3) {
+ if (is_attacker_step())
+ game.battle.fc = strike_left_or_right(is_striking, A2, D1, D2, D3)
+ else
+ game.battle.fc = strike_left_or_right(is_striking, D2, A1, A2, A3)
+ } else {
+ if (is_sa_without_rd()) {
+ game.battle.rc = strike_reserve_defender()
+ } else {
+ if (is_attacker_step())
+ game.battle.rc = strike_left_or_right(is_striking, SA2, RD1, RD2, RD3)
+ else
+ game.battle.rc = strike_left_or_right(is_striking, RD2, SA1, SA2, SA3)
+ }
+ }
+}
+
+function rout_unit(lord, type) {
+ if (lord === GARRISON) {
+ if (type === KNIGHTS)
+ game.battle.garrison.knights--
+ if (type === MEN_AT_ARMS)
+ game.battle.garrison.men_at_arms--
+ if (game.battle.garrison.knights + game.battle.garrison.men_at_arms === 0) {
+ log("Garrison routed.")
+ game.battle.garrison = 0
+ }
+ } else {
+ add_lord_forces(lord, type, -1)
+ add_lord_routed_forces(lord, type, 1)
+ }
+}
+
+function use_warrior_monks(lord, type) {
+ if (type === KNIGHTS) {
+ let bit = 1 << lord
+ if (game.battle.warrior_monks & bit) {
+ game.battle.warrior_monks ^= bit
+ return true
+ }
+ }
+ return false
+}
+
+function action_assign_hits(lord, type) {
+ let protection = FORCE_PROTECTION[type]
+ let evade = FORCE_EVADE[type]
+
+ // TODO: manual choice of hit type
+ let ap = (is_armored_force(type) && game.battle.xhits > 0) ? 2 : 0
+
+ if (type === SERGEANTS || type === MEN_AT_ARMS)
+ if (lord_has_capability(lord, AOW_TEUTONIC_HALBBRUDER))
+ protection += 1
+
+ // Evade only in Battle Melee steps
+ if (evade > 0 && !game.battle.storm && is_melee_step()) {
+ let die = roll_die()
+ if (die <= evade) {
+ logi(`${FORCE_TYPE_NAME[type]} ${die} <= ${evade}`)
+ } else {
+ logi(`${FORCE_TYPE_NAME[type]} ${die} > ${evade}`)
+ rout_unit(lord, type)
+ }
+ } else if (protection > 0) {
+ let die = roll_die()
+ if (die <= protection - ap) {
+ logi(`${FORCE_TYPE_NAME[type]} ${die} <= ${protection - ap}`)
+ } else {
+ logi(`${FORCE_TYPE_NAME[type]} ${die} > ${protection - ap}`)
+ if (use_warrior_monks(lord, type)) {
+ die = roll_die()
+ if (die <= protection - ap) {
+ logi(`Warrior Monks ${die} <= ${protection - ap}`)
+ } else {
+ logi(`Warrior Monks ${die} > ${protection - ap}`)
+ rout_unit(lord, type)
+ }
+ } else {
+ rout_unit(lord, type)
+ }
+ }
+ } else {
+ logi(`${FORCE_TYPE_NAME[type]} unprotected`)
+ rout_unit(lord, type)
+ }
+
+ if (game.battle.xhits)
+ game.battle.xhits--
+ else
+ game.battle.hits--
+
+ if (!lord_has_unrouted_units(lord))
+ // TODO: log list of new targets... after assign left/right
+ rout_lord(lord)
+
+ goto_assign_hits()
+}
+
// === BATTLE: NEW ROUND ===
function end_battle_round() {
@@ -8238,7 +8248,7 @@ function can_retreat_to(to) {
function can_retreat() {
if (game.march) {
// Battle after March
- if (game.active === game.battle.attacker)
+ if (is_attacker())
return can_retreat_to(game.march.from)
for (let [to, way] of data.locales[game.battle.where].ways)
if (way !== game.march.approach && can_retreat_to(to))
@@ -8276,7 +8286,7 @@ states.retreat = {
view.group = game.battle.retreated
if (game.march) {
// after March
- if (game.active === game.battle.attacker) {
+ if (is_attacker()) {
gen_action_locale(game.march.from)
} else {
for (let [to, way] of data.locales[game.battle.where].ways)
@@ -8293,7 +8303,7 @@ states.retreat = {
locale(to) {
push_undo()
if (game.march) {
- if (game.active === game.battle.attacker) {
+ if (is_attacker()) {
game.battle.retreat_to = to
game.battle.retreat_way = game.march.approach
retreat_1()
@@ -8340,7 +8350,7 @@ states.retreat_way = {
function retreat_1() {
// Retreated without having conceded the Field
- if (game.battle.conceded !== game.active) {
+ if (did_not_concede()) {
for (let lord of game.battle.retreated)
transfer_assets_except_ships(lord)
retreat_2()
@@ -8473,11 +8483,12 @@ function action_losses(lord, type) {
if (game.battle.storm) {
// Attackers in a Storm always roll vs 1
- if (game.active === game.battle.attacker)
+ if (is_attacker())
target = 1
} else {
- // Losers in a Battle roll vs 1 if they did not concede (unless they Withdrew)
- if (game.active === game.battle.loser && game.active !== game.battle.conceded)
+ // Losers in a Battle roll vs 1 if they did not concede
+ if (game.active === game.battle.loser && did_not_concede())
+ // unless they withdrow
if (is_lord_unbesieged(lord))
target = 1
}