summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
authorteisuru <31881306+teisuru@users.noreply.github.com>2023-06-01 15:58:24 +0200
committerTor Andersson <tor@ccxvii.net>2023-12-10 18:13:09 +0100
commitd2a919b98e3809d6a0f27c10ee9bb9ac61dbe542 (patch)
tree12bd6002b7b92e9068c377e5ae474989deaba65d /rules.js
parent4150149416bf5018846ef0a09504e361c2ea0fc2 (diff)
downloadplantagenet-d2a919b98e3809d6a0f27c10ee9bb9ac61dbe542.tar.gz
removed global capabilities
no global capabailities in plantagenet
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js7548
1 files changed, 7539 insertions, 9 deletions
diff --git a/rules.js b/rules.js
index 6b18c35..855fb5c 100644
--- a/rules.js
+++ b/rules.js
@@ -1,5 +1,7 @@
"use strict"
+const data = require("./data.js")
+
const BOTH = "Both"
const LANCASTER = "Lancaster"
const YORK = "York"
@@ -84,11 +86,8 @@ const FORCE_EVADE = [ 0, 0, 0, 0, 0, 0, 0 ]
// asset types
const PROV = 0
const COIN = 1
-//const LOOT = 2
const CART = 2
-//const SLED = 4
-//const BOAT = 5
-const SHIP = 4
+const SHIP = 3
const ASSET_TYPE_NAME = [ "Provender", "Coin", "Cart", "Ship" ]
@@ -126,12 +125,13 @@ const vassal_count = data.vassals.length
const first_lord = 0
const last_lord = lord_count - 1
-const first_p1_locale = 0
+/*const first_p1_locale = 0
const last_p1_locale = 23
const first_p2_locale = 24
-const last_p2_locale = 52
+const last_p2_locale = 52 */
const first_locale = 0
-const last_locale = 52
+const last_locale = data.locales.length - 1
+
const first_p1_card = 0
@@ -228,7 +228,7 @@ const LORD_SALISBURY = find_lord("Salisbury")
const LORD_RUTLAND = find_lord("Rutland")
const LORD_PEMBROKE = find_lord("Pembroke")
const LORD_DEVON = find_lord("Devon")
-const LORD_NORTHUMBERLAND = find_lord("Northumberland")
+const LORD_NORTHUMBERLANDY = find_lord("Northumberland")
const LORD_GLOUCESTER = find_lord("Gloucester")
const LORD_RICHARD_III = find_lord("Richard III")
const LORD_NORFOLK = find_lord("Norfolk")
@@ -240,6 +240,7 @@ const LORD_SOMERSET = find_lord("Somerset")
const LORD_EXETER = find_lord("Exeter")
const LORD_BUCKINGHAM = find_lord("Buckingham")
const LORD_CLARENCE = find_lord("Clarence")
+const LORD_NORTHUMBERLANDL = find_lord("Northumberland")
const LORD_JASPER_TUDOR = find_lord("Jasper Tudor")
const LORD_HENRY_TUDOR = find_lord("Henry Tudor")
@@ -308,4 +309,7533 @@ const LOC_ENGLISH_CHANNEL = find_locale("English Channel")
const LOC_IRISH_SEA = find_locale("Irish Sea")
const LOC_NORTH_SEA = find_locale("North Sea")
const LOC_SCARBOROUGH = find_locale("Scarborough")
-const LOC_RAVENSPUR = find_locale("Ravenspur") \ No newline at end of file
+const LOC_RAVENSPUR = find_locale("Ravenspur")
+
+
+
+// === === === === FROM NEVSKY === === === ===
+
+// TODO: log end victory conditions at scenario start
+
+// WONTFIX: choose crossbow/normal hit application order
+
+// Check all push/clear_undo
+
+const VASSAL_UNAVAILABLE = 0
+const VASSAL_READY = 1
+const VASSAL_MUSTERED = 2
+
+const NOBODY = -1
+const NOWHERE = -1
+const NOTHING = -1
+const NEVER = -1
+const CALENDAR = 100
+
+function current_turn() {
+ return game.turn >> 1
+}
+
+function current_turn_name() {
+ return String(game.turn >> 1)
+}
+
+function current_hand() {
+ if (game.active === P1)
+ return game.hand1
+ return game.hand2
+}
+
+function is_campaign_phase() {
+ return (game.turn & 1) === 1
+}
+
+function is_levy_phase() {
+ return (game.turn & 1) === 0
+}
+
+// === GAME STATE ===
+
+const first_p1_lord = 0
+const last_p1_lord = 13
+const first_p2_lord = 14
+const last_p2_lord = 27
+
+let first_friendly_lord = 0
+let last_friendly_lord = 13
+let first_enemy_lord = 14
+let last_enemy_lord = 27
+
+function update_aliases() {
+ if (game.active === P1) {
+ first_friendly_lord = 0
+ last_friendly_lord = 13
+ first_enemy_lord = 14
+ last_enemy_lord = 27
+ } else if (game.active === P2) {
+ first_friendly_lord = 14
+ last_friendly_lord = 27
+ first_enemy_lord = 0
+ last_enemy_lord = 13
+ } else {
+ first_friendly_lord = -1
+ last_friendly_lord = -1
+ first_enemy_lord = -1
+ last_enemy_lord = -1
+ }
+}
+
+function load_state(state) {
+ if (game !== state) {
+ game = state
+ update_aliases()
+ }
+}
+
+function push_state(next) {
+ if (!states[next])
+ throw Error("No such state: " + next)
+ game.stack.push([ game.state, game.who, game.count ])
+ game.state = next
+}
+
+function pop_state() {
+ ;[ game.state, game.who, game.count ] = game.stack.pop()
+}
+
+function set_active(new_active) {
+ if (game.active !== new_active) {
+ game.active = new_active
+ update_aliases()
+ }
+}
+
+function set_active_enemy() {
+ game.active = enemy_player()
+ update_aliases()
+}
+
+function enemy_player() {
+ if (game.active === P1)
+ return P2
+ if (game.active === P2)
+ return P1
+ return null
+}
+
+function has_any_spoils() {
+ return (
+ game.spoils &&
+ game.spoils[PROV] +
+ game.spoils[COIN] +
+ game.spoils[CART] +
+ game.spoils[SHIP] >
+ 0
+ )
+}
+
+function get_spoils(type) {
+ if (game.spoils)
+ return game.spoils[type]
+ return 0
+}
+
+function add_spoils(type, n) {
+ if (!game.spoils)
+ game.spoils = [ 0, 0, 0, 0, 0, 0, 0 ]
+ game.spoils[type] += n
+}
+
+function get_lord_calendar(lord) {
+ if (is_lord_on_calendar(lord))
+ return get_lord_locale(lord) - CALENDAR
+ else
+ return get_lord_service(lord)
+}
+
+function set_lord_cylinder_on_calendar(lord, turn) {
+ if (turn < 0) turn = 0
+ if (turn > 16) turn = 16
+ set_lord_locale(lord, CALENDAR + turn)
+}
+
+function set_lord_calendar(lord, turn) {
+ if (is_lord_on_calendar(lord))
+ set_lord_cylinder_on_calendar(lord, turn)
+ else
+ set_lord_service(lord, turn)
+}
+
+function get_lord_locale(lord) {
+ return game.pieces.locale[lord]
+}
+
+function get_lord_service(lord) {
+ return game.pieces.service[lord]
+}
+
+function get_lord_capability(lord, n) {
+ return game.pieces.capabilities[(lord << 1) + n]
+}
+
+function set_lord_capability(lord, n, x) {
+ game.pieces.capabilities[(lord << 1) + n] = x
+}
+
+function get_lord_assets(lord, n) {
+ return pack4_get(game.pieces.assets[lord], n)
+}
+
+function get_lord_forces(lord, n) {
+ return pack4_get(game.pieces.forces[lord], n)
+}
+
+function get_lord_routed_forces(lord, n) {
+ return pack4_get(game.pieces.routed[lord], n)
+}
+
+function lord_has_unrouted_units(lord) {
+ return game.pieces.forces[lord] !== 0
+}
+
+function lord_has_routed_units(lord) {
+ return game.pieces.routed[lord] !== 0
+}
+
+function set_lord_locale(lord, locale) {
+ game.pieces.locale[lord] = locale
+}
+
+function shift_lord_cylinder(lord, dir) {
+ set_lord_calendar(lord, get_lord_calendar(lord) + dir)
+}
+
+/*function set_lord_service(lord, service) {
+ if (service < 0)
+ service = 0
+ if (service > 17)
+ service = 17
+ game.pieces.service[lord] = service
+}*/
+
+/*function add_lord_service(lord, n) {
+ set_lord_service(lord, get_lord_service(lord) + n)
+}*/
+
+function set_lord_assets(lord, n, x) {
+ if (x < 0)
+ x = 0
+ if (x > 40)
+ x = 40
+ game.pieces.assets[lord] = pack4_set(game.pieces.assets[lord], n, x)
+}
+
+function add_lord_assets(lord, n, x) {
+ set_lord_assets(lord, n, get_lord_assets(lord, n) + x)
+}
+
+function set_lord_forces(lord, n, x) {
+ if (x < 0)
+ x = 0
+ if (x > 15)
+ x = 15
+ game.pieces.forces[lord] = pack4_set(game.pieces.forces[lord], n, x)
+}
+
+function add_lord_forces(lord, n, x) {
+ set_lord_forces(lord, n, get_lord_forces(lord, n) + x)
+}
+
+function set_lord_routed_forces(lord, n, x) {
+ if (x < 0)
+ x = 0
+ if (x > 15)
+ x = 15
+ game.pieces.routed[lord] = pack4_set(game.pieces.routed[lord], n, x)
+}
+
+function add_lord_routed_forces(lord, n, x) {
+ set_lord_routed_forces(lord, n, get_lord_routed_forces(lord, n) + x)
+}
+
+function clear_lords_moved() {
+ game.pieces.moved = 0
+}
+
+function get_lord_moved(lord) {
+ return pack2_get(game.pieces.moved, lord)
+}
+
+function set_lord_moved(lord, x) {
+ game.pieces.moved = pack2_set(game.pieces.moved, lord, x)
+}
+
+function set_lord_fought(lord) {
+ set_lord_moved(lord, 1)
+ game.battle.fought = pack1_set(game.battle.fought, lord, 1)
+}
+
+function get_lord_fought(lord) {
+ return pack1_get(game.battle.fought, lord)
+}
+
+function set_lord_unfed(lord, n) {
+ // reuse "moved" flag for hunger
+ set_lord_moved(lord, n)
+}
+
+function is_lord_unfed(lord) {
+ // reuse "moved" flag for hunger
+ return get_lord_moved(lord)
+}
+
+function feed_lord_skip(lord) {
+ // reuse "moved" flag for hunger
+ set_lord_moved(lord, 0)
+}
+
+function feed_lord(lord) {
+ // reuse "moved" flag for hunger
+ let n = get_lord_moved(lord) - 1
+ set_lord_moved(lord, n)
+ if (n === 0)
+ log(`Fed L${lord}.`)
+}
+
+function get_lord_array_position(lord) {
+ for (let p = 0; p < 12; ++p)
+ if (game.battle.array[p] === lord)
+ return p
+ return -1
+}
+
+/*function add_veche_vp(amount) {
+ game.pieces.veche_vp += amount
+ if (game.pieces.veche_vp < 0)
+ game.pieces.veche_vp = 0
+ if (game.pieces.veche_vp > 8)
+ game.pieces.veche_vp = 8
+}*/
+
+// === GAME STATE HELPERS ===
+
+function roll_die() {
+ return random(6) + 1
+}
+
+function get_shared_assets(loc, what) {
+ let n = 0
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === loc)
+ n += get_lord_assets(lord, what)
+ return n
+}
+
+function count_lord_all_forces(lord) {
+ return (
+ get_lord_forces(lord, RETINUE) +
+ get_lord_forces(lord, VASSAL) +
+ get_lord_forces(lord, BURGUNDIANS) +
+ get_lord_forces(lord, MERCENARIES) +
+ get_lord_forces(lord, MEN_AT_ARMS) +
+ get_lord_forces(lord, MILITIA) +
+ get_lord_forces(lord, LONGBOWMEN)
+ )
+}
+
+/*function count_lord_horses(lord) {
+ return (
+ get_lord_forces(lord, KNIGHTS) +
+ get_lord_forces(lord, SERGEANTS) +
+ get_lord_forces(lord, LIGHT_HORSE) +
+ get_lord_forces(lord, ASIATIC_HORSE)
+ )
+}*/
+
+function count_lord_ships(lord) {
+ let ships = get_lord_assets(lord, SHIP)
+ return ships
+}
+
+function count_shared_ships() {
+ let here = get_lord_locale(game.command)
+ let n = 0
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === here)
+ n += count_lord_ships(lord)
+ return n
+}
+
+function count_group_ships() {
+ let n = 0
+ for (let lord of game.group)
+ n += count_lord_ships(lord)
+ return n
+}
+
+function count_group_assets(type) {
+ let n = 0
+ for (let lord of game.group)
+ n += get_lord_assets(lord, type)
+ return n
+}
+
+function count_group_forces(type) {
+ let n = 0
+ for (let lord of game.group)
+ n += get_lord_forces(lord, type)
+ return n
+}
+/*
+function count_group_horses() {
+ let n = 0
+ for (let lord of game.group)
+ n += count_lord_horses(lord)
+ return n
+}*/
+
+function count_group_transport(type) {
+ let n = 0
+ for (let lord of game.group)
+ n += count_lord_transport(lord, type)
+ return n
+}
+
+function max_plan_length() {
+ switch (current_season()) {
+ case SUMMER:
+ return 6
+ case EARLY_WINTER:
+ return 4
+ case LATE_WINTER:
+ return 4
+ case RASPUTITSA:
+ return 5
+ }
+}
+
+function count_cards_in_plan(plan, lord) {
+ let n = 0
+ for (let c of plan)
+ if (c === lord)
+ ++n
+ return n
+}
+
+/*function is_marshal(lord) {
+ switch (lord) {
+ case LORD_ANDREAS:
+ return true
+ case LORD_HERMANN:
+ return !is_lord_on_map(LORD_ANDREAS)
+ case LORD_ALEKSANDR:
+ return true
+ case LORD_ANDREY:
+ return !is_lord_on_map(LORD_ALEKSANDR)
+ default:
+ return false
+ }
+}*/
+
+function is_armored_force(type) {
+ return type === MEN_AT_ARMS || type === BURGUNDIANS || type === RETINUE || type === VASSAL || type === MERCENARIES
+}
+/*
+function is_no_event_card(c) {
+ if (c === 18 || c === 19 || c === 20)
+ return true
+ if (c === 39 || c === 40 || c === 41)
+ return true
+ return false
+}*/
+
+function is_p1_card(c) {
+ return c >= first_p1_card && c <= last_p1_card_no_event
+}
+
+function is_p2_card(c) {
+ return c >= first_p2_card && c <= last_p2_card_no_event
+}
+
+function is_card_in_use(c) {
+ if (set_has(game.hand1, c))
+ return true
+ if (set_has(game.hand2, c))
+ return true
+ if (set_has(game.events, c))
+ return true
+ if (set_has(game.capabilities, c))
+ return true
+ if (game.pieces.capabilities.includes(c))
+ return true
+ return false
+}
+
+function list_deck() {
+ let deck = []
+ let first_card = (game.active === P1) ? first_p1_card : first_p2_card
+ let last_card = (game.active === P1) ? last_p1_card : last_p2_card
+ for (let c = first_card; c <= last_card; ++c)
+ if (!is_card_in_use(c))
+ deck.push(c)
+ for (let c = last_card + 1; c <= last_card + no; ++c)
+ deck.push(c)
+ return deck
+}
+
+function is_friendly_card(c) {
+ if (game.active === P1)
+ return is_p1_card(c)
+ return is_p2_card(c)
+}
+
+function has_card_in_hand(c) {
+ if (game.active === P1)
+ return set_has(game.hand1, c)
+ return set_has(game.hand2, c)
+}
+
+function can_discard_card(c) {
+ if (set_has(game.hand1, c))
+ return true
+ if (set_has(game.hand2, c))
+ return true
+ if (set_has(game.capabilities, c))
+ return true
+ if (game.pieces.capabilities.includes(c))
+ return true
+}
+
+function is_lord_on_map(lord) {
+ let loc = get_lord_locale(lord)
+ return loc !== NOWHERE && loc < CALENDAR
+}
+
+function is_lord_in_play(lord) {
+ return get_lord_locale(lord) !== NOWHERE
+}
+/*
+function is_lord_besieged(lord) {
+ return false
+}
+
+function is_lord_unbesieged(lord) {
+ return true
+}*/
+
+function is_lord_on_calendar(lord) {
+ let loc = get_lord_locale(lord)
+ return loc >= CALENDAR
+}
+
+function is_lord_ready(lord) {
+ let loc = get_lord_locale(lord)
+ return loc >= CALENDAR && loc <= CALENDAR + (game.turn >> 1)
+}
+/*
+function is_special_vassal_available(vassal) {
+ let cap = data.vassals[vassal].capability
+ if (cap === "Crusade")
+ return has_global_capability(AOW_TEUTONIC_CRUSADE)
+ if (cap === "Steppe Warriors")
+ return has_global_capability(AOW_RUSSIAN_STEPPE_WARRIORS)
+ return true
+}*/
+
+function is_vassal_ready(vassal) {
+ return game.pieces.vassals[vassal] === VASSAL_READY
+}
+
+function is_vassal_mustered(vassal) {
+ return game.pieces.vassals[vassal] === VASSAL_MUSTERED
+}
+
+function is_york_lord(lord) {
+ return lord >= first_p1_lord && lord <= last_p1_lord
+}
+
+function is_lancaster_lord(lord) {
+ return lord >= first_p2_lord && lord <= last_p2_lord
+}
+
+function is_p1_lord(lord) {
+ return lord >= first_p1_lord && lord <= last_p1_lord
+}
+
+function is_friendly_lord(lord) {
+ return lord >= first_friendly_lord && lord <= last_friendly_lord
+}
+
+function is_lord_at_friendly_locale(lord) {
+ let loc = get_lord_locale(lord)
+ return is_friendly_locale(loc)
+}
+/*
+function used_seat_capability(lord, where, extra) {
+ let seats = data.lords[lord].seats
+ if (extra) {
+ if (set_has(seats, where) && !extra.includes(where))
+ return -1
+ } else {
+ if (set_has(seats, where))
+ return -1
+ }
+ if (is_teutonic_lord(lord))
+ if (has_global_capability(AOW_TEUTONIC_ORDENSBURGEN))
+ return AOW_TEUTONIC_ORDENSBURGEN
+ if (is_russian_lord(lord))
+ if (has_global_capability(AOW_RUSSIAN_ARCHBISHOPRIC))
+ return AOW_RUSSIAN_ARCHBISHOPRIC
+ return -1
+}*/
+/*
+function for_each_seat(lord, fn, repeat = false) {
+ let list = data.lords[lord].seats
+
+ for (let seat of list)
+ fn(seat)
+
+ if (is_teutonic_lord(lord)) {
+ if (has_global_capability(AOW_TEUTONIC_ORDENSBURGEN)) {
+ for (let commandery of COMMANDERIES)
+ if (repeat || !set_has(list, commandery))
+ fn(commandery)
+ }
+ }
+
+ if (is_russian_lord(lord)) {
+ if (has_global_capability(AOW_RUSSIAN_ARCHBISHOPRIC))
+ if (repeat || !set_has(list, LOC_NOVGOROD))
+ fn(LOC_NOVGOROD)
+ }
+
+ if (lord === LORD_YAROSLAV) {
+ if (has_conquered_marker(LOC_PSKOV))
+ if (repeat || !set_has(list, LOC_PSKOV))
+ fn(LOC_PSKOV)
+ }
+}*/
+
+function is_lord_seat(lord, here) {
+ let result = false
+ for_each_seat(lord, seat => {
+ if (seat === here)
+ result = true
+ })
+ return result
+}
+
+function is_lord_at_seat(lord) {
+ return is_lord_seat(lord, get_lord_locale(lord))
+}
+
+function has_free_seat(lord) {
+ let result = false
+ for_each_seat(lord, seat => {
+ if (!result && is_friendly_locale(seat))
+ result = true
+ })
+ return result
+}
+
+function has_york_lord(here) {
+ for (let lord = first_p1_lord; lord <= last_p1_lord; ++lord)
+ if (get_lord_locale(lord) === here)
+ return true
+}
+
+function has_lancaster_lord(here) {
+ for (let lord = first_p2_lord; lord <= last_p2_lord; ++lord)
+ if (get_lord_locale(lord) === here)
+ return true
+}
+
+function has_friendly_lord(loc) {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === loc)
+ return true
+ return false
+}
+/*
+function has_besieged_friendly_lord(loc) {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === loc && is_lord_besieged(lord))
+ return true
+ return false
+}*/
+
+function has_enemy_lord(loc) {
+ for (let lord = first_enemy_lord; lord <= last_enemy_lord; ++lord)
+ if (get_lord_locale(lord) === loc)
+ return true
+ return false
+}
+/*
+function has_unbesieged_enemy_lord(loc) {
+ for (let lord = first_enemy_lord; lord <= last_enemy_lord; ++lord)
+ if (get_lord_locale(lord) === loc && is_lord_unbesieged(lord))
+ return true
+ return false
+}
+
+function has_unbesieged_friendly_lord(loc) {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === loc && is_lord_unbesieged(lord))
+ return true
+ return false
+}
+*/
+function is_p1_locale(loc) {
+ return loc >= first_p1_locale && loc <= last_p1_locale
+}
+
+function is_p2_locale(loc) {
+ return loc >= first_p2_locale && loc <= last_p2_locale
+}
+/*
+function is_friendly_territory(loc) {
+ if (game.active === P1)
+ return loc >= first_p1_locale && loc <= last_p1_locale
+ return loc >= first_p2_locale && loc <= last_p2_locale
+}
+
+function is_enemy_territory(loc) {
+ if (game.active === P1)
+ return loc >= first_p2_locale && loc <= last_p2_locale
+ return loc >= first_p1_locale && loc <= last_p1_locale
+}*/
+
+function is_seaport(loc) {
+ return set_has(data.seaports, loc)
+}
+
+function is_city(loc) {
+ return data.locales[loc].type === "city"
+}
+
+function is_town(loc) {
+ return data.locales[loc].type === "town"
+}
+
+function is_fortress(loc) {
+ return data.locales[loc].type === "fortress"
+}
+
+function is_calais(loc) {
+ return data.locales[loc].type === "calais"
+}
+
+function is_sea(loc) {
+ return data.locales[loc].type === "sea"
+}
+
+function is_london(loc) {
+ return data.locales[loc].type === "london"
+}
+
+function is_harlech(loc) {
+ return data.locales[loc].type === "harlech"
+}
+
+function is_stronghold(loc) {
+ return data.locales[loc].stronghold > 0
+}
+
+function has_favour_marker(loc) {
+ return set_has(game.pieces.favour, loc)
+}
+
+function add_favour_marker(loc) {
+ set_add(game.pieces.favour, loc)
+}
+
+function remove_favour_marker(loc) {
+ set_delete(game.pieces.favour, loc)
+}
+
+function has_exhausted_marker(loc) {
+ return set_has(game.pieces.exhausted, loc)
+}
+
+function add_exhausted_marker(loc) {
+ set_add(game.pieces.exhausted, loc)
+}
+
+function remove_exhausted_marker(loc) {
+ set_delete(game.pieces.ravaged, loc)
+}
+/*
+function conquer_trade_route(loc) {
+ if (game.active === RUSSIANS) {
+ if (has_conquered_marker(loc)) {
+ log(`Conquered %${loc}.`)
+ remove_conquered_marker(loc)
+ }
+ } else {
+ if (!has_conquered_marker(loc)) {
+ log(`Conquered %${loc}.`)
+ add_conquered_marker(loc)
+ }
+ }
+}
+
+
+function conquer_stronghold(loc) {
+ if (has_castle_marker(loc))
+ flip_castle(loc)
+
+ remove_all_siege_markers(loc)
+
+ if (is_enemy_territory(loc))
+ add_conquered_marker(loc)
+ else
+ remove_conquered_marker(loc)
+}
+
+function count_castles() {
+ return game.pieces.castles1.length + game.pieces.castles2.length
+}
+
+function add_friendly_castle(loc) {
+ // only P1 can add
+ set_add(game.pieces.castles1, loc)
+}
+
+function has_enemy_castle(loc) {
+ if (game.active === P1)
+ return set_has(game.pieces.castles2, loc)
+ return set_has(game.pieces.castles1, loc)
+}
+
+function has_friendly_castle(loc) {
+ if (game.active === P1)
+ return set_has(game.pieces.castles1, loc)
+ return set_has(game.pieces.castles2, loc)
+}
+
+function has_castle_marker(loc) {
+ return (
+ set_has(game.pieces.castles1, loc) ||
+ set_has(game.pieces.castles2, loc)
+ )
+}
+
+function flip_castle(loc) {
+ if (game.active === P1) {
+ set_delete(game.pieces.castles2, loc)
+ set_add(game.pieces.castles1, loc)
+ } else {
+ set_delete(game.pieces.castles1, loc)
+ set_add(game.pieces.castles2, loc)
+ }
+}
+
+
+function is_friendly_stronghold_locale(loc) {
+ if (is_stronghold(loc) || has_friendly_castle(loc))
+ return is_friendly_locale(loc)
+ return false
+}
+function is_enemy_stronghold(loc) {
+ if (is_stronghold(loc)) {
+ if (is_enemy_territory(loc) && !has_conquered_marker(loc))
+ return true
+ if (is_friendly_territory(loc) && has_conquered_marker(loc))
+ return true
+ }
+ if (has_enemy_castle(loc))
+ return true
+ return false
+}
+
+function is_friendly_stronghold(loc) {
+ if (is_stronghold(loc)) {
+ if (is_friendly_territory(loc) && !has_conquered_marker(loc))
+ return true
+ if (is_enemy_territory(loc) && has_conquered_marker(loc))
+ return true
+ }
+ if (has_friendly_castle(loc))
+ return true
+ return false
+}
+/*
+function is_unbesieged_enemy_stronghold(loc) {
+ return is_enemy_stronghold(loc) && !has_siege_marker(loc)
+}
+
+function is_unbesieged_friendly_stronghold(loc) {
+ return is_friendly_stronghold(loc) && !has_siege_marker(loc)
+}
+
+function is_besieged_enemy_stronghold(loc) {
+ return is_enemy_stronghold(loc) && has_siege_marker(loc)
+}
+*/
+function is_friendly_locale(loc) {
+ if (loc !== NOWHERE && loc < CALENDAR) {
+ if (has_enemy_lord(loc))
+ return false
+ if (has_favour_marker(loc)) { //to add friendly favour marker later
+ return true
+ }
+ }
+ return false
+}
+
+function can_add_transport(who, what) {
+ return get_lord_assets(who, what) < 100
+}
+
+function count_lord_transport(lord, type) {
+ let season = current_season()
+ let n = 0
+ n += get_lord_assets(lord, CART)
+ return n
+}
+
+function list_ways(from, to) {
+ for (let ways of data.locales[from].ways)
+ if (ways[0] === to)
+ return ways
+ return null
+}
+/*
+function is_upper_lord(lord) {
+ return map_has(game.pieces.lieutenants, lord)
+}
+
+function is_lower_lord(lord) {
+ for (let i = 1; i < game.pieces.lieutenants.length; i += 2)
+ if (game.pieces.lieutenants[i] === lord)
+ return true
+ return false
+}
+
+function get_lower_lord(upper) {
+ return map_get(game.pieces.lieutenants, upper, NOBODY)
+}
+
+function set_lower_lord(upper, lower) {
+ map_set(game.pieces.lieutenants, upper, lower)
+}
+
+function add_lieutenant(upper) {
+ map_set(game.pieces.lieutenants, upper, NOBODY)
+}
+
+function remove_lieutenant(lord) {
+ for (let i = 0; i < game.pieces.lieutenants.length; i += 2) {
+ if (game.pieces.lieutenants[i] === lord || game.pieces.lieutenants[i + 1] === lord) {
+ array_remove_pair(game.pieces.lieutenants, i)
+ return
+ }
+ }
+}*/
+
+function group_has_capability(c) {
+ for (let lord of game.group)
+ if (lord_has_capability(lord, c))
+ return true
+ return false
+}
+/*
+function count_unbesieged_friendly_lords(loc) {
+ let n = 0
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === loc && is_lord_unbesieged(lord))
+ ++n
+ return n
+}
+*/
+// === MAP ===
+
+function calculate_distance(start, adjacent) {
+ let queue = []
+ queue.push([start, 0])
+
+ let distance = new Array(last_locale+1).fill(-1)
+ distance[start] = 0
+
+ while (queue.length > 0) {
+ let [ here, d ] = queue.shift()
+ for (let next of data.locales[here][adjacent]) {
+ if (distance[next] < 0) {
+ distance[next] = d+1
+ queue.push([next, d+1])
+ }
+ }
+ }
+
+ return distance
+}
+
+for (let loc = 0; loc <= last_locale; ++loc)
+ data.locales[loc].distance = calculate_distance(loc, "adjacent")
+
+function locale_distance(from, to) {
+ return data.locales[from].distance[to]
+}
+
+// === SETUP ===
+
+function muster_lord_forces(lord) {
+ let info = data.lords[lord]
+ set_lord_forces(lord, RETINUE, info.forces.retinue | 0)
+ set_lord_forces(lord, MEN_AT_ARMS, info.forces.men_at_arms | 0)
+ set_lord_forces(lord, MILITIA, info.forces.militia | 0)
+ set_lord_forces(lord, LONGBOWMEN, info.forces.longbowmen | 0)
+}
+/*
+function muster_vassal_forces(lord, vassal) {
+ let info = data.vassals[vassal]
+ add_lord_forces(lord, RETINUE, info.forces.knights | 0)
+ add_lord_forces(lord, SERGEANTS, info.forces.sergeants | 0)
+ add_lord_forces(lord, LIGHT_HORSE, info.forces.light_horse | 0)
+ add_lord_forces(lord, ASIATIC_HORSE, info.forces.asiatic_horse | 0)
+ add_lord_forces(lord, MEN_AT_ARMS, info.forces.men_at_arms | 0)
+ add_lord_forces(lord, MILITIA, info.forces.militia | 0)
+ add_lord_forces(lord, SERFS, info.forces.serfs | 0)
+}*/
+
+function restore_lord_forces(lord, type, count) {
+ if (get_lord_forces(lord, type) < count) {
+ set_lord_forces(lord, type, count)
+ return 1
+ }
+ return 0
+}
+
+function muster_lord(lord, locale) {
+ let info = data.lords[lord]
+
+ set_lord_locale(lord, locale)
+
+ set_lord_assets(lord, PROV, info.assets.prov | 0)
+ set_lord_assets(lord, COIN, info.assets.coin | 0)
+
+ set_lord_assets(lord, CART, info.assets.cart | 0)
+ set_lord_assets(lord, SHIP, info.assets.ship | 0)
+
+ muster_lord_forces(lord)
+}
+/*
+function disband_vassal(vassal) {
+ let info = data.vassals[vassal]
+ let lord = data.vassals[vassal].lord
+
+ add_lord_forces(lord, KNIGHTS, -(info.forces.knights | 0))
+ add_lord_forces(lord, SERGEANTS, -(info.forces.sergeants | 0))
+ add_lord_forces(lord, LIGHT_HORSE, -(info.forces.light_horse | 0))
+ add_lord_forces(lord, ASIATIC_HORSE, -(info.forces.asiatic_horse | 0))
+ add_lord_forces(lord, MEN_AT_ARMS, -(info.forces.men_at_arms | 0))
+ add_lord_forces(lord, MILITIA, -(info.forces.militia | 0))
+ add_lord_forces(lord, SERFS, -(info.forces.serfs | 0))
+
+ game.pieces.vassals[vassal] = VASSAL_READY
+
+ if (!lord_has_unrouted_units(lord)) {
+ disband_lord(lord)
+ }
+} */
+
+function muster_vassal(lord, vassal) {
+ game.pieces.vassals[vassal] = VASSAL_MUSTERED
+ muster_vassal_forces(lord, vassal)
+}
+
+function draw_card(deck) {
+ let i = random(deck.length)
+ let c = deck[i]
+ set_delete(deck, c)
+ return c
+}
+
+function discard_events(when) {
+ for (let i = 0; i < game.events.length; ) {
+ let c = game.events[i]
+ if (data.cards[c].when === when)
+ array_remove(game.events, i)
+ else
+ ++i
+ }
+}
+
+function discard_friendly_events(when) {
+ for (let i = 0; i < game.events.length; ) {
+ let c = game.events[i]
+ if (is_friendly_card(c) && data.cards[c].when === when)
+ array_remove(game.events, i)
+ else
+ ++i
+ }
+}
+
+exports.setup = function (seed, scenario, options) {
+ game = {
+ seed,
+ scenario,
+ hidden: options.hidden ? 1 : 0,
+
+ log: [],
+ undo: [],
+
+ active: P1,
+ state: "setup_lords",
+ stack: [],
+
+ hand1: [],
+ hand2: [],
+ plan1: [],
+ plan2: [],
+
+ turn: 0,
+ events: [], // this levy/this campaign cards
+ capabilities: [], // global capabilities
+
+ pieces: {
+ locale: Array(lord_count).fill(NOWHERE),
+ service: Array(lord_count).fill(NEVER),
+ assets: Array(lord_count).fill(0),
+ forces: Array(lord_count).fill(0),
+ routed: Array(lord_count).fill(0),
+ capabilities: Array(lord_count << 1).fill(NOTHING),
+ besieged: 0,
+ moved: 0,
+ vassals: Array(vassal_count).fill(VASSAL_UNAVAILABLE),
+ favour: [],
+ },
+
+ flags: {
+ first_action: 0,
+ first_march: 0,
+ },
+
+ command: NOBODY,
+ actions: 0,
+ group: 0,
+ who: NOBODY,
+ where: NOWHERE,
+ what: NOTHING,
+ count: 0,
+
+ supply: 0,
+ march: 0,
+ battle: 0,
+ spoils: 0,
+ }
+
+ update_aliases()
+
+ log_h1(scenario)
+
+ switch (scenario) {
+ default:
+ case "Ia. Henry VI":
+ setup_Ia()
+ break
+ }
+
+ return game
+}
+
+function setup_Ia() {
+ game.turn = 1 << 1
+
+ muster_lord(LORD_YORK, LOC_ELY)
+ muster_lord(LORD_MARCH, LOC_LUDLOW)
+ muster_lord(LORD_HENRYVI, LOC_LONDON)
+ muster_lord(LORD_SOMERSET, LOC_WELLS)
+
+ set_lord_cylinder_on_calendar(LORD_NORTHUMBERLANDL, 2)
+ set_lord_cylinder_on_calendar(LORD_EXETER, 3)
+ set_lord_cylinder_on_calendar(LORD_BUCKINGHAM, 5)
+ set_lord_cylinder_on_calendar(LORD_SALISBURY, 2)
+ set_lord_cylinder_on_calendar(LORD_WARWICKY, 3)
+ set_lord_cylinder_on_calendar(LORD_RUTLAND, 5)
+}
+/*
+states.setup_lords = {
+ inactive: "Set up Lords",
+ prompt() {
+ view.prompt = "Set up your Lords."
+ let done = true
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (is_lord_on_map(lord) && !get_lord_moved(lord)) {
+ if (data.lords[lord].assets.transport > 0) {
+ gen_action_lord(lord)
+ done = false
+ }
+ }
+ }
+ if (done) {
+ view.prompt += " All done."
+ view.actions.end_setup = 1
+ }
+ },
+
+ lord(lord) {
+ push_undo()
+
+ // FIXME: clean up these transitions
+ push_state("muster_lord_transport")
+ set_lord_moved(lord, 1)
+ game.who = lord
+ game.count = data.lords[lord].assets.transport
+ },
+
+} */
+
+
+function end_setup() {
+ clear_lords_moved()
+ set_active_enemy()
+ if (game.active === P1) {
+ log_h1("Levy " + current_turn_name())
+ goto_levy_arts_of_war_first()
+ }
+}
+
+// === EVENTS ===
+
+function is_event_in_play(c) {
+ return set_has(game.events, c)
+}
+
+function is_ravens_rock_in_play() {
+ if (game.battle.round <= 1 && is_melee_step()) {
+ if (game.active === RUSSIANS)
+ return is_event_in_play(EVENT_RUSSIAN_RAVENS_ROCK)
+ }
+ return false
+}
+
+function is_marsh_in_play() {
+ if (game.battle.round <= 2) {
+ if (game.active === TEUTONS && is_event_in_play(EVENT_RUSSIAN_MARSH))
+ return true
+ if (game.active === RUSSIANS && is_event_in_play(EVENT_TEUTONIC_MARSH))
+ return true
+ }
+ return false
+}
+
+function is_hill_in_play() {
+ if (game.battle.round <= 2) {
+ if (game.active === TEUTONS && is_event_in_play(EVENT_TEUTONIC_HILL))
+ return true
+ if (game.active === RUSSIANS && is_event_in_play(EVENT_RUSSIAN_HILL))
+ return true
+ }
+ return false
+}
+
+function is_famine_in_play() {
+ if (game.active === TEUTONS)
+ if (is_event_in_play(EVENT_RUSSIAN_FAMINE))
+ return true
+ if (game.active === RUSSIANS)
+ if (is_event_in_play(EVENT_TEUTONIC_FAMINE))
+ return true
+ return false
+}
+
+function no_muster_of_or_by_lord(lord) {
+ if (lord === LORD_KNUD_ABEL)
+ return is_event_in_play(EVENT_RUSSIAN_VALDEMAR)
+ if (lord === LORD_ANDREAS || lord === LORD_RUDOLF)
+ return is_event_in_play(EVENT_RUSSIAN_DIETRICH_VON_GRUNINGEN)
+ return false
+}
+
+function goto_immediate_event(c) {
+ switch (c) {
+ // This Levy / Campaign
+ case EVENT_TEUTONIC_FAMINE:
+ case EVENT_RUSSIAN_FAMINE:
+ set_add(game.events, c)
+ // No immediate effects
+ return end_immediate_event()
+ case EVENT_RUSSIAN_DEATH_OF_THE_POPE:
+ set_add(game.events, c)
+ return goto_russian_event_death_of_the_pope()
+ case EVENT_RUSSIAN_VALDEMAR:
+ set_add(game.events, c)
+ return goto_russian_event_valdemar()
+ case EVENT_RUSSIAN_DIETRICH_VON_GRUNINGEN:
+ set_add(game.events, c)
+ return goto_russian_event_dietrich()
+
+ // Add to capabilities...
+ case EVENT_TEUTONIC_POPE_GREGORY:
+ deploy_global_capability(c)
+ return goto_teutonic_event_pope_gregory()
+
+ // Discard
+ case EVENT_TEUTONIC_GRAND_PRINCE:
+ return goto_teutonic_event_grand_prince()
+ case EVENT_TEUTONIC_KHAN_BATY:
+ return goto_teutonic_event_khan_baty()
+ case EVENT_TEUTONIC_SWEDISH_CRUSADE:
+ return goto_teutonic_event_swedish_crusade()
+ case EVENT_RUSSIAN_OSILIAN_REVOLT:
+ return goto_russian_event_osilian_revolt()
+ case EVENT_RUSSIAN_BATU_KHAN:
+ return goto_russian_event_batu_khan()
+ case EVENT_RUSSIAN_PRUSSIAN_REVOLT:
+ return goto_russian_event_prussian_revolt()
+ case EVENT_TEUTONIC_BOUNTIFUL_HARVEST:
+ return goto_event_bountiful_harvest()
+ case EVENT_RUSSIAN_BOUNTIFUL_HARVEST:
+ return goto_event_bountiful_harvest()
+ case EVENT_TEUTONIC_MINDAUGAS:
+ return goto_teutonic_event_mindaugas()
+ case EVENT_RUSSIAN_MINDAUGAS:
+ return goto_russian_event_mindaugas()
+ case EVENT_TEUTONIC_TORZHOK:
+ return goto_teutonic_event_torzhok()
+ case EVENT_RUSSIAN_TEMPEST:
+ return goto_russian_event_tempest()
+
+ default:
+ log("NOT IMPLEMENTED")
+ return end_immediate_event()
+ }
+}
+
+function end_immediate_event() {
+ clear_undo()
+ resume_levy_arts_of_war()
+}
+
+// === EVENTS: UNIQUE IMMEDIATE EVENTS ===
+
+// === EVENTS: SHIFT LORD OR SERVICE (IMMEDIATE) ===
+/*
+function prompt_shift_lord_on_calendar(boxes) {
+ if (game.who !== NOBODY) {
+ // Shift in direction beneficial to active player.
+ if (is_friendly_lord(game.who)) {
+ if (is_lord_on_calendar(game.who))
+ gen_action_calendar(get_lord_calendar(game.who) - boxes)
+ else
+ gen_action_calendar(get_lord_calendar(game.who) + boxes)
+ } else {
+ if (is_lord_on_calendar(game.who))
+ gen_action_calendar(get_lord_calendar(game.who) + boxes)
+ else
+ gen_action_calendar(get_lord_calendar(game.who) - boxes)
+ }
+ }
+}
+*/
+// === EVENTS: HOLD ===
+
+function play_held_event(c) {
+ log(`Played E${c}.`)
+ if (c >= first_p1_card && c <= last_p1_card_no_event)
+ set_delete(game.hand1, c)
+ else
+ set_delete(game.hand2, c)
+}
+
+function end_held_event() {
+ pop_state()
+ game.what = NOTHING
+}
+
+function prompt_held_event() {
+ for (let c of current_hand())
+ if (can_play_held_event(c))
+ gen_action_card(c)
+}
+
+function prompt_held_event_lordship() {
+ for (let c of current_hand())
+ if (can_play_held_event(c) || can_play_held_event_lordship(c))
+ gen_action_card(c)
+}
+
+function can_play_held_event(c) {
+ switch (c) {
+ }
+ return false
+}
+
+function can_play_held_event_lordship(c) {
+ switch (c) {
+ }
+ return false
+}
+
+function action_held_event(c) {
+ push_undo()
+ play_held_event(c)
+ game.what = c
+ goto_held_event(c)
+}
+
+function goto_held_event(c) {
+ switch (c) {
+ }
+}
+
+// === EVENTS: HOLD - UNIQUE ===
+
+// === EVENTS: HOLD - SHIFT CYLINDER ===
+
+function action_held_event_lordship(c) {
+ push_undo()
+ play_held_event(c)
+ if (can_play_held_event(c)) {
+ goto_held_event(c)
+ game.what = c
+ } else {
+ push_state("lordship")
+ game.what = c
+ }
+}
+/*
+states.lordship = {
+ get inactive() {
+ return data.cards[game.what].event
+ },
+ prompt() {
+ view.prompt = `${data.cards[game.what].event}: Play for +2 Lordship.`
+ view.actions.lordship = 1
+ },
+ lordship() {
+ end_held_event()
+ log("+2 Lordship")
+ game.count += 2
+ }
+}*/
+
+function prompt_shift_cylinder(list, boxes) {
+
+ // HACK: look at parent state to see if this can be used as a +2 Lordship event
+ let lordship = NOBODY
+ let parent = game.stack[game.stack.length-1]
+ if (parent[0] === "levy_muster_lord")
+ lordship = parent[1]
+
+ let names
+ if (game.what === EVENT_RUSSIAN_PRINCE_OF_POLOTSK)
+ names = "a Russian Lord"
+ else
+ names = list.filter(lord => is_lord_on_calendar(lord)).map(lord => lord_name[lord]).join(" or ")
+
+ if (boxes === 1)
+ view.prompt = `${data.cards[game.what].event}: Shift ${names} 1 Calendar box`
+ else
+ view.prompt = `${data.cards[game.what].event}: Shift ${names} 2 Calendar boxes`
+
+ for (let lord of list) {
+ if (lord === lordship) {
+ view.prompt += " or +2 Lordship"
+ view.actions.lordship = 1
+ }
+ if (is_lord_on_calendar(lord))
+ prompt_select_lord(lord)
+ }
+
+ view.prompt += "."
+
+ prompt_shift_lord_on_calendar(boxes)
+}
+/*
+function action_shift_cylinder_calendar(turn) {
+ log(`Shifted L${game.who} to ${turn}.`)
+ set_lord_calendar(game.who, turn)
+ game.who = NOBODY
+ end_held_event()
+}
+
+function action_shift_cylinder_lordship() {
+ end_held_event()
+ log("+2 Lordship")
+ game.count += 2
+}*/
+
+// === CAPABILITIES ===
+
+// === LEVY: ARTS OF WAR (FIRST TURN) ===
+
+function draw_two_cards() {
+ let deck = list_deck()
+ return [ draw_card(deck), draw_card(deck) ]
+}
+
+function discard_card_capability(c) {
+ log(`${game.active} discarded C${c}.`)
+}
+
+function discard_card_event(c) {
+ log(`${game.active} discarded E${c}.`)
+}
+
+function goto_levy_arts_of_war_first() {
+ if (game.active === TEUTONS)
+ log_h2("Teutonic Arts of War")
+ else
+ log_h2("Russian Arts of War")
+ game.state = "levy_arts_of_war_first"
+ game.what = draw_two_cards()
+}
+
+function resume_levy_arts_of_war_first() {
+ if (game.what.length === 0)
+ end_levy_arts_of_war_first()
+}
+
+states.levy_arts_of_war_first = {
+ inactive: "Arts of War",
+ prompt() {
+ let c = game.what[0]
+ view.arts_of_war = game.what
+ view.what = c
+ if (is_no_event_card(c)) {
+ view.prompt = `Arts of War: No Capability.`
+ view.actions.discard = 1
+ } else if (data.cards[c].this_lord) {
+ let discard = true
+ for (let lord of data.cards[c].lords) {
+ if (is_lord_on_map(lord) && !lord_already_has_capability(lord, c)) {
+ gen_action_lord(lord)
+ discard = false
+ }
+ }
+ if (discard) {
+ view.prompt = `Arts of War: Discard ${data.cards[c].capability}.`
+ view.actions.discard = 1
+ } else {
+ view.prompt = `Arts of War: Assign ${data.cards[c].capability} to a Lord.`
+ }
+ } else {
+ view.prompt = `Arts of War: Deploy ${data.cards[c].capability}.`
+ view.actions.deploy = 1
+ }
+ },
+ lord(lord) {
+ push_undo()
+ let c = game.what.shift()
+ log(`${game.active} deployed Capability.`)
+ add_lord_capability(lord, c)
+ resume_levy_arts_of_war_first()
+ },
+ deploy() {
+ push_undo()
+ let c = game.what.shift()
+ log(`${game.active} deployed C${c}.`)
+ deploy_global_capability(c)
+ resume_levy_arts_of_war_first()
+ },
+ discard() {
+ push_undo()
+ let c = game.what.shift()
+ discard_card_capability(c)
+ resume_levy_arts_of_war_first()
+ },
+}
+
+
+function end_levy_arts_of_war_first() {
+ clear_undo()
+ game.what = NOTHING
+ set_active_enemy()
+ if (game.active === P2)
+ goto_levy_arts_of_war_first()
+ else
+ goto_pay()
+}
+
+// === LEVY: ARTS OF WAR ===
+
+function goto_levy_arts_of_war() {
+ if (game.active === TEUTONS)
+ log_h2("Teutonic Arts of War")
+ else
+ log_h2("Russian Arts of War")
+ game.what = draw_two_cards()
+ resume_levy_arts_of_war()
+}
+
+function resume_levy_arts_of_war() {
+ game.state = "levy_arts_of_war"
+ if (game.what.length === 0)
+ end_levy_arts_of_war()
+}
+
+states.levy_arts_of_war = {
+ inactive: "Arts of War",
+ prompt() {
+ let c = game.what[0]
+ view.arts_of_war = [ c ]
+ view.what = c
+ switch (data.cards[c].when) {
+ case "this_levy":
+ case "this_campaign":
+ case "now":
+ view.prompt = `Arts of War: Play ${data.cards[c].event}.`
+ view.actions.play = 1
+ break
+ case "hold":
+ view.prompt = `Arts of War: Hold ${data.cards[c].event}.`
+ view.actions.hold = 1
+ break
+ case "never":
+ view.prompt = `Arts of War: Discard ${data.cards[c].event}.`
+ view.actions.discard = 1
+ break
+ }
+ },
+ play() {
+ let c = game.what.shift()
+ log(`${game.active} played E${c}.`)
+ goto_immediate_event(c)
+ },
+ hold() {
+ let c = game.what.shift()
+ log(`${game.active} Held Event.`)
+ if (game.active === P1)
+ set_add(game.hand1, c)
+ else
+ set_add(game.hand2, c)
+ resume_levy_arts_of_war()
+ },
+ discard() {
+ let c = game.what.shift()
+ discard_card_event(c)
+ resume_levy_arts_of_war()
+ },
+}
+
+function end_levy_arts_of_war() {
+ game.what = NOTHING
+ set_active_enemy()
+ if (game.active === P2)
+ goto_levy_arts_of_war()
+ else
+ goto_pay()
+}
+
+// === LEVY: MUSTER ===
+
+function goto_levy_muster() {
+ if (game.active === TEUTONS)
+ log_h2("Teutonic Muster")
+ else
+ log_h2("Russian Muster")
+ game.state = "levy_muster"
+}
+
+function end_levy_muster() {
+ clear_lords_moved()
+ set_active_enemy()
+ if (game.active === P2)
+ goto_levy_muster()
+ else
+ goto_levy_call_to_arms()
+}
+
+states.levy_muster = {
+ inactive: "Muster",
+ prompt() {
+ view.prompt = "Levy: Muster with your Lords."
+
+ prompt_held_event()
+
+ let done = true
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (is_lord_at_friendly_locale(lord) && !get_lord_moved(lord)) {
+ if (!no_muster_of_or_by_lord(lord)) {
+ gen_action_lord(lord)
+ done = false
+ }
+ }
+ }
+ if (done) {
+ view.prompt += " All done."
+ view.actions.end_muster = 1
+ }
+ },
+ lord(lord) {
+ push_undo()
+ log(`Mustered with L${lord}.`)
+ push_state("levy_muster_lord")
+ game.who = lord
+ game.count = data.lords[lord].lordship
+ },
+ end_muster() {
+ clear_undo()
+ end_levy_muster()
+ },
+ card: action_held_event,
+}
+
+function resume_levy_muster_lord() {
+ --game.count
+ if (game.count === 0) {
+ set_lord_moved(game.who, 1)
+ pop_state()
+ }
+}
+
+states.levy_muster_lord = {
+ inactive: "Muster",
+ prompt() {
+ if (game.count === 1)
+ view.prompt = `Levy: ${lord_name[game.who]} has ${game.count} action.`
+ else
+ view.prompt = `Levy: ${lord_name[game.who]} has ${game.count} actions.`
+
+ prompt_held_event_lordship()
+
+ if (game.count > 0) {
+ // Roll to muster Ready Lord at Seat
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (no_muster_of_or_by_lord(lord))
+ continue
+ if (is_lord_ready(lord) && has_free_seat(lord))
+ gen_action_lord(lord)
+ }
+
+ // Muster Ready Vassal Forces
+ for (let vassal of data.lords[game.who].vassals) {
+ if (is_vassal_ready(vassal))
+ gen_action_vassal(vassal)
+ }
+
+ // Add Transport
+ if (data.lords[game.who].ships) {
+ if (can_add_transport(game.who, SHIP))
+ view.actions.take_ship = 1
+ }
+ if (can_add_transport(game.who, CART))
+ view.actions.take_cart = 1
+
+ // Add Capability
+ if (can_muster_capability())
+ view.actions.capability = 1
+ }
+
+ view.actions.done = 1
+ },
+
+ card: action_held_event_lordship,
+
+ lord(other) {
+ clear_undo()
+ let die = roll_die()
+ let fealty = data.lords[other].fealty
+ if (die <= fealty) {
+ log(`L${other} ${range(fealty)}: ${HIT[die]}`)
+ push_state("muster_lord_at_seat")
+ game.who = other
+ } else {
+ log(`L${other} ${range(fealty)}: ${MISS[die]}`)
+ resume_levy_muster_lord()
+ }
+ },
+
+ vassal(vassal) {
+ push_undo()
+ muster_vassal(game.who, vassal)
+ resume_levy_muster_lord()
+ },
+
+ take_ship() {
+ push_undo()
+ add_lord_assets(game.who, SHIP, 1)
+ resume_levy_muster_lord()
+ },
+ take_cart() {
+ push_undo()
+ add_lord_assets(game.who, CART, 1)
+ resume_levy_muster_lord()
+ },
+
+ capability() {
+ push_undo()
+ push_state("muster_capability")
+ },
+
+ done() {
+ set_lord_moved(game.who, 1)
+ pop_state()
+ },
+}
+
+states.muster_lord_at_seat = {
+ inactive: "Muster",
+ prompt() {
+ view.prompt = `Muster: Select Seat for ${lord_name[game.who]}.`
+ for_each_seat(game.who, seat => {
+ if (is_friendly_locale(seat))
+ gen_action_locale(seat)
+ })
+ },
+ locale(loc) {
+ push_undo()
+
+ let cap = used_seat_capability(game.who, loc)
+ if (cap >= 0)
+ log(`L${game.who} to %${loc} (C${cap}).`)
+ else
+ log(`L${game.who} to %${loc}.`)
+
+ // FIXME: clean up these transitions
+ set_lord_moved(game.who, 1)
+ muster_lord(game.who, loc)
+ game.state = "muster_lord_transport"
+ game.count = data.lords[game.who].assets.transport | 0
+ resume_muster_lord_transport()
+ },
+}
+
+function resume_muster_lord_transport() {
+ if (game.count === 0)
+ pop_state()
+ if (game.state === "levy_muster_lord")
+ resume_levy_muster_lord()
+}
+
+states.muster_lord_transport = {
+ inactive: "Muster",
+ prompt() {
+ if (game.state === "veliky_knyaz")
+ view.prompt = `Veliky Knyaz: Select Transport for ${lord_name[game.who]}.`
+ else
+ view.prompt = `Muster: Select Transport for ${lord_name[game.who]}.`
+ view.prompt += ` ${game.count} left.`
+ if (data.lords[game.who].ships) {
+ if (can_add_transport(game.who, SHIP))
+ view.actions.take_ship = 1
+ }
+ if (can_add_transport(game.who, CART))
+ view.actions.take_cart = 1
+ },
+ take_ship() {
+ push_undo()
+ add_lord_assets(game.who, SHIP, 1)
+ --game.count
+ resume_muster_lord_transport()
+ },
+ take_cart() {
+ push_undo()
+ add_lord_assets(game.who, CART, 1)
+ --game.count
+ resume_muster_lord_transport()
+ },
+}
+
+function lord_has_capability_card(lord, c) {
+ if (get_lord_capability(lord, 0) === c)
+ return true
+ if (get_lord_capability(lord, 1) === c)
+ return true
+ return false
+}
+
+function lord_has_capability(lord, card_or_list) {
+ if (Array.isArray(card_or_list)) {
+ for (let card of card_or_list)
+ if (lord_has_capability_card(lord, card))
+ return true
+ return false
+ }
+ return lord_has_capability_card(lord, card_or_list)
+}
+
+function lord_already_has_capability(lord, c) {
+ // compare capabilities by name...
+ let name = data.cards[c].capability
+ let c1 = get_lord_capability(lord, 0)
+ if (c1 >= 0 && data.cards[c1].capability === name)
+ return true
+ let c2 = get_lord_capability(lord, 1)
+ if (c2 >= 0 && data.cards[c2].capability === name)
+ return true
+ return false
+}
+
+function can_add_lord_capability(lord) {
+ if (get_lord_capability(lord, 0) < 0)
+ return true
+ if (get_lord_capability(lord, 1) < 0)
+ return true
+ return false
+}
+
+function add_lord_capability(lord, c) {
+ if (get_lord_capability(lord, 0) < 0)
+ return set_lord_capability(lord, 0, c)
+ if (get_lord_capability(lord, 1) < 0)
+ return set_lord_capability(lord, 1, c)
+ throw new Error("no empty capability slots!")
+}
+
+function discard_lord_capability_n(lord, n) {
+ set_lord_capability(lord, n, NOTHING)
+}
+
+function discard_lord_capability(lord, c) {
+ if (get_lord_capability(lord, 0) === c)
+ return set_lord_capability(lord, 0, NOTHING)
+ if (get_lord_capability(lord, 1) === c)
+ return set_lord_capability(lord, 1, NOTHING)
+ throw new Error("capability not found")
+}
+
+function can_muster_capability() {
+ let deck = list_deck()
+ for (let c of deck) {
+ if (is_no_event_card(c))
+ continue
+ if (!data.cards[c].lords || set_has(data.cards[c].lords, game.who)) {
+ if (data.cards[c].this_lord) {
+ if (!lord_already_has_capability(game.who, c))
+ return true
+ } else {
+ if (can_deploy_global_capability(c))
+ return true
+ }
+ }
+ }
+ return false
+}
+
+states.muster_capability = {
+ inactive: "Muster",
+ prompt() {
+ let deck = list_deck()
+ view.prompt = `Muster: Select a new Capability for ${lord_name[game.who]}.`
+ view.arts_of_war = deck
+ for (let c of deck) {
+ if (is_no_event_card(c))
+ continue
+ if (!data.cards[c].lords || set_has(data.cards[c].lords, game.who)) {
+ if (data.cards[c].this_lord) {
+ if (!lord_already_has_capability(game.who, c))
+ gen_action_card(c)
+ } else {
+ if (can_deploy_global_capability(c))
+ gen_action_card(c)
+ }
+ }
+ }
+ },
+ card(c) {
+ if (data.cards[c].this_lord) {
+ if (can_add_lord_capability(game.who, c)) {
+ add_lord_capability(game.who, c)
+ } else {
+ game.what = c
+ game.state = "muster_capability_discard"
+ return
+ }
+ } else {
+ deploy_global_capability(c)
+ }
+ pop_state()
+ resume_levy_muster_lord()
+ },
+}
+
+states.muster_capability_discard = {
+ inactive: "Muster",
+ prompt() {
+ view.prompt = `Muster: Remove a Capability from ${lord_name[game.who]}.`
+ gen_action_card(get_lord_capability(game.who, 0))
+ gen_action_card(get_lord_capability(game.who, 1))
+ },
+ card(c) {
+ push_undo()
+ discard_lord_capability(game.who, c)
+ add_lord_capability(game.who, game.what)
+ game.what = NOTHING
+ pop_state()
+ resume_levy_muster_lord()
+ },
+}
+
+// === LEVY: CALL TO ARMS ===
+
+function goto_levy_call_to_arms() {
+ if (game.active === TEUTONS)
+ goto_teutonic_call_to_arms()
+ else
+ goto_russian_call_to_arms()
+}
+
+function end_levy_call_to_arms() {
+ clear_undo()
+ clear_lords_moved()
+ set_active_enemy()
+ if (game.active === P2)
+ goto_levy_call_to_arms()
+ else
+ goto_levy_discard_events()
+}
+
+function goto_levy_discard_events() {
+
+ // Discard "This Levy" events from play.
+ discard_events("this_levy")
+
+ set_active(P1)
+ goto_capability_discard()
+}
+
+// === LEVY: CALL TO ARMS - PAPAL LEGATE ===
+
+function goto_teutonic_call_to_arms() {
+ end_levy_call_to_arms()
+}
+
+// === LEVY: CALL TO ARMS - NOVGOROD VECHE ===
+
+function goto_russian_call_to_arms() {
+ end_levy_call_to_arms()
+}
+
+// === CAMPAIGN: CAPABILITY DISCARD ===
+
+function count_mustered_lords() {
+ let n = 0
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (is_lord_on_map(lord))
+ ++n
+ return n
+}
+
+function count_global_capabilities() {
+ let n = 0
+ for (let c of game.capabilities) {
+ if (game.active === P1 && c >= first_p1_card && c <= last_p1_card)
+ ++n
+ if (game.active === P2 && c >= first_p2_card && c <= last_p2_card)
+ ++n
+ }
+ return n
+}
+
+function goto_capability_discard() {
+
+ // Start of Campaign phase
+ if (check_campaign_victory())
+ return
+
+ if (count_global_capabilities() > count_mustered_lords())
+ game.state = "capability_discard"
+ else
+ end_capability_discard()
+}
+
+states.capability_discard = {
+ inactive: "Discard Capabilities",
+ prompt() {
+ if (count_global_capabilities() > count_mustered_lords()) {
+ view.prompt = "Discard Capabilities in excess of Mustered Lords."
+ for (let c of game.capabilities) {
+ if (game.active === P1 && c >= first_p1_card && c <= last_p1_card)
+ gen_action_card(c)
+ if (game.active === P2 && c >= first_p2_card && c <= last_p2_card)
+ gen_action_card(c)
+ }
+ } else {
+ view.prompt = "Discard Capabilities: All done."
+ view.actions.end_discard = 1
+ }
+ },
+ card(c) {
+ push_undo()
+ discard_global_capability(c)
+ },
+ end_discard() {
+ clear_undo()
+ end_capability_discard()
+ },
+}
+
+function end_capability_discard() {
+ set_active_enemy()
+ if (game.active === P2)
+ goto_capability_discard()
+ else
+ goto_campaign_plan()
+}
+
+// === CAMPAIGN: PLAN ===
+
+function goto_campaign_plan() {
+ game.turn++
+
+ log_h1("Campaign " + current_turn_name())
+
+ set_active(BOTH)
+ game.state = "campaign_plan"
+ game.plan1 = []
+ game.plan2 = []
+}
+
+function plan_has_lieutenant(first, last) {
+ for (let lord = first; lord <= last; ++lord)
+ if (is_upper_lord(lord))
+ return true
+ return false
+}
+
+function plan_selected_lieutenant(first, last) {
+ for (let lord = first; lord <= last; ++lord)
+ if (is_upper_lord(lord) && get_lower_lord(lord) === NOBODY)
+ return lord
+ return NOBODY
+}
+
+function plan_can_make_lieutenant(plan, upper, first, last) {
+ for (let lord = first; lord <= last; ++lord) {
+ if (!is_lord_on_map(lord))
+ continue
+ if (lord === upper)
+ continue
+ if (is_marshal(lord) || is_lord_besieged(lord))
+ continue
+ if (is_upper_lord(lord) || is_lower_lord(lord))
+ continue
+ if (get_lord_locale(upper) === get_lord_locale(lord))
+ return true
+ }
+ return false
+}
+
+states.campaign_plan = {
+ inactive: "Plan",
+ prompt(current) {
+ let plan = current === P1 ? game.plan1 : game.plan2
+ let first = current === P1 ? first_p1_lord : first_p2_lord
+ let last = current === P1 ? last_p1_lord : last_p2_lord
+ let upper = plan_selected_lieutenant(first, last)
+
+ view.plan = plan
+ view.who = upper
+ view.actions.plan = []
+
+ if (plan.length === 0 && upper === NOBODY)
+ view.prompt = "Plan: Designate Lieutenants and build a Plan."
+ else if (plan.length === 0 && upper !== NOBODY)
+ view.prompt = `Plan: Select Lower Lord for ${lord_name[upper]}.`
+ else if (plan.length === max_plan_length())
+ view.prompt = "Plan: All done."
+ else
+ view.prompt = "Plan: Build a Plan."
+
+ if (upper === NOBODY) {
+ if (plan.length < max_plan_length()) {
+ view.actions.end_plan = 0
+ if (count_cards_in_plan(plan, NOBODY) < 3)
+ gen_action_plan(NOBODY)
+ for (let lord = first; lord <= last; ++lord) {
+ if (is_lord_on_map(lord) && count_cards_in_plan(plan, lord) < 3)
+ gen_action_plan(lord)
+ }
+ } else {
+ view.actions.end_plan = 1
+ }
+ } else {
+ view.actions.end_plan = 0
+ }
+
+ // Designate Lieutenants only if no plan started.
+ if (plan.length === 0) {
+ if (upper !== NOBODY)
+ gen_action_lord(upper)
+
+ for (let lord = first; lord <= last; ++lord) {
+ if (is_marshal(lord) || is_lord_besieged(lord))
+ continue
+ if (is_upper_lord(lord) || is_lower_lord(lord))
+ continue
+ if (upper === NOBODY) {
+ if (plan_can_make_lieutenant(plan, lord, first, last))
+ gen_action_lord(lord)
+ } else {
+ if (get_lord_locale(upper) === get_lord_locale(lord))
+ gen_action_lord(lord)
+ }
+ }
+ }
+
+ if (plan.length > 0 || plan_has_lieutenant(first, last))
+ view.actions.undo = 1
+ else
+ view.actions.undo = 0
+ },
+ lord(lord, current) {
+ let upper
+ if (current === P1)
+ upper = plan_selected_lieutenant(first_p1_lord, last_p1_lord)
+ else
+ upper = plan_selected_lieutenant(first_p2_lord, last_p2_lord)
+ if (lord === upper)
+ remove_lieutenant(upper)
+ else if (upper === NOBODY)
+ add_lieutenant(lord)
+ else
+ set_lower_lord(upper, lord)
+ },
+ plan(lord, current) {
+ if (current === P1)
+ game.plan1.push(lord)
+ else
+ game.plan2.push(lord)
+ },
+ undo(_, current) {
+ if (current === P1) {
+ if (game.plan1.length > 0) {
+ game.plan1.pop()
+ } else {
+ for (let lord = first_p1_lord; lord <= last_p1_lord; ++lord)
+ if (is_upper_lord(lord))
+ remove_lieutenant(lord)
+ }
+ } else {
+ if (game.plan2.length > 0) {
+ game.plan2.pop()
+ } else {
+ for (let lord = first_p2_lord; lord <= last_p2_lord; ++lord)
+ if (is_upper_lord(lord))
+ remove_lieutenant(lord)
+ }
+ }
+ },
+ end_plan(_, current) {
+ if (game.active === BOTH) {
+ if (current === P1)
+ set_active(P2)
+ else
+ set_active(P1)
+ } else {
+ end_campaign_plan()
+ }
+ },
+}
+
+function end_campaign_plan() {
+ if (game.pieces.lieutenants.length > 0) {
+ log("Lieutenants")
+ for (let i = 0; i < game.pieces.lieutenants.length; i += 2) {
+ let upper = game.pieces.lieutenants[i]
+ let lower = game.pieces.lieutenants[i + 1]
+ logi(`L${upper} over L${lower}`)
+ }
+ }
+
+ set_active(P1)
+ goto_command_activation()
+}
+
+// === CAMPAIGN: COMMAND ACTIVATION ===
+
+function goto_command_activation() {
+ if (game.plan2.length === 0) {
+ game.command = NOBODY
+ goto_end_campaign()
+ return
+ }
+
+ if (check_campaign_victory())
+ return
+
+ if (game.plan2.length > game.plan1.length) {
+ set_active(P2)
+ game.command = game.plan2.shift()
+ } else {
+ set_active(P1)
+ game.command = game.plan1.shift()
+ }
+
+ if (game.command === NOBODY) {
+ log_h2("Pass")
+ goto_command_activation()
+ } else if (is_lower_lord(game.command)) {
+ log_h2(`L${game.command} - Pass`)
+ goto_command_activation()
+ } else if (!is_lord_on_map(game.command)) {
+ log_h2(`L${game.command} - Pass`)
+ goto_command_activation()
+ } else {
+ log_h2(`L${game.command} at %${get_lord_locale(game.command)}`)
+ goto_command()
+ }
+}
+
+// === CAMPAIGN: ACTIONS ===
+
+function set_active_command() {
+ if (game.command >= first_p1_lord && game.command <= last_p1_lord)
+ set_active(P1)
+ else
+ set_active(P2)
+}
+
+function is_active_command() {
+ if (game.command >= first_p1_lord && game.command <= last_p1_lord)
+ return game.active === P1
+ else
+ return game.active === P2
+}
+
+function is_first_action() {
+ return game.flags.first_action
+}
+
+function is_first_march() {
+ return game.flags.first_march
+}
+
+function goto_command() {
+ game.actions = data.lords[game.command].command
+
+ game.flags.first_action = 1
+ game.flags.first_march = 1
+
+ // 4.1.3 Lieutenants MUST take lower lord
+ game.group = [ game.command ]
+ let lower = get_lower_lord(game.command)
+ if (lower !== NOBODY)
+ set_add(game.group, lower)
+
+ resume_command()
+ update_supply_possible()
+}
+
+function resume_command() {
+ game.state = "command"
+}
+
+function spend_action(cost) {
+ game.flags.first_action = 0
+ game.actions -= cost
+}
+
+function spend_march_action(cost) {
+ game.flags.first_action = 0
+ game.flags.first_march = 0
+ game.actions -= cost
+}
+
+function spend_all_actions() {
+ game.flags.first_action = 0
+ game.flags.first_march = 0
+ game.actions = 0
+}
+
+function end_command() {
+ log_br()
+
+ game.group = 0
+
+ game.flags.first_action = 0
+ game.flags.first_march = 0
+ game.flags.famine = 0
+
+ // NOTE: Feed currently acting side first for expedience.
+ set_active_command()
+ goto_feed()
+}
+
+function this_lord_has_russian_druzhina() {
+ if (game.active === RUSSIANS)
+ if (lord_has_capability(game.command, AOW_RUSSIAN_DRUZHINA))
+ return get_lord_forces(game.command, KNIGHTS) > 0
+ return false
+}
+
+function this_lord_has_house_of_suzdal() {
+ if (game.active === RUSSIANS)
+ if (lord_has_capability(game.command, AOW_RUSSIAN_HOUSE_OF_SUZDAL))
+ return is_lord_on_map(LORD_ALEKSANDR) && is_lord_on_map(LORD_ANDREY)
+ return false
+}
+
+states.command = {
+ inactive: "Command",
+ prompt() {
+ if (game.actions === 0)
+ view.prompt = `Command: ${lord_name[game.command]} has no more actions.`
+ else if (game.actions === 1)
+ view.prompt = `Command: ${lord_name[game.command]} has ${game.actions} action.`
+ else
+ view.prompt = `Command: ${lord_name[game.command]} has ${game.actions} actions.`
+
+ view.group = game.group
+
+ let here = get_lord_locale(game.command)
+
+ prompt_held_event()
+
+ // 4.3.2 Marshals MAY take other lords
+ if (is_marshal(game.command)) {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (lord !== game.command && !is_lower_lord(lord))
+ if (get_lord_locale(lord) === here)
+ gen_action_lord(lord)
+ }
+
+ if (game.actions > 0)
+ view.actions.pass = 1
+ else
+ view.actions.end_command = 1
+
+ prompt_march()
+
+ if (can_action_supply())
+ view.actions.supply = 1
+
+ if (can_action_siege())
+ view.actions.siege = 1
+ if (can_action_forage())
+ view.actions.forage = 1
+ if (can_action_ravage())
+ view.actions.ravage = 1
+ if (can_action_tax())
+ view.actions.tax = 1
+ if (can_action_sail())
+ view.actions.sail = 1
+ },
+
+ pass() {
+ push_undo()
+ log("Passed.")
+ spend_all_actions()
+ },
+
+ end_command() {
+ push_undo()
+ end_command()
+ },
+
+ forage: goto_forage,
+ ravage: goto_ravage,
+ supply: goto_supply,
+ tax: goto_tax,
+ sail: goto_sail,
+
+ locale: goto_march,
+
+ lord(lord) {
+ set_toggle(game.group, lord)
+ if (is_upper_lord(lord))
+ set_toggle(game.group, get_lower_lord(lord))
+ },
+
+ card: action_held_event,
+}
+
+// === ACTION: MARCH ===
+
+function format_group_move() {
+ if (game.group.length > 1) {
+ let list = []
+ for (let lord of game.group)
+ if (lord !== game.command)
+ list.push("L" + lord)
+ return " with " + list.join(" and ")
+ }
+ return ""
+}
+
+function group_has_teutonic_converts() {
+ if (game.active === TEUTONS) {
+ if (is_first_march())
+ if (group_has_capability(AOW_TEUTONIC_CONVERTS))
+ if (count_group_forces(LIGHT_HORSE) > 0)
+ return true
+ }
+ return false
+}
+
+function prompt_march() {
+ if (game.actions > 0 || group_has_teutonic_converts()) {
+ let here = get_lord_locale(game.command)
+ for (let to of data.locales[here].adjacent)
+ gen_action_locale(to)
+ }
+}
+
+function goto_march(to) {
+ push_undo()
+ let from = get_lord_locale(game.command)
+ let ways = list_ways(from, to)
+ if (ways.length > 2) {
+ game.march = { from, to, approach: -1, avoid: -1 }
+ game.state = "march_way"
+ } else {
+ game.march = { from, to, approach: ways[1], avoid: -1 }
+ march_with_group_1()
+ }
+}
+
+states.march_way = {
+ inactive: "March",
+ prompt() {
+ view.prompt = `March: Select Way.`
+ view.group = game.group
+ let from = game.march.from
+ let to = game.march.to
+ let ways = list_ways(from, to)
+ for (let i = 1; i < ways.length; ++i)
+ gen_action_way(ways[i])
+ },
+ way(way) {
+ game.march.approach = way
+ march_with_group_1()
+ },
+}
+
+function march_with_group_1() {
+ let way = game.march.approach
+ let type = data.ways[way].type
+
+ let transport = count_group_transport(type)
+ let prov = count_group_assets(PROV)
+
+ if (group_has_teutonic_converts() && prov <= transport * 2)
+ return march_with_group_2()
+
+ if (prov > transport)
+ game.state = "march_laden"
+ else
+ march_with_group_2()
+}
+
+states.march_laden = {
+ inactive: "March",
+ prompt() {
+ let to = game.march.to
+ let way = game.march.approach
+ let transport = count_group_transport(data.ways[way].type)
+ let prov = count_group_assets(PROV)
+
+ view.group = game.group
+
+ if (prov <= transport * 2 && group_has_teutonic_converts())
+ view.prompt = `March: Converts.`
+ else if (prov > transport * 2 || (prov > transport && view.actions < 2))
+ view.prompt = `March: Hindered with ${prov} Provender, and ${transport} Transport.`
+ else if (prov > transport)
+ view.prompt = `March: Laden with ${prov} Provender, and ${transport} Transport.`
+ else
+ view.prompt = `March: Unladen.`
+
+ if (group_has_teutonic_converts()) {
+ if (prov <= transport * 2) {
+ view.actions.march = 1
+ gen_action_locale(to)
+ } else {
+ for (let lord of game.group) {
+ if (get_lord_assets(lord, PROV) > 0) {
+ view.prompt += " Discard Provender."
+ gen_action_prov(lord)
+ }
+ }
+ }
+ return
+ }
+
+ if (prov <= transport * 2) {
+ if (prov > transport) {
+ if (game.actions >= 2) {
+ view.actions.march = 1 // other button?
+ gen_action_laden_march(to)
+ } else {
+ view.prompt += " 1 action left."
+ }
+ } else {
+ view.actions.march = 1
+ gen_action_locale(to)
+ }
+ }
+
+ if (prov > transport) {
+ for (let lord of game.group) {
+ if (prov > transport) {
+ if (get_lord_assets(lord, PROV) > 0) {
+ view.prompt += " Discard Provender."
+ gen_action_prov(lord)
+ }
+ }
+ }
+ }
+ },
+ prov: drop_prov,
+ march: march_with_group_2,
+ locale: march_with_group_2,
+ laden_march: march_with_group_2,
+}
+
+function march_with_group_2() {
+ let from = get_lord_locale(game.command)
+ let way = game.march.approach
+ let to = game.march.to
+ let transport = count_group_transport(data.ways[way].type)
+ let prov = count_group_assets(PROV)
+ let laden = prov > transport
+
+ if (group_has_teutonic_converts()) {
+ logcap(AOW_TEUTONIC_CONVERTS)
+ spend_march_action(0)
+ }
+ else if (laden)
+ spend_march_action(2)
+ else
+ spend_march_action(1)
+
+ if (data.ways[way].name)
+ log(`Marched to %${to} via W${way}${format_group_move()}.`)
+ else
+ log(`Marched to %${to}${format_group_move()}.`)
+
+ for (let lord of game.group) {
+ set_lord_locale(lord, to)
+ set_lord_moved(lord, 1)
+ }
+
+ if (has_unbesieged_enemy_lord(to)) {
+ goto_confirm_approach()
+ } else {
+ march_with_group_3()
+ }
+}
+
+function march_with_group_3() {
+ let here = get_lord_locale(game.command)
+
+ // Disbanded in battle!
+ if (here === NOWHERE) {
+ game.march = 0
+ spend_all_actions()
+ resume_command()
+ update_supply_possible()
+ return
+ }
+
+ if (is_unbesieged_enemy_stronghold(here)) {
+ add_siege_marker(here)
+ spend_all_actions() // ENCAMP
+ }
+
+ if (is_trade_route(here))
+ conquer_trade_route(here)
+
+ game.march = 0
+
+ resume_command()
+ update_supply_possible()
+}
+
+function goto_confirm_approach() {
+ if (game.skip_confirm_approach) {
+ goto_avoid_battle()
+ return
+ }
+ game.state = "confirm_approach"
+}
+
+states.confirm_approach = {
+ inactive: "March",
+ prompt() {
+ view.prompt = `March: Confirm Approach to enemy Lord.`
+ view.group = game.group
+ view.actions.approach = 1
+ },
+ approach() {
+ goto_avoid_battle()
+ }
+}
+
+// === ACTION: MARCH - AVOID BATTLE ===
+
+function count_besieged_lords(loc) {
+ let n = 0
+ for (let lord = first_lord; lord <= last_lord; ++lord)
+ if (get_lord_locale(lord) === loc && is_lord_besieged(lord))
+ ++n
+ return n
+}
+
+function stronghold_strength(loc) {
+ if (has_castle_marker(loc))
+ return 2
+ return data.locales[loc].stronghold
+}
+
+function stronghold_capacity(loc) {
+ return stronghold_strength(loc) - count_besieged_lords(loc)
+}
+
+function spoil_prov(lord) {
+ add_lord_assets(lord, PROV, -1)
+ add_spoils(PROV, 1)
+}
+
+function can_any_avoid_battle() {
+ let here = game.march.to
+ for (let [to, way] of data.locales[here].ways)
+ if (can_avoid_battle(to, way))
+ return true
+ return false
+}
+
+function can_avoid_battle(to, way) {
+ if (way === game.march.approach)
+ return false
+ if (has_unbesieged_enemy_lord(to))
+ return false
+ if (is_unbesieged_enemy_stronghold(to))
+ return false
+ return true
+}
+
+function goto_avoid_battle() {
+ clear_undo()
+ set_active_enemy()
+ if (can_any_avoid_battle()) {
+ // TODO: pre-select lone lord?
+ game.march.group = game.group // save group
+ game.state = "avoid_battle"
+ game.spoils = 0
+ resume_avoid_battle()
+ } else {
+ goto_march_withdraw()
+ }
+}
+
+function resume_avoid_battle() {
+ let here = game.march.to
+ if (has_unbesieged_friendly_lord(here)) {
+ game.group = []
+ game.state = "avoid_battle"
+ } else {
+ end_avoid_battle()
+ }
+}
+
+states.avoid_battle = {
+ inactive: "Avoid Battle",
+ prompt() {
+ view.prompt = "March: Select Lords and destination to Avoid Battle."
+ view.group = game.group
+
+ let here = game.march.to
+
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === here && !is_lower_lord(lord))
+ gen_action_lord(lord)
+
+ if (game.group.length > 0) {
+ for (let [to, way] of data.locales[here].ways) {
+ if (can_avoid_battle(to, way))
+ gen_action_locale(to)
+ }
+ }
+
+ view.actions.end_avoid_battle = 1
+ },
+ lord(lord) {
+ set_toggle(game.group, lord)
+ if (is_upper_lord(lord))
+ set_toggle(game.group, get_lower_lord(lord))
+ },
+ locale(to) {
+ push_undo()
+
+ // Save Assets and Lords in case Ambush cancels Avoid Battle.
+ if (!game.march.ambush) {
+ if (could_enemy_play_ambush()) {
+ // TODO: ambush object...
+ game.march.ambush = {
+ lords: [],
+ assets: game.pieces.assets.slice(),
+ conquered: game.pieces.conquered.slice(),
+ }
+ }
+ }
+
+ let from = get_lord_locale(game.command)
+ let ways = list_ways(from, to)
+ if (ways.length > 2) {
+ game.march.avoid_to = to
+ game.state = "avoid_battle_way"
+ } else {
+ game.march.avoid_to = to
+ game.march.avoid_way = ways[1]
+ avoid_battle_1()
+ }
+ },
+ end_avoid_battle() {
+ push_undo()
+ end_avoid_battle()
+ },
+}
+
+states.avoid_battle_way = {
+ inactive: "Avoid Battle",
+ prompt() {
+ view.prompt = `Avoid Battle: Select Way to destination.`
+ view.group = game.group
+ let from = game.march.to
+ let to = game.march.avoid_to
+ let ways = list_ways(from, to)
+ for (let i = 1; i < ways.length; ++i)
+ if (can_avoid_battle(to, ways[i]))
+ gen_action_way(ways[i])
+ },
+ way(way) {
+ game.march.avoid_way = way
+ avoid_battle_1()
+ },
+}
+
+function avoid_battle_1() {
+ let way = game.march.avoid_way
+ let transport = count_group_transport(data.ways[way].type)
+ let prov = count_group_assets(PROV)
+ if (prov > transport)
+ game.state = "avoid_battle_laden"
+ else
+ avoid_battle_2()
+}
+
+states.avoid_battle_laden = {
+ inactive: "Avoid Battle",
+ prompt() {
+ let to = game.march.avoid_to
+ let way = game.march.avoid_way
+ let transport = count_group_transport(data.ways[way].type)
+ let prov = count_group_assets(PROV)
+
+ if (prov > transport)
+ view.prompt = `Avoid Battle: Hindered with ${prov} Provender and ${transport} Transport.`
+ else
+ view.prompt = `Avoid Battle: Unladen.`
+ view.group = game.group
+
+ if (prov > transport) {
+ view.prompt += " Discard Provender."
+ for (let lord of game.group) {
+ if (get_lord_assets(lord, PROV) > 0)
+ gen_action_prov(lord)
+ }
+ } else {
+ gen_action_locale(to)
+ view.actions.avoid = 1
+ }
+ },
+ prov(lord) {
+ push_undo()
+ spoil_prov(lord)
+ },
+ locale(_) {
+ avoid_battle_2()
+ },
+ avoid() {
+ avoid_battle_2()
+ },
+}
+
+function avoid_battle_2() {
+ let to = game.march.avoid_to
+
+ for (let lord of game.group) {
+ log(`L${lord} Avoided Battle to %${to}.`)
+ if (game.march.ambush)
+ set_add(game.march.ambush.lords, lord)
+ set_lord_locale(lord, to)
+ set_lord_moved(lord, 1)
+ }
+
+ if (is_trade_route(to))
+ conquer_trade_route(to)
+
+ game.march.avoid_to = 0
+ game.march.avoid_way = 0
+ resume_avoid_battle()
+}
+
+function end_avoid_battle() {
+ game.group = game.march.group // restore group
+ game.march.group = 0
+ goto_march_withdraw()
+}
+
+// === ACTION: MARCH - WITHDRAW ===
+
+function can_withdraw(here, n) {
+ if (is_unbesieged_friendly_stronghold(here))
+ if (stronghold_capacity(here) >= n)
+ return true
+ return false
+}
+
+function goto_march_withdraw() {
+ let here = game.march.to
+ if (has_unbesieged_friendly_lord(here) && can_withdraw(here, 1)) {
+ game.state = "march_withdraw"
+ } else {
+ end_march_withdraw()
+ }
+}
+
+states.march_withdraw = {
+ inactive: "Withdraw",
+ prompt() {
+ view.prompt = "March: Select Lords to Withdraw into Stronghold."
+
+ let here = get_lord_locale(game.command)
+ let capacity = stronghold_capacity(here)
+
+ if (capacity >= 1) {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (get_lord_locale(lord) === here && !is_lower_lord(lord) && is_lord_unbesieged(lord)) {
+ if (is_upper_lord(lord)) {
+ if (capacity >= 2)
+ gen_action_lord(lord)
+ } else {
+ gen_action_lord(lord)
+ }
+ }
+ }
+ }
+
+ view.actions.end_withdraw = 1
+ },
+ lord(lord) {
+ push_undo()
+ let lower = get_lower_lord(lord)
+
+ log(`L${lord} Withdrew.`)
+ set_lord_besieged(lord, 1)
+
+ if (lower !== NOBODY) {
+ log(`L${lower} Withdrew.`)
+ set_lord_besieged(lower, 1)
+ }
+ },
+ end_withdraw() {
+ end_march_withdraw()
+ },
+}
+
+function end_march_withdraw() {
+ clear_undo()
+ set_active_enemy()
+ goto_march_ambush()
+}
+
+// === ACTION: MARCH - AMBUSH ===
+
+function could_enemy_play_ambush() {
+ if (game.active === TEUTONS)
+ return could_play_card(EVENT_RUSSIAN_AMBUSH)
+ else
+ return could_play_card(EVENT_TEUTONIC_AMBUSH)
+}
+
+function goto_march_ambush() {
+ if (game.march.ambush && game.march.ambush.lords.length > 0)
+ game.state = "march_ambush"
+ else
+ goto_spoils_after_avoid_battle()
+}
+
+states.march_ambush = {
+ inactive: "Ambush",
+ prompt() {
+ view.prompt = "Avoid Battle: You may play Ambush if you have it."
+ if (has_card_in_hand(EVENT_TEUTONIC_AMBUSH))
+ gen_action_card(EVENT_TEUTONIC_AMBUSH)
+ if (has_card_in_hand(EVENT_RUSSIAN_AMBUSH))
+ gen_action_card(EVENT_RUSSIAN_AMBUSH)
+ view.actions.pass = 1
+ },
+ card(c) {
+ play_held_event(c)
+
+ // Restore assets and spoils and withdrawn lords
+ game.pieces.assets = game.march.ambush.assets
+ game.pieces.conquered = game.march.ambush.conquered
+ game.spoils = 0
+
+ // Restore lords who avoided battle
+ for (let lord of game.march.ambush.lords) {
+ set_lord_locale(lord, game.march.to)
+ set_lord_moved(lord, 0)
+ }
+
+ set_active_enemy()
+ game.march.ambush = 0
+ goto_march_withdraw()
+ },
+ pass() {
+ game.march.ambush = 0
+ goto_spoils_after_avoid_battle()
+ },
+}
+
+// === ACTION: MARCH - DIVIDE SPOILS AFTER AVOID BATTLE ===
+
+function list_spoils() {
+ let list = []
+ for (let type = 0; type < 7; ++type) {
+ let n = get_spoils(type)
+ if (n > 0)
+ list.push(`${n} ${ASSET_TYPE_NAME[type]}`)
+ }
+ if (list.length > 0)
+ return list.join(", ")
+ return "nothing"
+}
+
+function prompt_spoils() {
+ if (get_spoils(PROV) > 0)
+ view.actions.take_prov = 1
+ if (get_spoils(COIN) > 0)
+ view.actions.take_coin = 1
+ if (get_spoils(CART) > 0)
+ view.actions.take_cart = 1
+}
+
+function take_spoils(type) {
+ push_undo_without_who()
+ add_lord_assets(game.who, type, 1)
+ add_spoils(type, -1)
+ if (!has_any_spoils())
+ game.who = NOBODY
+}
+
+function take_spoils_prov() { take_spoils(PROV) }
+function take_spoils_coin() { take_spoils(COIN) }
+function take_spoils_cart() { take_spoils(CART) }
+
+function goto_spoils_after_avoid_battle() {
+ if (has_any_spoils()) {
+ game.state = "spoils_after_avoid_battle"
+ if (game.group.length === 1)
+ game.who = game.group[0]
+ } else {
+ goto_battle()
+ }
+}
+
+states.spoils_after_avoid_battle = {
+ inactive: "Spoils",
+ prompt() {
+ if (has_any_spoils()) {
+ view.prompt = "Spoils: Divide " + list_spoils() + "."
+ // only moving lords get to divide the spoils
+ for (let lord of game.group)
+ prompt_select_lord(lord)
+ if (game.who !== NOBODY)
+ prompt_spoils()
+ } else {
+ view.prompt = "Spoils: All done."
+ view.actions.end_spoils = 1
+ }
+ },
+ lord: action_select_lord,
+ take_prov: take_spoils_prov,
+ end_spoils() {
+ clear_undo()
+ game.spoils = 0
+ game.who = NOBODY
+ goto_battle()
+ },
+}
+
+// === ACTION: SUPPLY (SEARCHING) ===
+
+let _supply_stop = new Array(last_locale+1)
+let _supply_reached = new Array(last_locale+1)
+
+let _supply_seen = new Array(last_locale+1).fill(0)
+let _supply_cost = new Array(last_locale+1)
+let _supply_carts = new Array(last_locale+1)
+
+function is_supply_forbidden(here) {
+ if (has_unbesieged_enemy_lord(here))
+ return true
+ if (is_unbesieged_enemy_stronghold(here))
+ return true
+ if (is_friendly_territory(here) && has_conquered_marker(here))
+ return true
+ return false
+}
+
+function init_supply_forbidden() {
+ for (let here = 0; here <= last_locale; ++here) {
+ if (is_supply_forbidden(here))
+ _supply_stop[here] = 1
+ else
+ _supply_stop[here] = 0
+ }
+}
+
+function init_supply() {
+ let season = current_season()
+ let here = get_lord_locale(game.command)
+ let carts = 0
+ let ships = 0
+ let available = 2
+
+ if (season === SUMMER) {
+ carts = get_shared_assets(here, CART)
+ }
+ if (season === SUMMER || season === RASPUTITSA) {
+ ships = count_shared_ships()
+ }
+
+ if (ships > 2)
+ ships = 2
+
+ if (is_famine_in_play())
+ available = game.flags.famine ? 0 : 1
+
+ let seats = []
+ if (available > 0) {
+ for_each_seat(game.command, seat => {
+ if (!is_supply_forbidden(seat))
+ seats.push(seat)
+ }, true)
+ available = Math.min(seats.length, available)
+ }
+
+ let seaports = []
+ if (ships > 0) {
+ if (game.active === TEUTONS)
+ for (let port of data.seaports)
+ if (!is_supply_forbidden(port))
+ seaports.push(port)
+ if (game.active === RUSSIANS)
+ if (!is_supply_forbidden(LOC_NOVGOROD))
+ seaports.push(LOC_NOVGOROD)
+ }
+ if (seaports.length === 0)
+ ships = 0
+
+ game.supply = { seats, seaports, available, carts, ships }
+}
+
+function search_supply(start, carts, exit) {
+ if (_supply_stop[start])
+ return 0
+ _supply_reached[start] = 1
+ _supply_cost[start] = 0
+ if (exit && set_has(exit, start))
+ return 1
+ if (carts === 0)
+ return 0
+ let queue = [ start ]
+ while (queue.length > 0) {
+ let item = queue.shift()
+ let here = item & 63
+ let used = item >> 6
+ if (used + 1 <= carts) {
+ for (let next of data.locales[here].adjacent) {
+ if (!_supply_reached[next] && !_supply_stop[next]) {
+ if (exit && set_has(exit, next))
+ return 1
+ _supply_reached[next] = 1
+ _supply_cost[next] = used + 1
+ if (used + 1 < carts)
+ queue.push(next | ((used + 1) << 6))
+ }
+ }
+ }
+ }
+ return 0
+}
+
+// === ACTION: SUPPLY ===
+
+function update_supply_possible() {
+ if (game.actions < 1) {
+ game.supply = 0
+ return
+ }
+
+ update_supply_possible_pass()
+}
+
+function update_supply_possible_pass() {
+ init_supply()
+ init_supply_forbidden()
+ _supply_reached.fill(0)
+ let sources = []
+ for (let loc of game.supply.seats)
+ set_add(sources, loc)
+ for (let loc of game.supply.seaports)
+ set_add(sources, loc)
+ game.supply = search_supply(get_lord_locale(game.command), game.supply.carts, sources)
+}
+
+function search_supply_cost() {
+ init_supply_forbidden()
+ _supply_reached.fill(0)
+ search_supply(get_lord_locale(game.command), game.supply.carts, null)
+}
+
+function can_action_supply() {
+ if (game.actions < 1)
+ return false
+ return !!game.supply
+}
+
+function can_supply() {
+ if (game.supply.available > 0 && game.supply.seats.length > 0)
+ return true
+ if (game.supply.ships > 0 && game.supply.seaports.length > 0)
+ return true
+ return false
+}
+
+function goto_supply() {
+ push_undo()
+
+ if (is_famine_in_play() && !game.flags.famine) {
+ if (game.active === TEUTONS)
+ logevent(EVENT_RUSSIAN_FAMINE)
+ else
+ logevent(EVENT_TEUTONIC_FAMINE)
+ }
+
+ log(`Supplied`)
+ init_supply()
+ resume_supply()
+ game.state = "supply_source"
+}
+
+function resume_supply() {
+ if (game.supply.available + game.supply.ships === 0) {
+ game.supply.seats = []
+ game.supply.seaports = []
+ } else {
+ search_supply_cost()
+ game.supply.seats = game.supply.seats.filter(loc => _supply_reached[loc])
+ game.supply.seaports = game.supply.seaports.filter(loc => _supply_reached[loc])
+ }
+
+ if (can_supply())
+ game.state = "supply_source"
+ else
+ end_supply()
+}
+
+states.supply_source = {
+ inactive: "Supply",
+ prompt() {
+ if (!can_supply()) {
+ view.prompt = "Supply: No valid Supply Sources."
+ return
+ }
+
+ view.prompt = "Supply: Select Supply Source and Route."
+
+ let list = []
+ if (game.supply.carts > 0)
+ list.push(`${game.supply.carts} Cart`)
+ if (game.supply.ships > 0)
+ list.push(`${game.supply.ships} Ship`)
+
+ if (list.length > 0)
+ view.prompt += " " + list.join(", ") + "."
+
+ if (game.supply.available > 0)
+ for (let source of game.supply.seats)
+ gen_action_locale(source)
+ if (game.supply.ships > 0)
+ for (let source of game.supply.seaports)
+ gen_action_locale(source)
+ view.actions.end_supply = 1
+ },
+ locale(source) {
+ if (game.supply.available > 0 && game.supply.seats.includes(source)) {
+ array_remove_item(game.supply.seats, source)
+
+ let cap = used_seat_capability(game.command, source, game.supply.seats)
+ if (cap >= 0)
+ logi(`Seat at %${source} (C${cap})`)
+ else
+ logi(`Seat at %${source}`)
+
+ game.supply.available--
+ if (is_famine_in_play())
+ game.flags.famine = 1
+ } else {
+ logi(`Seaport at %${source}`)
+ game.supply.ships--
+ }
+
+ add_lord_assets(game.command, PROV, 1)
+
+ spend_supply_transport(source)
+ },
+ end_supply: end_supply,
+}
+
+function end_supply() {
+ spend_action(1)
+ resume_command()
+ game.supply = 1 // supply is possible!
+}
+
+function spend_supply_transport(source) {
+ if (source === get_lord_locale(game.command)) {
+ resume_supply()
+ return
+ }
+
+ search_supply_cost()
+ game.supply.carts -= _supply_cost[source]
+ resume_supply()
+}
+
+states.supply_path = {
+ inactive: "Supply",
+ prompt() {
+ view.prompt = "Supply: Trace Route to Supply Source."
+ view.supply = [ game.supply.here, game.supply.end ]
+ if (game.supply.carts > 0)
+ view.prompt += ` ${game.supply.carts} cart`
+ for (let i = 0; i < game.supply.path.length; i += 2) {
+ let wayloc = game.supply.path[i]
+ gen_action_locale(wayloc >> 8)
+ }
+ },
+ locale(next) {
+ let useloc = -1
+ let useway = -1
+ let twoway = false
+ for (let i = 0; i < game.supply.path.length; i += 2) {
+ let wayloc = game.supply.path[i]
+ let way = wayloc & 255
+ let loc = wayloc >> 8
+ if (loc === next) {
+ if (useloc < 0) {
+ useloc = loc
+ useway = way
+ } else {
+ twoway = true
+ }
+ }
+ }
+ if (twoway) {
+ game.state = "supply_path_way"
+ game.supply.next = next
+ } else {
+ walk_supply_path_way(next, useway)
+ }
+ },
+}
+
+function walk_supply_path_way(next, way) {
+ let type = data.ways[way].type
+ game.supply.carts--
+ game.supply.here = next
+ game.supply.path = map_get(game.supply.path, (next << 8) | way)
+ if (game.supply.path === 0)
+ resume_supply()
+ else
+ // Auto-pick path if only one choice.
+ if (AUTOWALK && game.supply.path.length === 2)
+ walk_supply_path_way(game.supply.path[0] >> 8, game.supply.path[0] & 255)
+}
+
+states.supply_path_way = {
+ inactive: "Supply",
+ prompt() {
+ view.prompt = "Supply: Trace path to supply source."
+ view.supply = [ game.supply.here, game.supply.end ]
+ if (game.supply.carts > 0)
+ view.prompt += ` ${game.supply.carts} cart`
+ for (let i = 0; i < game.supply.path.length; i += 2) {
+ let wayloc = game.supply.path[i]
+ let way = wayloc & 255
+ let loc = wayloc >> 8
+ if (loc === game.supply.next)
+ gen_action_way(way)
+ }
+ },
+ way(way) {
+ game.state = "supply_path"
+ walk_supply_path_way(game.supply.next, way)
+ },
+}
+
+// === ACTION: FORAGE ===
+
+function can_action_forage() {
+ if (game.actions < 1)
+ return false
+
+ if (is_famine_in_play())
+ return false
+
+ let here = get_lord_locale(game.command)
+ if (has_ravaged_marker(here))
+ return false
+ if (is_summer())
+ return true
+ if (is_friendly_stronghold_locale(here)) // FIXME: simpler check?
+ return true
+ return false
+}
+
+function goto_forage() {
+ push_undo()
+ let here = get_lord_locale(game.command)
+ log(`Foraged at %${here}`)
+ add_lord_assets(game.command, PROV, 1)
+ spend_action(1)
+ resume_command()
+}
+
+// === ACTION: RAVAGE ===
+
+function has_adjacent_unbesieged_enemy_lord(loc) {
+ for (let next of data.locales[loc].adjacent)
+ if (has_unbesieged_enemy_lord(next))
+ return true
+ return false
+}
+
+function can_ravage_locale(loc) {
+ if (!is_enemy_territory(loc))
+ return false
+ if (has_conquered_marker(loc))
+ return false
+ if (has_ravaged_marker(loc))
+ return false
+ if (is_friendly_locale(loc)) // faster check?
+ return false
+ if (has_adjacent_unbesieged_enemy_lord(loc))
+ return game.actions >= 2
+ else
+ return game.actions >= 1
+}
+
+function can_action_ravage() {
+ if (game.actions < 1)
+ return false
+
+ let here = get_lord_locale(game.command)
+
+ if (can_ravage_locale(here))
+ return true
+
+ if (this_lord_has_teutonic_raiders()) {
+ for (let there of data.locales[here].adjacent_by_trackway)
+ // XXX has_enemy_lord redundant with is_friendly_locale in can_ravage_locale
+ if (can_ravage_locale(there) && !has_enemy_lord(there))
+ return true
+ }
+
+ if (this_lord_has_russian_raiders()) {
+ for (let there of data.locales[here].adjacent)
+ // XXX has_enemy_lord redundant with is_friendly_locale in can_ravage_locale
+ if (can_ravage_locale(there) && !has_enemy_lord(there))
+ return true
+ }
+
+ return false
+}
+
+function goto_ravage() {
+ push_undo()
+ if (this_lord_has_teutonic_raiders() || this_lord_has_russian_raiders()) {
+ game.state = "ravage"
+ } else {
+ let here = get_lord_locale(game.command)
+ ravage_location(here, here)
+ }
+}
+
+states.ravage = {
+ inactive: "Ravage",
+ prompt() {
+ view.prompt = `Ravage: Select enemy territory to Ravage.`
+
+ let here = get_lord_locale(game.command)
+
+ if (can_ravage_locale(here))
+ gen_action_locale(here)
+
+ if (this_lord_has_teutonic_raiders()) {
+ for (let there of data.locales[here].adjacent_by_trackway)
+ if (can_ravage_locale(there) && !has_enemy_lord(there))
+ gen_action_locale(there)
+ }
+
+ if (this_lord_has_russian_raiders()) {
+ for (let there of data.locales[here].adjacent)
+ if (can_ravage_locale(there) && !has_enemy_lord(there))
+ gen_action_locale(there)
+ }
+ },
+ locale(there) {
+ let here = get_lord_locale(game.command)
+ ravage_location(here, there)
+ },
+}
+
+function ravage_location(here, there) {
+ if (here !== there) {
+ if (is_teutonic_lord(game.command))
+ log(`Ravaged %${there} (C${AOW_TEUTONIC_RAIDERS}).`)
+ else
+ log(`Ravaged %${there} (C${which_lord_capability(game.command, AOW_RUSSIAN_RAIDERS)}).`)
+ } else {
+ log(`Ravaged %${there}.`)
+ }
+
+ add_ravaged_marker(there)
+ add_lord_assets(game.command, PROV, 1)
+
+ if (here !== there && game.active === TEUTONS)
+ game.flags.teutonic_raiders = 1
+
+ if (has_adjacent_unbesieged_enemy_lord(there))
+ spend_action(2)
+ else
+ spend_action(1)
+ resume_command()
+}
+
+// === ACTION: TAX ===
+
+function restore_mustered_forces(lord) {
+ muster_lord_forces(lord)
+ for (let v of data.lords[lord].vassals)
+ if (is_vassal_mustered(v))
+ muster_vassal_forces(lord, v)
+}
+
+function can_action_tax() {
+ // Must use whole action
+ if (!is_first_action())
+ return false
+
+ // Must have space left to hold Coin
+ if (get_lord_assets(game.command, COIN) >= 8)
+ return false
+
+ // Must be at own seat
+ return is_lord_at_seat(game.command)
+}
+
+function goto_tax() {
+ push_undo()
+
+ let here = get_lord_locale(game.command)
+ log(`Taxed %${here}.`)
+
+ add_lord_assets(game.command, COIN, 1)
+
+ spend_all_actions()
+ resume_command()
+
+ if (lord_has_capability(game.command, AOW_RUSSIAN_VELIKY_KNYAZ)) {
+ logcap(AOW_RUSSIAN_VELIKY_KNYAZ)
+ restore_mustered_forces(game.command)
+ push_state("veliky_knyaz")
+ game.who = game.command
+ game.count = 2
+ }
+}
+
+states.veliky_knyaz = states.muster_lord_transport
+
+// === ACTION: SAIL ===
+
+function drop_prov(lord) {
+ add_lord_assets(lord, PROV, -1)
+}
+
+function has_enough_available_ships_for_horses() {
+ let ships = count_group_ships()
+ let horses = count_group_horses()
+
+ let needed_ships = horses
+ if (game.active === RUSSIANS)
+ needed_ships = horses * 2
+
+ return needed_ships <= ships
+}
+
+function can_action_sail() {
+ // Must use whole action
+ if (!is_first_action())
+ return false
+
+ // at a seaport
+ let here = get_lord_locale(game.command)
+ if (!is_seaport(here))
+ return false
+
+ // during Rasputitsa or Summer
+ if (is_winter())
+ return false
+
+ // with enough ships to carry all the horses
+ if (!has_enough_available_ships_for_horses())
+ return false
+
+ // and a valid destination
+ for (let to of data.seaports)
+ if (to !== here && !has_enemy_lord(to))
+ return true
+
+ return false
+}
+
+function goto_sail() {
+ push_undo()
+ game.state = "sail"
+}
+
+states.sail = {
+ inactive: "Sail",
+ prompt() {
+ view.group = game.group
+
+ let here = get_lord_locale(game.command)
+ let ships = count_group_ships()
+ let horses = count_group_horses()
+ let prov = count_group_assets(PROV)
+
+ let overflow = 0
+ if (game.active === TEUTONS)
+ overflow = (horses + prov) - ships
+ if (game.active === RUSSIANS)
+ overflow = (horses * 2 + prov) - ships
+
+ if (overflow > 0) {
+ view.prompt = `Sailing with ${ships} Ships and ${horses} Horses. Discard Loot or Provender.`
+ // TODO: stricter greed!
+ if (prov > 0) {
+ for (let lord of game.group) {
+ if (get_lord_assets(lord, PROV) > 0)
+ gen_action_prov(lord)
+ }
+ }
+ } else {
+ view.prompt = `Sail: Select a destination Seaport.`
+ for (let to of data.seaports) {
+ if (to === here)
+ continue
+ if (!has_enemy_lord(to))
+ gen_action_locale(to)
+ }
+ }
+ },
+ prov: drop_prov,
+ locale(to) {
+ push_undo()
+ log(`Sailed to %${to}${format_group_move()}.`)
+
+ let from = get_lord_locale(game.command)
+
+ for (let lord of game.group) {
+ set_lord_locale(lord, to)
+ set_lord_moved(lord, 1)
+ }
+
+ if (is_trade_route(to))
+ conquer_trade_route(to)
+
+ spend_all_actions()
+ resume_command()
+ update_supply_possible()
+ },
+}
+
+// === BATTLE ===
+
+function set_active_attacker() {
+ set_active(game.battle.attacker)
+}
+
+function set_active_defender() {
+ if (game.battle.attacker === P1)
+ set_active(P2)
+ else
+ set_active(P1)
+}
+
+function goto_battle() {
+ if (has_unbesieged_enemy_lord(game.march.to))
+ start_battle()
+ else
+ march_with_group_3()
+}
+
+function init_battle(here) {
+ game.battle = {
+ where: here,
+ round: 1,
+ step: 0,
+ relief: 0,
+ attacker: game.active,
+ ambush: 0,
+ conceded: 0,
+ loser: 0,
+ fought: 0, // flag all lords who participated
+ array: [
+ -1, game.command, -1,
+ -1, -1, -1,
+ -1, -1, -1,
+ -1, -1, -1,
+ ],
+ garrison: 0,
+ reserves: [],
+ retreated: 0,
+ rearguard: 0,
+ strikers: 0,
+ warrior_monks: 0,
+ hits: 0,
+ xhits: 0,
+ fc: -1,
+ rc: -1,
+ }
+}
+
+function start_battle() {
+ let here = get_lord_locale(game.command)
+
+ log_h3(`Battle at %${here}`)
+
+ init_battle(here, 0, 0)
+
+ // All attacking lords to reserve
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (get_lord_locale(lord) === here && !is_lord_besieged(lord)) {
+ set_lord_fought(lord)
+ if (lord !== game.command)
+ set_add(game.battle.reserves, lord)
+ }
+ }
+
+ // Array attacking lords if fewer than 3.
+ if (game.battle.reserves.length === 2)
+ game.battle.array[A3] = game.battle.reserves.pop()
+ if (game.battle.reserves.length === 1)
+ game.battle.array[A1] = game.battle.reserves.pop()
+
+ // All defending lords to reserve
+ for (let lord = first_enemy_lord; lord <= last_enemy_lord; ++lord) {
+ if (get_lord_locale(lord) === here && !is_lord_besieged(lord)) {
+ set_lord_fought(lord)
+ set_add(game.battle.reserves, lord)
+ }
+ }
+
+ goto_relief_sally()
+}
+
+function init_garrison(knights, men_at_arms) {
+ game.battle.garrison = { knights, men_at_arms }
+}
+
+// === BATTLE: BATTLE ARRAY ===
+
+// 0) Defender decides to stand for Battle, not Avoid.
+// 1) Attacker decides which Lords will relief sally, if any.
+// 2) Attacker positions front A.
+// 3) Defender positions front D.
+// 4) Attacker positions SA.
+// 5) Defender positions reaguard RG.
+
+function has_friendly_reserves() {
+ for (let lord of game.battle.reserves)
+ if (is_friendly_lord(lord))
+ return true
+ return false
+}
+
+function has_friendly_attacking_reserves() {
+ for (let lord of game.battle.reserves)
+ if (is_friendly_lord(lord) && (game.battle.sally || is_lord_unbesieged(lord)))
+ return true
+ return false
+}
+
+function has_friendly_sallying_reserves() {
+ for (let lord of game.battle.reserves)
+ if (is_friendly_lord(lord) && is_lord_besieged(lord))
+ return true
+ return false
+}
+
+function count_friendly_reserves() {
+ let n = 0
+ for (let lord of game.battle.reserves)
+ if (is_friendly_lord(lord))
+ ++n
+ return n
+}
+
+function pop_first_reserve() {
+ for (let lord of game.battle.reserves) {
+ if (is_friendly_lord(lord)) {
+ set_delete(game.battle.reserves, lord)
+ return lord
+ }
+ }
+ return NOBODY
+}
+
+function prompt_array_place_opposed(X1, X2, X3, Y1, Y3) {
+ let array = game.battle.array
+ if (array[X2] === NOBODY) {
+ gen_action_array(X2)
+ } else if (array[Y1] !== NOBODY && array[Y3] === NOBODY && array[X1] === NOBODY) {
+ gen_action_array(X1)
+ } else if (array[Y1] === NOBODY && array[Y3] !== NOBODY && array[X3] === NOBODY) {
+ gen_action_array(X3)
+ } else {
+ if (array[X1] === NOBODY)
+ gen_action_array(X1)
+ if (array[X3] === NOBODY)
+ gen_action_array(X3)
+ }
+}
+
+function action_array_place(pos) {
+ push_undo_without_who()
+ game.battle.array[pos] = game.who
+ set_delete(game.battle.reserves, game.who)
+ game.who = NOBODY
+}
+
+function goto_array_attacker() {
+ clear_undo()
+ set_active_attacker()
+ game.state = "array_attacker"
+ game.who = NOBODY
+ if (!has_friendly_attacking_reserves())
+ end_array_attacker()
+}
+
+function goto_array_defender() {
+ clear_undo()
+ set_active_defender()
+ game.state = "array_defender"
+ game.who = NOBODY
+ let n = count_friendly_reserves()
+ if (n === 1) {
+ game.battle.array[D2] = pop_first_reserve()
+ end_array_defender()
+ }
+ if (n === 0)
+ end_array_defender()
+}
+
+function goto_array_sally() {
+ clear_undo()
+ set_active_attacker()
+ game.state = "array_sally"
+ game.who = NOBODY
+ if (!has_friendly_sallying_reserves())
+ end_array_sally()
+}
+
+function goto_array_rearguard() {
+ clear_undo()
+ set_active_defender()
+ game.state = "array_rearguard"
+ game.who = NOBODY
+ if (!has_friendly_reserves() || empty(SA2))
+ end_array_rearguard()
+}
+
+// NOTE: The order here can be easily change to attacker/sally/defender/rearguard if desired.
+
+function end_array_attacker() {
+ goto_array_defender()
+}
+
+function end_array_defender() {
+ goto_array_sally()
+}
+
+function end_array_sally() {
+ goto_array_rearguard()
+}
+
+function end_array_rearguard() {
+ goto_attacker_events()
+}
+
+states.array_attacker = {
+ inactive: "Array Attacking Lords",
+ prompt() {
+ view.prompt = "Battle Array: Position your Attacking Lords."
+ let array = game.battle.array
+ let done = true
+ if (array[A1] === NOBODY || array[A2] === NOBODY || array[A3] === NOBODY) {
+ for (let lord of game.battle.reserves) {
+ if (lord !== game.who && is_friendly_lord(lord)) {
+ if (game.battle.sally || is_lord_unbesieged(lord)) {
+ gen_action_lord(lord)
+ done = false
+ }
+ }
+ }
+ }
+ if (game.who === NOBODY && done)
+ view.actions.end_array = 1
+ if (game.who !== NOBODY) {
+ // A2 is already filled by command lord!
+ if (array[A1] === NOBODY)
+ gen_action_array(A1)
+ if (array[A3] === NOBODY)
+ gen_action_array(A3)
+ }
+ },
+ array: action_array_place,
+ lord: action_select_lord,
+ end_array: end_array_attacker,
+}
+
+states.array_defender = {
+ inactive: "Array Defending Lords",
+ prompt() {
+ view.prompt = "Battle Array: Position your Defending Lords."
+ let array = game.battle.array
+ let done = true
+ if (array[D1] === NOBODY || array[D2] === NOBODY || array[D3] === NOBODY) {
+ for (let lord of game.battle.reserves) {
+ if (lord !== game.who && is_friendly_lord(lord)) {
+ gen_action_lord(lord)
+ done = false
+ }
+ }
+ }
+ if (done && game.who === NOBODY)
+ view.actions.end_array = 1
+ if (game.who !== NOBODY)
+ prompt_array_place_opposed(D1, D2, D3, A1, A3)
+ },
+ array: action_array_place,
+ lord: action_select_lord,
+ end_array: end_array_defender,
+}
+
+// === BATTLE: EVENTS ===
+
+function goto_attacker_events() {
+ clear_undo()
+ set_active_attacker()
+ log_br()
+ if (can_play_battle_events())
+ game.state = "attacker_events"
+ else
+ end_attacker_events()
+}
+
+function end_attacker_events() {
+ goto_defender_events()
+}
+
+function goto_defender_events() {
+ set_active_defender()
+ log_br()
+ if (can_play_battle_events())
+ game.state = "defender_events"
+ else
+ end_defender_events()
+}
+
+function end_defender_events() {
+ goto_battle_rounds()
+}
+
+function resume_battle_events() {
+ game.what = -1
+ if (is_attacker())
+ goto_attacker_events()
+ else
+ goto_defender_events()
+}
+
+function could_play_card(c) {
+ if (set_has(game.capabilities, c))
+ return false
+ if (!game.hidden) {
+ // TODO: check capabilities on lords revealed in battle if hidden
+ if (game.pieces.capabilities.includes(c))
+ return false
+ }
+ if (set_has(game.events, c))
+ return false
+ if (is_p1_card(c))
+ return game.hand1.length > 0
+ if (is_p2_card(c))
+ return game.hand2.length > 0
+ return true
+}
+
+function has_lords_in_battle() {
+ for (let p = 0; p < 12; ++p)
+ if (is_friendly_lord(game.battle.array[p]))
+ return true
+ return has_friendly_reserves()
+}
+
+function can_play_battle_events() {
+ if (game.active === TEUTONS) {
+ if (could_play_card(EVENT_TEUTONIC_AMBUSH))
+ return true
+ if (is_defender()) {
+ if (could_play_card(EVENT_TEUTONIC_HILL))
+ return true
+ if (!is_winter())
+ if (could_play_card(EVENT_TEUTONIC_MARSH))
+ return true
+ }
+ if (!is_winter())
+ if (could_play_card(EVENT_TEUTONIC_BRIDGE))
+ return true
+ }
+
+ if (game.active === RUSSIANS) {
+ if (could_play_card(EVENT_RUSSIAN_AMBUSH))
+ return true
+ if (is_defender()) {
+ if (could_play_card(EVENT_RUSSIAN_HILL))
+ return true
+ if (!is_winter())
+ if (could_play_card(EVENT_RUSSIAN_MARSH))
+ return true
+ }
+ if (!is_winter())
+ if (could_play_card(EVENT_RUSSIAN_BRIDGE))
+ return true
+ if (!is_summer())
+ if (could_play_card(EVENT_RUSSIAN_RAVENS_ROCK))
+ return true
+ }
+
+ // Battle or Storm
+ if (game.active === TEUTONS) {
+ if (could_play_card(EVENT_TEUTONIC_FIELD_ORGAN))
+ if (has_lords_in_battle())
+ return true
+ }
+
+ return false
+}
+
+function prompt_battle_events() {
+ // both attacker and defender events
+ if (game.active === TEUTONS) {
+ gen_action_card_if_held(EVENT_TEUTONIC_AMBUSH)
+ if (!is_winter())
+ gen_action_card_if_held(EVENT_TEUTONIC_BRIDGE)
+ if (has_lords_in_battle())
+ gen_action_card_if_held(EVENT_TEUTONIC_FIELD_ORGAN)
+ }
+
+ if (game.active === RUSSIANS) {
+ gen_action_card_if_held(EVENT_RUSSIAN_AMBUSH)
+ if (!is_winter())
+ gen_action_card_if_held(EVENT_RUSSIAN_BRIDGE)
+ if (!is_summer())
+ gen_action_card_if_held(EVENT_RUSSIAN_RAVENS_ROCK)
+ }
+
+ view.actions.done = 1
+}
+
+states.attacker_events = {
+ inactive: "Attacker Events",
+ prompt() {
+ view.prompt = "Attacker may play Events."
+ prompt_battle_events()
+ },
+ card: action_battle_events,
+ done() {
+ end_attacker_events()
+ },
+}
+
+states.defender_events = {
+ inactive: "Defender Events",
+ prompt() {
+ view.prompt = "Defender may play Events."
+
+ prompt_battle_events()
+
+ // defender only events
+ if (game.active === TEUTONS) {
+ if (!is_winter())
+ gen_action_card_if_held(EVENT_TEUTONIC_MARSH)
+ gen_action_card_if_held(EVENT_TEUTONIC_HILL)
+ }
+
+ if (game.active === RUSSIANS) {
+ if (!is_winter())
+ gen_action_card_if_held(EVENT_RUSSIAN_MARSH)
+ gen_action_card_if_held(EVENT_RUSSIAN_HILL)
+ }
+ },
+ card: action_battle_events,
+ done() {
+ end_defender_events()
+ },
+}
+
+function action_battle_events(c) {
+ game.what = c
+ set_delete(current_hand(), c)
+ set_add(game.events, c)
+ switch (c) {
+ case EVENT_TEUTONIC_HILL:
+ case EVENT_TEUTONIC_MARSH:
+ case EVENT_RUSSIAN_HILL:
+ case EVENT_RUSSIAN_MARSH:
+ case EVENT_RUSSIAN_RAVENS_ROCK:
+ // nothing more needs to be done for these
+ log(`Played E${c}.`)
+ resume_battle_events()
+ break
+ case EVENT_TEUTONIC_AMBUSH:
+ case EVENT_RUSSIAN_AMBUSH:
+ log(`Played E${c}.`)
+ if (is_attacker())
+ game.battle.ambush |= 2
+ else
+ game.battle.ambush |= 1
+ break
+ case EVENT_TEUTONIC_BRIDGE:
+ case EVENT_RUSSIAN_BRIDGE:
+ // must select target lord
+ game.state = "bridge"
+ break
+ case EVENT_TEUTONIC_FIELD_ORGAN:
+ // must select target lord
+ game.state = "field_organ"
+ break
+ }
+}
+
+states.bridge = {
+ inactive: "Bridge",
+ prompt() {
+ view.prompt = "Bridge: Play on a Center Lord."
+ view.what = game.what
+ let array = game.battle.array
+ if (is_attacker()) {
+ if (array[D2] !== NOBODY)
+ gen_action_lord(array[D2])
+ if (array[RG2] !== NOBODY)
+ gen_action_lord(array[RG2])
+ } else {
+ // Cannot play on Relief Sallying lord
+ if (array[A2] !== NOBODY)
+ gen_action_lord(array[A2])
+ }
+ },
+ lord(lord) {
+ log(`Played E${game.what} on L${lord}.`)
+ if (!game.battle.bridge)
+ game.battle.bridge = { lord1: NOBODY, lord2: NOBODY, n1: 0, n2: 0 }
+ if (is_p1_lord(lord))
+ game.battle.bridge.lord1 = lord
+ else
+ game.battle.bridge.lord2 = lord
+ resume_battle_events()
+ },
+}
+
+states.field_organ = {
+ inactive: "Field Organ",
+ prompt() {
+ view.prompt = "Field Organ: Play on a Lord."
+ view.what = game.what
+ let array = game.battle.array
+ if (is_attacker()) {
+ for (let pos of battle_attacking_positions)
+ if (array[pos] !== NOBODY)
+ gen_action_lord(array[pos])
+ } else {
+ for (let pos of battle_defending_positions)
+ if (array[pos] !== NOBODY)
+ gen_action_lord(array[pos])
+ }
+ },
+ lord(lord) {
+ log(`Played E${game.what} on L${lord}.`)
+ game.battle.field_organ = lord
+ resume_battle_events()
+ },
+}
+
+
+// === BATTLE: CONCEDE THE FIELD ===
+
+function goto_battle_rounds() {
+ set_active_attacker()
+ goto_concede()
+}
+
+function goto_concede() {
+ log_h4(`Battle Round ${game.battle.round}`)
+ game.state = "concede_battle"
+}
+
+states.concede_battle = {
+ inactive: "Concede",
+ prompt() {
+ view.prompt = "Battle: Concede the Field?"
+ view.actions.concede = 1
+ view.actions.battle = 1
+ },
+ concede() {
+ log(game.active + " Conceded.")
+ game.battle.conceded = game.active
+ goto_reposition_battle()
+ },
+ battle() {
+ set_active_enemy()
+ if (is_attacker())
+ goto_reposition_battle()
+ },
+}
+
+// === BATTLE: REPOSITION ===
+
+function send_to_reserve(pos) {
+ if (game.battle.array[pos] !== NOBODY) {
+ set_add(game.battle.reserves, game.battle.array[pos])
+ game.battle.array[pos] = NOBODY
+ }
+}
+
+function slide_array(from, to) {
+ game.battle.array[to] = game.battle.array[from]
+ game.battle.array[from] = NOBODY
+}
+
+function goto_reposition_battle() {
+ let array = game.battle.array
+
+ // If all D routed.
+ if (array[D1] === NOBODY && array[D2] === NOBODY && array[D3] === NOBODY) {
+ log("Defenders Routed.")
+ }
+
+ // If all A routed.
+ if (array[A1] === NOBODY && array[A2] === NOBODY && array[A3] === NOBODY) {
+ log("Attackers Routed.")
+ }
+
+ set_active_attacker()
+ goto_reposition_advance()
+}
+
+function goto_reposition_advance() {
+ if (can_reposition_advance())
+ game.state = "reposition_advance"
+ else
+ end_reposition_advance()
+}
+
+function end_reposition_advance() {
+ game.who = NOBODY
+ set_active_enemy()
+ if (is_attacker())
+ goto_reposition_center()
+ else
+ goto_reposition_advance()
+}
+
+function goto_reposition_center() {
+ if (can_reposition_center())
+ game.state = "reposition_center"
+ else
+ end_reposition_center()
+}
+
+function end_reposition_center() {
+ game.who = NOBODY
+ set_active_enemy()
+ if (is_attacker())
+ goto_first_strike()
+ else
+ goto_reposition_center()
+}
+
+function can_reposition_advance() {
+ if (has_friendly_reserves()) {
+ let array = game.battle.array
+ if (is_attacker()) {
+ if (array[A1] === NOBODY || array[A2] === NOBODY || array[A3] === NOBODY)
+ return true
+ } else {
+ if (array[D1] === NOBODY || array[D2] === NOBODY || array[D3] === NOBODY)
+ return true
+ }
+ }
+ return false
+}
+
+states.reposition_advance = {
+ inactive: "Reposition",
+ prompt() {
+ view.prompt = "Reposition: Advance from Reserve."
+ let array = game.battle.array
+
+ for (let lord of game.battle.reserves)
+ if (is_friendly_lord(lord) && lord !== game.who)
+ gen_action_lord(lord)
+
+ if (game.who !== NOBODY) {
+ 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)
+ } else {
+ if (array[D1] === NOBODY) gen_action_array(D1)
+ if (array[D2] === NOBODY) gen_action_array(D2)
+ if (array[D3] === NOBODY) gen_action_array(D3)
+ }
+ }
+ },
+ lord(lord) {
+ game.who = lord
+ },
+ array(pos) {
+ set_delete(game.battle.reserves, game.who)
+ game.battle.array[pos] = game.who
+ game.who = NOBODY
+ goto_reposition_advance()
+ },
+}
+
+function can_reposition_center() {
+ let array = game.battle.array
+ if (is_attacker()) {
+ if (array[A2] === NOBODY && (array[A1] !== NOBODY || array[A3] !== NOBODY))
+ return true
+ } else {
+ if (array[D2] === NOBODY && (array[D1] !== NOBODY || array[D3] !== NOBODY))
+ return true
+ }
+ return false
+}
+
+states.reposition_center = {
+ inactive: "Reposition",
+ prompt() {
+ view.prompt = "Reposition: Slide to Center."
+ let array = game.battle.array
+
+ 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])
+ }
+ } else {
+ if (array[D2] === NOBODY) {
+ if (array[D1] !== NOBODY) gen_action_lord(game.battle.array[D1])
+ if (array[D3] !== NOBODY) gen_action_lord(game.battle.array[D3])
+ }
+ }
+
+ if (game.who !== NOBODY) {
+ let from = get_lord_array_position(game.who)
+ if (from === A1 || from === A3) gen_action_array(A2)
+ if (from === D1 || from === D3) gen_action_array(D2)
+ }
+ },
+ lord(lord) {
+ game.who = lord
+ },
+ array(pos) {
+ let from = get_lord_array_position(game.who)
+ slide_array(from, pos)
+ game.who = NOBODY
+ goto_reposition_center()
+ },
+}
+
+// === 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)
+ if (pos === A1 || pos === A3)
+ return NOBODY
+ if (game.battle.ambush & 2)
+ if (pos === D1 || pos === D3)
+ return NOBODY
+ return game.battle.array[pos]
+}
+
+function filled(pos) {
+ return get_battle_array(pos) !== NOBODY
+}
+
+function empty(pos) {
+ return get_battle_array(pos) === NOBODY
+}
+
+const battle_defending_positions = [ D1, D2, D3 ]
+const battle_attacking_positions = [ A1, A2, A3 ]
+
+const battle_steps = [
+ { 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 },
+]
+
+function count_zero_hits(_) {
+ return 0
+}
+
+function count_archery_xhits(lord) {
+ let xhits = 0
+ if (lord_has_capability(lord, AOW_TEUTONIC_BALISTARII) || lord_has_capability(lord, AOW_RUSSIAN_STRELTSY))
+ xhits += get_lord_forces(lord, MEN_AT_ARMS)
+ if (is_hill_in_play())
+ return xhits << 1
+ return xhits
+}
+
+function count_archery_hits(lord) {
+ let hits = 0
+ if (!is_marsh_in_play()) {
+ if (lord_has_capability(lord, AOW_RUSSIAN_LUCHNIKI)) {
+ hits += get_lord_forces(lord, LIGHT_HORSE)
+ hits += get_lord_forces(lord, MILITIA)
+ }
+ hits += get_lord_forces(lord, ASIATIC_HORSE)
+ } else {
+ if (lord_has_capability(lord, AOW_RUSSIAN_LUCHNIKI)) {
+ hits += get_lord_forces(lord, MILITIA)
+ }
+ }
+ if (is_hill_in_play())
+ return hits << 1
+ return hits
+}
+
+function count_melee_hits(lord) {
+ return count_horse_hits(lord) + count_foot_hits(lord)
+}
+
+function assemble_melee_forces(lord) {
+ let forces = {
+ knights: get_lord_forces(lord, KNIGHTS),
+ sergeants: get_lord_forces(lord, SERGEANTS),
+ light_horse: get_lord_forces(lord, LIGHT_HORSE),
+ men_at_arms: get_lord_forces(lord, MEN_AT_ARMS),
+ militia: get_lord_forces(lord, MILITIA),
+ serfs: get_lord_forces(lord, SERFS),
+ }
+
+ if (is_marsh_in_play()) {
+ forces.knights = 0
+ forces.sergeants = 0
+ forces.light_horse = 0
+ }
+
+ if (game.battle.bridge && (game.battle.bridge.lord1 === lord || game.battle.bridge.lord2 === lord)) {
+ let n = is_p1_lord(lord) ? game.battle.bridge.n1 : game.battle.bridge.n2
+
+ log(`Bridge L${lord}`)
+
+ if (is_horse_step()) {
+ // Pick at most 1 LH if there are any Foot (for +1/2 rounding benefit)
+ if (forces.men_at_arms + forces.militia + forces.serfs > 0 && forces.light_horse > 1)
+ forces.light_horse = 1
+
+ if (forces.knights >= n)
+ forces.knights = n
+ n -= forces.knights
+ if (forces.sergeants >= n)
+ forces.sergeants = n
+ n -= forces.sergeants
+ if (forces.light_horse >= n)
+ forces.light_horse = n
+ n -= forces.light_horse
+
+ if (forces.knights > 0) logi(`${forces.knights} Knights`)
+ if (forces.sergeants > 0) logi(`${forces.sergeants} Sergeants`)
+ if (forces.light_horse > 0) logi(`${forces.light_horse} Light Horse`)
+ if (forces.knights + forces.sergeants + forces.light_horse === 0) logi(`None`)
+ }
+
+ if (is_foot_step()) {
+ if (forces.men_at_arms >= n)
+ forces.men_at_arms = n
+ n -= forces.men_at_arms
+ if (forces.militia >= n)
+ forces.militia = n
+ n -= forces.militia
+ if (forces.serfs >= n)
+ forces.serfs = n
+ n -= forces.serfs
+
+ if (forces.men_at_arms > 0) logi(`${forces.men_at_arms} Men-at-Arms`)
+ if (forces.militia > 0) logi(`${forces.militia} Militia`)
+ if (forces.serfs > 0) logi(`${forces.serfs} Serfs`)
+ if (forces.men_at_arms + forces.militia + forces.serfs === 0) logi(`None`)
+ }
+
+ if (is_p1_lord(lord))
+ game.battle.bridge.n1 = n
+ else
+ game.battle.bridge.n2 = n
+ }
+
+ return forces
+}
+
+function count_horse_hits(lord) {
+ let hits = 0
+ if (!is_marsh_in_play()) {
+ let forces = assemble_melee_forces(lord)
+
+ hits += forces.knights << 2
+ hits += forces.sergeants << 1
+ hits += forces.light_horse
+
+ if (game.battle.field_organ === lord && game.battle.round === 1) {
+ log(`E${EVENT_TEUTONIC_FIELD_ORGAN} L${lord}.`)
+ hits += forces.knights << 1
+ hits += forces.sergeants << 1
+ }
+ }
+ return hits
+}
+
+function count_foot_hits(lord) {
+ let forces = assemble_melee_forces(lord)
+ let hits = 0
+ hits += forces.men_at_arms << 1
+ hits += forces.militia
+ hits += forces.serfs
+ return hits
+}
+
+function count_garrison_xhits() {
+ if (is_archery_step())
+ return game.battle.garrison.men_at_arms
+ return 0
+}
+
+function count_garrison_hits() {
+ if (is_melee_step())
+ return (game.battle.garrison.knights << 1) + (game.battle.garrison.men_at_arms << 1)
+ return 0
+}
+
+function count_lord_xhits(lord) {
+ return battle_steps[game.battle.step].xhits(lord)
+}
+
+function count_lord_hits(lord) {
+ return battle_steps[game.battle.step].hits(lord)
+}
+
+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
+ 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 is_horse_step() {
+ return game.battle.step === 2 || game.battle.step === 3
+}
+
+function is_foot_step() {
+ return game.battle.step === 4 || game.battle.step === 5
+}
+
+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() {
+ return is_attacker_step() ? battle_attacking_positions : battle_defending_positions
+}
+
+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)
+ }
+}
+
+function has_strike_target(S) {
+ if (is_attacker_step() && has_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)
+}
+
+function has_no_strike_targets() {
+ if (is_defender_step() && has_garrison())
+ if (has_strike_target(D2))
+ return false
+ for (let striker of game.battle.strikers)
+ if (has_strike_target(striker))
+ return false
+ return true
+}
+
+function has_no_strikers_and_strike_targets() {
+ if (is_defender_step() && has_garrison()) {
+ if (is_archery_step() && game.battle.garrison.men_at_arms > 0)
+ if (has_strike_target(D2))
+ return false
+ if (is_melee_step() && game.battle.garrison.men_at_arms + game.battle.garrison.knights > 0)
+ if (has_strike_target(D2))
+ return false
+ }
+ 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)
+}
+
+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(S2, T1, T2, T3) {
+ if (has_strike(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 1000 // No target!
+}
+
+function strike_defender_row() {
+ 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 ===
+
+// 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_strike_step() {
+ return battle_steps[game.battle.step].name
+}
+
+function format_hits() {
+ if (game.battle.xhits > 0 && game.battle.hits > 0) {
+ if (game.battle.xhits > 1 && game.battle.hits > 1)
+ return `${game.battle.xhits} Crossbow Hits and ${game.battle.hits} Hits`
+ else if (game.battle.xhits > 1)
+ return `${game.battle.xhits} Crossbow Hits and ${game.battle.hits} Hit`
+ else if (game.battle.hits > 1)
+ return `${game.battle.xhits} Crossbow Hit and ${game.battle.hits} Hits`
+ else
+ return `${game.battle.xhits} Crossbow Hit and ${game.battle.hits} Hit`
+ } else if (game.battle.xhits > 0) {
+ if (game.battle.xhits > 1)
+ return `${game.battle.xhits} Crossbow Hits`
+ else
+ return `${game.battle.xhits} Crossbow Hit`
+ } else {
+ if (game.battle.hits > 1)
+ return `${game.battle.hits} Hits`
+ else
+ return `${game.battle.hits} Hit`
+ }
+}
+
+function goto_first_strike() {
+ game.battle.step = 0
+
+ if (game.battle.bridge) {
+ game.battle.bridge.n1 = game.battle.round * 2
+ game.battle.bridge.n2 = game.battle.round * 2
+ }
+
+ goto_strike()
+}
+
+function goto_next_strike() {
+ let end = 6
+ game.battle.step++
+ if (game.battle.step >= end)
+ end_battle_round()
+ else
+ goto_strike()
+}
+
+function goto_strike() {
+ // Exit early if one side is completely routed
+ if (is_battle_over()) {
+ end_battle_round()
+ return
+ }
+
+ if (is_attacker_step())
+ set_active_attacker()
+ else
+ set_active_defender()
+
+ log_h5(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
+ }
+ }
+
+ if (is_marsh_in_play()) {
+ if (game.active === TEUTONS)
+ logevent(EVENT_RUSSIAN_MARSH)
+ else
+ logevent(EVENT_TEUTONIC_MARSH)
+ }
+
+ if (is_archery_step() && is_hill_in_play()) {
+ if (game.active === TEUTONS)
+ logevent(EVENT_TEUTONIC_HILL)
+ else
+ logevent(EVENT_RUSSIAN_HILL)
+ }
+
+ // Generate hits
+ game.battle.ah = [ 0, 0, 0, 0, 0, 0 ]
+ game.battle.ahx = [ 0, 0, 0, 0, 0, 0 ]
+
+ for (let pos of current_strike_positions()) {
+ let lord = get_battle_array(pos)
+ if (lord !== NOBODY) {
+ let hits = count_lord_hits(lord)
+ let xhits = count_lord_xhits(lord)
+
+ game.battle.ah[pos] = hits
+ game.battle.ahx[pos] = xhits
+
+ if (xhits > 2)
+ log(`L${lord} ${frac(xhits)} Crossbow Hits.`)
+ else if (xhits > 0)
+ log(`L${lord} ${frac(xhits)} Crossbow Hit.`)
+ if (hits > 2)
+ log(`L${lord} ${frac(hits)} Hits.`)
+ else if (hits > 0)
+ log(`L${lord} ${frac(hits)} Hit.`)
+ }
+ }
+
+ if (did_concede())
+ log("Pursuit.")
+
+ // Strike left or right or defender
+ if (is_attacker_step())
+ game.battle.fc = strike_left_or_right(A2, D1, D2, D3)
+ else
+ game.battle.fc = strike_left_or_right(D2, A1, A2, A3)
+
+ if (has_no_strikers_and_strike_targets())
+ log("None.")
+
+ resume_strike()
+}
+
+function resume_strike() {
+ 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
+ goto_strike_group()
+}
+
+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 prompt_left_right() {
+ 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)
+}
+
+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
+ game.battle.rc = pos
+}
+
+states.strike_left_right = {
+ get inactive() {
+ return format_strike_step() + " \u2014 Strike"
+ },
+ prompt: prompt_left_right,
+ lord(lord) {
+ action_left_right(lord)
+ resume_strike()
+ },
+}
+
+states.assign_left_right = {
+ get inactive() {
+ return format_strike_step() + " \u2014 Strike"
+ },
+ prompt: prompt_left_right,
+ lord(lord) {
+ action_left_right(lord)
+ set_active_enemy()
+ goto_assign_hits()
+ },
+}
+
+function goto_strike_group() {
+ game.state = "strike_group"
+}
+
+function select_strike_group(pos) {
+ game.battle.strikers = create_strike_group(pos)
+ goto_strike_total_hits()
+}
+
+states.strike_group = {
+ get inactive() {
+ return format_strike_step() + " \u2014 Strike"
+ },
+ prompt() {
+ view.prompt = `${format_strike_step()}: Strike.`
+ if (has_garrison_strike()) {
+ view.actions.garrison = 1
+ if (!has_strike(D2))
+ view.prompt = `${format_strike_step()}: Strike with Garrison.`
+ }
+ for (let pos of current_strike_positions())
+ if (has_strike(pos))
+ gen_action_lord(game.battle.array[pos])
+ },
+ lord(lord) {
+ select_strike_group(get_lord_array_position(lord))
+ },
+ garrison() {
+ if (has_strike(D2))
+ select_strike_group(D2)
+ else
+ select_strike_group(-1)
+ },
+}
+
+// === BATTLE: TOTAL HITS (ROUND UP) ===
+
+function goto_strike_total_hits() {
+ let hits = 0
+ let xhits = 0
+
+ let slist = []
+
+ // Total hits
+ for (let pos of game.battle.strikers) {
+ if (game.battle.ah[pos] + game.battle.ahx[pos] > 0) {
+ slist.push(lord_name[game.battle.array[pos]])
+ hits += game.battle.ah[pos]
+ xhits += game.battle.ahx[pos]
+ }
+ }
+
+ // 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)
+ }
+
+ // Conceding side halves its total Hits, rounded up.
+ if (did_concede()) {
+ hits = (hits + 1) >> 1
+ xhits = (xhits + 1) >> 1
+ }
+
+ game.battle.hits = hits
+ game.battle.xhits = xhits
+
+ log_br()
+ log(slist.join(", "))
+
+ goto_strike_roll_walls()
+}
+
+// === BATTLE: ROLL WALLS ===
+
+function goto_strike_roll_walls() {
+ set_active_enemy()
+
+ if (game.battle.xhits > 0)
+ log_hits(game.battle.xhits, "Crossbow Hit")
+ if (game.battle.hits > 0)
+ log_hits(game.battle.hits, "Hit")
+
+ game.who = -2
+ goto_assign_hits()
+}
+
+function log_hits(total, name) {
+ if (total === 1)
+ logi(`${total} ${name}`)
+ else if (total > 1)
+ logi(`${total} ${name}s`)
+ else
+ logi(`No ${name}s`)
+}
+
+// === BATTLE: ASSIGN HITS TO UNITS / ROLL BY HIT / ROUT ===
+
+function goto_assign_hits() {
+ if (game.battle.hits + game.battle.xhits === 0)
+ return end_assign_hits()
+
+ if (has_no_strike_targets()) {
+ log("Lost " + format_hits() + ".")
+ return end_assign_hits()
+ }
+
+ if (is_attacker_step()) {
+ if (game.battle.fc < 0 && set_has(game.battle.strikers, A2))
+ return goto_assign_left_right()
+ } else {
+ if (game.battle.fc < 0 && set_has(game.battle.strikers, D2))
+ return goto_assign_left_right()
+ }
+
+ game.state = "assign_hits"
+}
+
+function goto_assign_left_right() {
+ set_active_enemy()
+ game.state = "assign_left_right"
+}
+
+function end_assign_hits() {
+ for (let pos of game.battle.strikers) {
+ game.battle.ah[pos] = 0
+ game.battle.ahx[pos] = 0
+ }
+ game.who = NOBODY
+ game.battle.strikers = 0
+ game.battle.hits = 0
+ game.battle.xhits = 0
+ set_active_enemy()
+ resume_strike()
+}
+
+function for_each_target(fn) {
+ if (is_defender_step() && has_garrison()) {
+ if (filled(A2))
+ fn(game.battle.array[A2])
+ return
+ }
+
+ let start = game.battle.strikers[0]
+
+ let target = find_strike_target(start)
+
+ fn(game.battle.array[target])
+
+ // If any striker flanks target, target must take all hits
+ for (let striker of game.battle.strikers)
+ if (flanks_position(striker, target))
+ return
+
+ // 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])
+}
+
+function prompt_hit_armored_forces() {
+ let has_armored = false
+ for_each_target(lord => {
+ if (get_lord_forces(lord, KNIGHTS) > 0) {
+ gen_action_knights(lord)
+ has_armored = true
+ }
+ if (get_lord_forces(lord, SERGEANTS) > 0) {
+ gen_action_sergeants(lord)
+ has_armored = true
+ }
+ if (get_lord_forces(lord, MEN_AT_ARMS) > 0) {
+ gen_action_men_at_arms(lord)
+ has_armored = true
+ }
+ })
+ return has_armored
+}
+
+function prompt_hit_unarmored_forces() {
+ 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)
+ gen_action_asiatic_horse(lord)
+ if (get_lord_forces(lord, MILITIA) > 0)
+ gen_action_militia(lord)
+ if (get_lord_forces(lord, SERFS) > 0)
+ gen_action_serfs(lord)
+ })
+}
+
+function prompt_hit_forces() {
+ for_each_target(lord => {
+ if (get_lord_forces(lord, KNIGHTS) > 0)
+ gen_action_knights(lord)
+ if (get_lord_forces(lord, SERGEANTS) > 0)
+ gen_action_sergeants(lord)
+ if (get_lord_forces(lord, LIGHT_HORSE) > 0)
+ gen_action_light_horse(lord)
+ if (get_lord_forces(lord, ASIATIC_HORSE) > 0)
+ gen_action_asiatic_horse(lord)
+ if (get_lord_forces(lord, MEN_AT_ARMS) > 0)
+ gen_action_men_at_arms(lord)
+ if (get_lord_forces(lord, MILITIA) > 0)
+ gen_action_militia(lord)
+ if (get_lord_forces(lord, SERFS) > 0)
+ gen_action_serfs(lord)
+ })
+}
+
+states.assign_hits = {
+ get inactive() {
+ return format_strike_step() + " \u2014 Assign " + format_hits()
+ },
+ prompt() {
+ view.prompt = `${format_strike_step()}: Assign ${format_hits()} to units.`
+
+ view.group = game.battle.strikers.map(p => game.battle.array[p])
+
+ prompt_hit_forces()
+ },
+ knights(lord) {
+ action_assign_hits(lord, KNIGHTS)
+ },
+ sergeants(lord) {
+ action_assign_hits(lord, SERGEANTS)
+ },
+ light_horse(lord) {
+ action_assign_hits(lord, LIGHT_HORSE)
+ },
+ asiatic_horse(lord) {
+ action_assign_hits(lord, ASIATIC_HORSE)
+ },
+ men_at_arms(lord) {
+ action_assign_hits(lord, MEN_AT_ARMS)
+ },
+ militia(lord) {
+ action_assign_hits(lord, MILITIA)
+ },
+ serfs(lord) {
+ action_assign_hits(lord, SERFS)
+ },
+}
+
+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 defender
+
+ if (pos >= A1 && pos <= A3) {
+ game.battle.fc = strike_left_or_right(D2, A1, A2, A3)
+ }
+
+ else if (pos >= D1 && pos <= D3) {
+ game.battle.fc = strike_left_or_right(A2, D1, D2, D3)
+ if (is_sa_without_rg())
+ game.battle.rc = strike_defender_row()
+ }
+}
+
+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 remove_serf(lord) {
+ add_lord_forces(lord, SERFS, -1)
+ game.pieces.smerdi++
+}
+
+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 which_lord_capability(lord, list) {
+ for (let c of list)
+ if (lord_has_capability_card(lord, c))
+ return c
+ return -1
+}
+
+function assign_hit_roll(what, prot, extra) {
+ let die = roll_die()
+ if (die <= prot) {
+ logi(`${what} ${range(prot)}: ${MISS[die]}${extra}`)
+ return false
+ } else {
+ logi(`${what} ${range(prot)}: ${HIT[die]}${extra}`)
+ return true
+ }
+
+}
+
+function action_assign_hits(lord, type) {
+ let protection = FORCE_PROTECTION[type]
+ let evade = FORCE_EVADE[type]
+
+ if (game.who !== lord) {
+ game.who = lord
+ if (lord === GARRISON)
+ log("Garrison")
+ else
+ log(`L${lord}`)
+ }
+
+ let extra = ""
+
+ let crossbow = 0
+ if (is_armored_force(type) && game.battle.xhits > 0) {
+ extra += " (-2\xa0Crossbow)"
+ crossbow = 2
+ }
+
+ if (type === SERGEANTS || type === MEN_AT_ARMS) {
+ if (lord_has_capability(lord, AOW_TEUTONIC_HALBBRUDER)) {
+ extra += ` (+1\xa0C${which_lord_capability(lord, AOW_TEUTONIC_HALBBRUDER)})`
+ protection += 1
+ }
+ }
+
+ // Evade only in Battle Melee steps
+ if (evade > 0 && is_melee_step()) {
+ if (assign_hit_roll(FORCE_TYPE_NAME[type], evade, extra))
+ rout_unit(lord, type)
+ } else if (protection > 0) {
+ if (assign_hit_roll(FORCE_TYPE_NAME[type], protection - crossbow, extra)) {
+ if (use_warrior_monks(lord, type)) {
+ let monks = which_lord_capability(lord, AOW_TEUTONIC_WARRIOR_MONKS)
+ if (assign_hit_roll(`C${monks}`, protection - crossbow, extra))
+ rout_unit(lord, type)
+ } else {
+ rout_unit(lord, type)
+ }
+ }
+ } else {
+ logi(`${FORCE_TYPE_NAME[type]} removed`)
+ remove_serf(lord, type)
+ }
+
+ if (game.battle.xhits)
+ game.battle.xhits--
+ else
+ game.battle.hits--
+
+ if (!lord_has_unrouted_units(lord))
+ rout_lord(lord)
+
+ goto_assign_hits()
+}
+
+// === BATTLE: NEW ROUND ===
+
+function end_battle_round() {
+ if (game.battle.conceded) {
+ game.battle.loser = game.battle.conceded
+ end_battle()
+ return
+ }
+
+ set_active_attacker()
+ if (has_no_unrouted_forces()) {
+ game.battle.loser = game.active
+ end_battle()
+ return
+ }
+
+ set_active_defender()
+ if (has_no_unrouted_forces()) {
+ game.battle.loser = game.active
+ end_battle()
+ return
+ }
+
+ game.battle.round ++
+
+ game.battle.ambush = 0
+
+ set_active_attacker()
+ goto_concede()
+}
+
+// === ENDING THE BATTLE ===
+
+// Ending the Battle - optimized from rules as written
+// Loser retreat / withdraw / remove
+// Loser losses
+// Loser service
+// Victor losses
+// Victor spoils
+
+// Ending the Storm
+// Sack (loser removes lords)
+// Victor losses
+// Victor spoils
+
+function set_active_loser() {
+ set_active(game.battle.loser)
+}
+
+function set_active_victor() {
+ if (game.battle.loser === P1)
+ set_active(P2)
+ else
+ set_active(P1)
+}
+
+function end_battle() {
+ log_h4(`${game.battle.loser} Lost`)
+
+ game.battle.array = 0
+
+ goto_battle_withdraw()
+}
+
+// === ENDING THE STORM: SACK ===
+
+function award_spoils(n) {
+ add_spoils(PROV, n)
+ add_spoils(COIN, n)
+}
+
+function goto_sack() {
+ let here = game.battle.where
+
+ set_active_victor()
+
+ log(`${game.active} Sacked %${here}.`)
+
+ conquer_stronghold(game.battle.where)
+
+ remove_walls(game.battle.where)
+
+ if (here === LOC_NOVGOROD) {
+ if (game.pieces.veche_coin > 0) {
+ add_spoils(COIN, game.pieces.veche_coin)
+ log(`Awarded ${game.pieces.veche_coin} Coin from Veche.`)
+ game.pieces.veche_coin = 0
+ }
+ award_spoils(3)
+ }
+ else if (is_city(here))
+ award_spoils(2)
+ else if (is_fort(here))
+ award_spoils(1)
+ else if (is_bishopric(here))
+ award_spoils(2)
+ else if (is_castle(here))
+ award_spoils(1)
+
+ set_active_loser()
+ resume_sack()
+}
+
+function resume_sack() {
+ if (has_friendly_lord(game.battle.where))
+ game.state = "sack"
+ else
+ goto_battle_losses_loser()
+}
+
+states.sack = {
+ inactive: "Remove Lords",
+ prompt() {
+ let here = game.battle.where
+ view.prompt = `Sack: Remove all Lords at ${data.locales[here].name}.`
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === here)
+ gen_action_lord(lord)
+ },
+ lord(lord) {
+ transfer_assets_except_ships(lord)
+ if (can_ransom_lord_battle(lord)) {
+ goto_ransom(lord)
+ } else {
+ disband_lord(lord, true)
+ resume_sack()
+ }
+ },
+}
+
+function end_ransom_sack() {
+ resume_sack()
+}
+
+// === ENDING THE BATTLE: WITHDRAW ===
+
+function withdrawal_capacity_needed(here) {
+ let has_upper = 0
+ let has_other = 0
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (get_lord_locale(lord) === here && is_lord_unbesieged(lord) && !is_lower_lord(lord)) {
+ if (is_upper_lord(lord))
+ has_upper++
+ else
+ has_other++
+ }
+ }
+ if (has_upper)
+ return 2
+ if (has_other)
+ return 1
+ return 0
+}
+
+function goto_battle_withdraw() {
+ set_active_loser()
+ game.spoils = 0
+ let here = game.battle.where
+ let wn = withdrawal_capacity_needed(here)
+ if (wn > 0 && can_withdraw(here, wn)) {
+ game.state = "battle_withdraw"
+ } else {
+ end_battle_withdraw()
+ }
+}
+
+function end_battle_withdraw() {
+ goto_retreat()
+}
+
+states.battle_withdraw = {
+ inactive: "Withdraw",
+ prompt() {
+ let here = game.battle.where
+ let capacity = stronghold_capacity(here)
+
+ view.prompt = "Battle: Select Lords to Withdraw into Stronghold."
+
+ // NOTE: Sallying lords are still flagged "besieged" and are thus already withdrawn!
+
+ if (capacity >= 1) {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (get_lord_locale(lord) === here && !is_lower_lord(lord) && is_lord_unbesieged(lord)) {
+ if (is_upper_lord(lord)) {
+ if (capacity >= 2)
+ gen_action_lord(lord)
+ } else {
+ gen_action_lord(lord)
+ }
+ }
+ }
+ }
+
+ view.actions.end_withdraw = 1
+ },
+ lord(lord) {
+ push_undo()
+ let lower = get_lower_lord(lord)
+
+ log(`L${lord} Withdrew.`)
+ set_lord_besieged(lord, 1)
+
+ if (lower !== NOBODY) {
+ log(`L${lower} Withdrew.`)
+ set_lord_besieged(lower, 1)
+ }
+ },
+ end_withdraw() {
+ clear_undo()
+ end_battle_withdraw()
+ },
+}
+
+// === ENDING THE BATTLE: RETREAT ===
+
+function count_retreat_transport(type) {
+ let n = 0
+ for (let lord of game.battle.retreated)
+ n += count_lord_transport(lord, type)
+ return n
+}
+
+function count_retreat_assets(type) {
+ let n = 0
+ for (let lord of game.battle.retreated)
+ n += get_lord_assets(lord, type)
+ return n
+}
+
+function transfer_assets_except_ships(lord) {
+ add_spoils(PROV, get_lord_assets(lord, PROV))
+ add_spoils(COIN, get_lord_assets(lord, COIN))
+ add_spoils(CART, get_lord_assets(lord, CART))
+ set_lord_assets(lord, PROV, 0)
+ set_lord_assets(lord, COIN, 0)
+ set_lord_assets(lord, CART, 0)
+}
+
+function can_retreat_to(to) {
+ return !has_unbesieged_enemy_lord(to) && !is_unbesieged_enemy_stronghold(to)
+}
+
+function can_retreat() {
+ if (game.march) {
+ // Battle after March
+ 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))
+ return true
+ } else {
+ // Battle after Sally
+ for (let to of data.locales[game.battle.where].adjacent)
+ if (can_retreat_to(to))
+ return true
+ }
+ return false
+}
+
+function goto_retreat() {
+ let here = game.battle.where
+ if (count_unbesieged_friendly_lords(here) > 0 && can_retreat()) {
+ game.battle.retreated = []
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === here && is_lord_unbesieged(lord))
+ set_add(game.battle.retreated, lord)
+ game.state = "retreat"
+ } else {
+ end_retreat()
+ }
+}
+
+function end_retreat() {
+ goto_battle_remove()
+}
+
+states.retreat = {
+ inactive: "Retreat",
+ prompt() {
+ view.prompt = "Battle: Retreat losing Lords."
+ view.group = game.battle.retreated
+ if (game.march) {
+ // after March
+ if (is_attacker()) {
+ gen_action_locale(game.march.from)
+ } else {
+ for (let [to, way] of data.locales[game.battle.where].ways)
+ if (way !== game.march.approach && can_retreat_to(to))
+ gen_action_locale(to)
+ }
+ } else {
+ // after Sally
+ for (let to of data.locales[game.battle.where].adjacent)
+ if (can_retreat_to(to))
+ gen_action_locale(to)
+ }
+ },
+ locale(to) {
+ push_undo()
+ if (game.march) {
+ if (is_attacker()) {
+ game.battle.retreat_to = to
+ game.battle.retreat_way = game.march.approach
+ retreat_1()
+ } else {
+ let ways = list_ways(game.battle.where, to)
+ if (ways.length > 2) {
+ game.battle.retreat_to = to
+ game.state = "retreat_way"
+ } else {
+ game.battle.retreat_to = to
+ game.battle.retreat_way = ways[1]
+ retreat_1()
+ }
+ }
+ } else {
+ let ways = list_ways(game.battle.where, to)
+ if (ways.length > 2) {
+ game.battle.retreat_to = to
+ game.state = "retreat_way"
+ } else {
+ game.battle.retreat_to = to
+ game.battle.retreat_way = ways[1]
+ retreat_1()
+ }
+ }
+ },
+}
+
+states.retreat_way = {
+ inactive: "Retreat",
+ prompt() {
+ view.prompt = `Retreat: Select Way.`
+ view.group = game.battle.retreated
+ let from = game.battle.where
+ let to = game.battle.retreat_to
+ let ways = list_ways(from, to)
+ for (let i = 1; i < ways.length; ++i)
+ gen_action_way(ways[i])
+ },
+ way(way) {
+ game.battle.retreat_way = way
+ retreat_1()
+ },
+}
+
+function retreat_1() {
+ // Retreated without having conceded the Field
+ if (did_not_concede()) {
+ for (let lord of game.battle.retreated)
+ transfer_assets_except_ships(lord)
+ retreat_2()
+ } else {
+ let way = game.battle.retreat_way
+ let transport = count_retreat_transport(data.ways[way].type)
+ let prov = count_retreat_assets(PROV)
+ if (prov > transport)
+ game.state = "retreat_laden"
+ else
+ retreat_2()
+ }
+}
+
+states.retreat_laden = {
+ inactive: "Retreat",
+ prompt() {
+ let to = game.battle.retreat_to
+ let way = game.battle.retreat_way
+ let transport = count_retreat_transport(data.ways[way].type)
+ let prov = count_retreat_assets(PROV)
+
+ if (prov > transport)
+ view.prompt = `Retreat: Hindered with ${prov} Provender and ${transport} Transport.`
+ else
+ view.prompt = `Retreat: Unladen.`
+ view.group = game.battle.retreated
+
+ if (prov > transport) {
+ view.prompt += " Discard Provender."
+ for (let lord of game.battle.retreated) {
+ if (get_lord_assets(lord, PROV) > 0)
+ gen_action_prov(lord)
+ }
+ } else {
+ gen_action_locale(to)
+ view.actions.retreat = 1
+ }
+ },
+ prov(lord) {
+ spoil_prov(lord)
+ },
+ locale(_) {
+ retreat_2()
+ },
+ retreat() {
+ retreat_2()
+ },
+}
+
+function retreat_2() {
+ let to = game.battle.retreat_to
+ let way = game.battle.retreat_way
+
+ if (data.ways[way].name)
+ log(`Retreated via W${way} to %${to}.`)
+ else
+ log(`Retreated to %${to}.`)
+
+ for (let lord of game.battle.retreated)
+ set_lord_locale(lord, to)
+
+ if (is_trade_route(to))
+ conquer_trade_route(to)
+
+ game.battle.retreat_to = 0
+ game.battle.retreat_way = 0
+ end_retreat()
+}
+
+// === ENDING THE BATTLE: REMOVE ===
+
+function goto_battle_remove() {
+ if (count_unbesieged_friendly_lords(game.battle.where) > 0)
+ game.state = "battle_remove"
+ else
+ goto_battle_losses_loser()
+}
+
+states.battle_remove = {
+ inactive: "Remove Lords",
+ prompt() {
+ view.prompt = "Battle: Remove losing Lords who cannot Retreat or Withdraw."
+ let here = game.battle.where
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === here && is_lord_unbesieged(lord))
+ gen_action_lord(lord)
+ },
+ lord(lord) {
+ transfer_assets_except_ships(lord)
+ if (can_ransom_lord_battle(lord)) {
+ goto_ransom(lord)
+ } else {
+ disband_lord(lord, true)
+ goto_battle_remove()
+ }
+ },
+}
+
+function end_ransom_battle_remove() {
+ goto_battle_remove()
+}
+
+// === ENDING THE BATTLE: LOSSES ===
+
+function has_battle_losses() {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (lord_has_routed_units(lord))
+ return true
+ return false
+}
+
+function goto_battle_losses_loser() {
+ clear_undo()
+ set_active_loser()
+ game.who = NOBODY
+ if (has_battle_losses())
+ if (game.active === P1)
+ log_h4("Teutonic Losses")
+ else
+ log_h4("Russian Losses")
+ resume_battle_losses()
+}
+
+function goto_battle_losses_victor() {
+ clear_undo()
+ set_active_victor()
+ game.who = NOBODY
+ if (has_battle_losses())
+ if (game.active === P1)
+ log_h4("Teutonic Losses")
+ else
+ log_h4("Russian Losses")
+ resume_battle_losses()
+}
+
+function resume_battle_losses() {
+ game.state = "battle_losses"
+ if (!has_battle_losses())
+ goto_battle_losses_remove()
+}
+
+function action_losses(lord, type) {
+ let protection = FORCE_PROTECTION[type]
+ let evade = FORCE_EVADE[type]
+ let target = Math.max(protection, evade)
+
+ // 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
+
+ if (game.who !== lord) {
+ log(`L${lord}`)
+ game.who = lord
+ }
+
+ let die = roll_die()
+ if (die <= target) {
+ logi(`${FORCE_TYPE_NAME[type]} ${range(target)}: ${MISS[die]}`)
+ add_lord_routed_forces(lord, type, -1)
+ add_lord_forces(lord, type, 1)
+ } else {
+ logi(`${FORCE_TYPE_NAME[type]} ${range(target)}: ${HIT[die]}`)
+ add_lord_routed_forces(lord, type, -1)
+ }
+
+ resume_battle_losses()
+}
+
+states.battle_losses = {
+ inactive: "Losses",
+ prompt() {
+ view.prompt = "Losses: Determine the fate of your Routed units."
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (is_lord_on_map(lord) && lord_has_routed_units(lord)) {
+ if (get_lord_routed_forces(lord, KNIGHTS) > 0)
+ gen_action_routed_knights(lord)
+ if (get_lord_routed_forces(lord, SERGEANTS) > 0)
+ gen_action_routed_sergeants(lord)
+ if (get_lord_routed_forces(lord, LIGHT_HORSE) > 0)
+ gen_action_routed_light_horse(lord)
+ if (get_lord_routed_forces(lord, ASIATIC_HORSE) > 0)
+ gen_action_routed_asiatic_horse(lord)
+ if (get_lord_routed_forces(lord, MEN_AT_ARMS) > 0)
+ gen_action_routed_men_at_arms(lord)
+ if (get_lord_routed_forces(lord, MILITIA) > 0)
+ gen_action_routed_militia(lord)
+ if (get_lord_routed_forces(lord, SERFS) > 0)
+ gen_action_routed_serfs(lord)
+ }
+ }
+ },
+ routed_knights(lord) {
+ action_losses(lord, KNIGHTS)
+ },
+ routed_sergeants(lord) {
+ action_losses(lord, SERGEANTS)
+ },
+ routed_light_horse(lord) {
+ action_losses(lord, LIGHT_HORSE)
+ },
+ routed_asiatic_horse(lord) {
+ action_losses(lord, ASIATIC_HORSE)
+ },
+ routed_men_at_arms(lord) {
+ action_losses(lord, MEN_AT_ARMS)
+ },
+ routed_militia(lord) {
+ action_losses(lord, MILITIA)
+ },
+ routed_serfs(lord) {
+ action_losses(lord, SERFS)
+ },
+}
+
+// === ENDING THE BATTLE: LOSSES (REMOVE LORDS) ===
+
+function goto_battle_losses_remove() {
+ game.state = "battle_losses_remove"
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (is_lord_on_map(lord) && !lord_has_unrouted_units(lord))
+ return
+ end_battle_losses_remove()
+}
+
+function end_battle_losses_remove() {
+ game.who = NOBODY
+ if (game.active === game.battle.loser)
+ goto_battle_service()
+ else
+ goto_battle_spoils()
+}
+
+states.battle_losses_remove = {
+ inactive: "Remove Lords",
+ prompt() {
+ view.prompt = "Losses: Remove Lords who lost all their Forces."
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (is_lord_on_map(lord) && !lord_has_unrouted_units(lord))
+ gen_action_lord(lord)
+ },
+ lord(lord) {
+ set_delete(game.battle.retreated, lord)
+ if (game.active === game.battle.loser)
+ transfer_assets_except_ships(lord)
+ if (can_ransom_lord_battle(lord)) {
+ goto_ransom(lord)
+ } else {
+ disband_lord(lord, true)
+ goto_battle_losses_remove()
+ }
+ },
+}
+
+function end_ransom_battle_losses_remove() {
+ goto_battle_losses_remove()
+}
+
+// === ENDING THE BATTLE: SPOILS (VICTOR) ===
+
+function log_spoils() {
+ if (game.spoils[PROV] > 0)
+ logi(game.spoils[PROV] + " Provender")
+ if (game.spoils[COIN] > 0)
+ logi(game.spoils[COIN] + " Coin")
+ if (game.spoils[CART] > 0)
+ logi(game.spoils[CART] + " Cart")
+ if (game.spoils[SHIP] > 0)
+ logi(game.spoils[SHIP] + " Ship")
+}
+
+function find_lone_friendly_lord_at(loc) {
+ let who = NOBODY
+ let n = 0
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (get_lord_locale(lord) === loc) {
+ who = lord
+ ++n
+ }
+ }
+ if (n === 1)
+ return who
+ return NOBODY
+}
+
+function goto_battle_spoils() {
+ if (has_any_spoils() && has_friendly_lord(game.battle.where)) {
+ log_h4("Spoils")
+ log_spoils()
+ game.state = "battle_spoils"
+ game.who = find_lone_friendly_lord_at(game.battle.where)
+ } else {
+ end_battle_spoils()
+ }
+}
+
+function end_battle_spoils() {
+ game.who = NOBODY
+ game.spoils = 0
+ goto_battle_aftermath()
+}
+
+states.battle_spoils = {
+ inactive: "Spoils",
+ prompt() {
+ if (has_any_spoils()) {
+ view.prompt = "Spoils: Divide " + list_spoils() + "."
+ let here = game.battle.where
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === here)
+ prompt_select_lord(lord)
+ if (game.who !== NOBODY)
+ prompt_spoils()
+ } else {
+ view.prompt = "Spoils: All done."
+ view.actions.end_spoils = 1
+ }
+ },
+ lord: action_select_lord,
+ take_prov: take_spoils_prov,
+ take_coin: take_spoils_coin,
+ take_cart: take_spoils_cart,
+ end_spoils() {
+ clear_undo()
+ end_battle_spoils()
+ },
+}
+
+// === ENDING THE BATTLE: SERVICE (LOSER) ===
+
+function goto_battle_service() {
+ if (game.battle.retreated) {
+ log_h4("Service")
+ resume_battle_service()
+ } else {
+ end_battle_service()
+ }
+}
+
+function resume_battle_service() {
+ if (game.battle.retreated.length > 0)
+ game.state = "battle_service"
+ else
+ end_battle_service()
+}
+
+states.battle_service = {
+ inactive: "Service",
+ prompt() {
+ view.prompt = "Battle: Roll to shift Service of each Retreated Lord."
+ for (let lord of game.battle.retreated)
+ gen_action_service_bad(lord)
+ },
+ service_bad(lord) {
+ let die = roll_die()
+ if (die <= 2)
+ add_lord_service(lord, -1)
+ else if (die <= 4)
+ add_lord_service(lord, -2)
+ else if (die <= 6)
+ add_lord_service(lord, -3)
+ log(`L${lord} ${HIT[die]}, shifted to ${get_lord_service(lord)}.`)
+ set_delete(game.battle.retreated, lord)
+ resume_battle_service()
+ },
+}
+
+function end_battle_service() {
+ goto_battle_losses_victor()
+}
+
+// === ENDING THE BATTLE: AFTERMATH ===
+
+function goto_battle_aftermath() {
+ set_active(game.battle.attacker)
+
+ // Events
+ discard_events("hold")
+
+ // Recovery
+ spend_all_actions()
+
+ if (check_campaign_victory())
+ return
+
+ // Siege/Conquest
+ if (game.march) {
+ game.battle = 0
+ march_with_group_3()
+ } else {
+ game.battle = 0
+ resume_command()
+ }
+}
+
+// === CAMPAIGN: FEED ===
+
+function can_feed_from_shared(lord) {
+ let loc = get_lord_locale(lord)
+ return get_shared_assets(loc, PROV) > 0
+}
+
+function has_friendly_lord_who_must_feed() {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (is_lord_unfed(lord))
+ return true
+ return false
+}
+
+function can_lord_use_hillforts(lord) {
+ return is_lord_unfed(lord) && is_lord_unbesieged(lord) && is_in_livonia(get_lord_locale(lord))
+}
+
+function can_use_hillforts() {
+ if (game.active === TEUTONS) {
+ if (has_global_capability(AOW_TEUTONIC_HILLFORTS)) {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (can_lord_use_hillforts(lord))
+ return true
+ }
+ }
+ return false
+}
+
+function goto_feed() {
+ log_br()
+
+ // Count how much food each lord needs
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (get_lord_moved(lord)) {
+ if (count_lord_all_forces(lord) >= 7)
+ set_lord_unfed(lord, 2)
+ else
+ set_lord_unfed(lord, 1)
+ } else {
+ set_lord_unfed(lord, 0)
+ }
+ }
+
+ if (has_friendly_lord_who_must_feed()) {
+ if (can_use_hillforts())
+ game.state = "hillforts"
+ else
+ game.state = "feed"
+ } else {
+ end_feed()
+ }
+}
+
+states.hillforts = {
+ inactive: "Hillforts",
+ prompt() {
+ view.prompt = "Hillforts: Skip Feed of one Unbesieged Lord in Livonia."
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (can_lord_use_hillforts(lord))
+ gen_action_lord(lord)
+ },
+ lord(lord) {
+ push_undo()
+ log(`C${AOW_TEUTONIC_HILLFORTS} skipped L${lord}.`)
+ feed_lord_skip(lord)
+ if (has_friendly_lord_who_must_feed())
+ game.state = "feed"
+ else
+ end_feed()
+ },
+}
+
+states.feed = {
+ inactive: "Feed",
+ prompt() {
+ view.prompt = "Feed: You must Feed Lords who Moved or Fought."
+
+ let done = true
+
+ prompt_held_event()
+
+ // Feed from own mat
+ if (done) {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (is_lord_unfed(lord)) {
+ if (get_lord_assets(lord, PROV) > 0) {
+ gen_action_prov(lord)
+ done = false
+ }
+ }
+ }
+ }
+
+ // Sharing
+ if (done) {
+ view.prompt = "Feed: You must Feed Lords with Shared Loot or Provender."
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (is_lord_unfed(lord) && can_feed_from_shared(lord)) {
+ gen_action_lord(lord)
+ done = false
+ }
+ }
+ }
+
+ // Unfed
+ if (done) {
+ view.prompt = "Feed: You must shift the Service of any Unfed Lords."
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (is_lord_unfed(lord)) {
+ gen_action_service_bad(lord)
+ done = false
+ }
+ }
+ }
+
+ // All done!
+ if (done) {
+ view.prompt = "Feed: All done."
+ view.actions.end_feed = 1
+ }
+ },
+ prov(lord) {
+ push_undo()
+ add_lord_assets(lord, PROV, -1)
+ feed_lord(lord)
+ },
+ lord(lord) {
+ push_undo()
+ game.who = lord
+ game.state = "feed_lord_shared"
+ },
+ service_bad(lord) {
+ push_undo()
+ add_lord_service(lord, -1)
+ log(`Unfed L${lord} to ${get_lord_service(lord)}.`)
+ set_lord_unfed(lord, 0)
+ },
+ end_feed() {
+ push_undo()
+ end_feed()
+ },
+ card: action_held_event,
+}
+
+function resume_feed_lord_shared() {
+ if (!is_lord_unfed(game.who) || !can_feed_from_shared(game.who)) {
+ game.who = NOBODY
+ game.state = "feed"
+ }
+}
+
+states.feed_lord_shared = {
+ inactive: "Feed",
+ prompt() {
+ view.prompt = `Feed: You must Feed ${lord_name[game.who]} with Shared Loot or Provender.`
+ let loc = get_lord_locale(game.who)
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (get_lord_locale(lord) === loc) {
+ if (get_lord_assets(lord, PROV) > 0)
+ gen_action_prov(lord)
+ }
+ }
+ },
+ prov(lord) {
+ push_undo()
+ add_lord_assets(lord, PROV, -1)
+ feed_lord(game.who)
+ resume_feed_lord_shared()
+ },
+}
+
+function end_feed() {
+ goto_pay()
+}
+
+// === LEVY & CAMPAIGN: PAY ===
+
+function can_pay_lord(lord) {
+ if (get_lord_service(lord) > 16)
+ return false
+ if (game.active === RUSSIANS) {
+ if (game.pieces.veche_coin > 0 && is_lord_unbesieged(lord))
+ return true
+ }
+ let loc = get_lord_locale(lord)
+ if (get_shared_assets(loc, COIN) > 0)
+ return true
+ return false
+}
+
+function has_friendly_lord_who_may_be_paid() {
+ if (game.active === TEUTONS) {
+ // Open a window to play Heinrich sees the Curia.
+ if (could_play_card(EVENT_TEUTONIC_HEINRICH_SEES_THE_CURIA) && can_play_heinrich_sees_the_curia())
+ return true
+ }
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (is_lord_on_map(lord) && can_pay_lord(lord))
+ return true
+ return false
+}
+
+function goto_pay() {
+ log_br()
+ game.state = "pay"
+ game.who = NOBODY
+ if (!has_friendly_lord_who_may_be_paid())
+ end_pay()
+}
+
+function resume_pay() {
+ if (!can_pay_lord(game.who))
+ game.who = NOBODY
+}
+
+states.pay = {
+ inactive: "Pay",
+ prompt() {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (is_lord_on_map(lord) && can_pay_lord(lord)) {
+ prompt_select_lord(lord)
+ prompt_select_service(lord)
+ }
+ }
+
+ prompt_held_event()
+
+ if (game.who === NOBODY) {
+ view.prompt = "Pay: You may Pay your Lords."
+ } else {
+
+ let here = get_lord_locale(game.who)
+
+ view.prompt = `Pay: You may Pay ${lord_name[game.who]} with Coin.`
+
+ if (game.active === RUSSIANS) {
+ if (game.pieces.veche_coin > 0 && is_lord_unbesieged(game.who))
+ view.actions.veche_coin = 1
+ }
+
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (get_lord_locale(lord) === here) {
+ if (get_lord_assets(lord, COIN) > 0)
+ gen_action_coin(lord)
+ }
+ }
+ }
+
+ view.actions.end_pay = 1
+ },
+ lord: action_select_lord,
+ service: action_select_lord,
+ coin(lord) {
+ push_undo_without_who()
+ log(`Paid L${game.who}.`)
+ add_lord_assets(lord, COIN, -1)
+ add_lord_service(game.who, 1)
+ resume_pay()
+ },
+ veche_coin() {
+ push_undo_without_who()
+ log(`Paid L${game.who} from Veche.`)
+ game.pieces.veche_coin--
+ add_lord_service(game.who, 1)
+ resume_pay()
+ },
+ end_pay() {
+ push_undo_without_who()
+ end_pay()
+ },
+ card: action_held_event,
+}
+
+function end_pay() {
+ // NOTE: We can combine Pay & Disband steps because disband is mandatory only.
+ game.who = NOBODY
+ goto_disband()
+}
+
+// === LEVY & CAMPAIGN: DISBAND ===
+
+function has_friendly_lord_who_must_disband() {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (is_lord_on_map(lord) && get_lord_service(lord) <= current_turn())
+ return true
+ return false
+}
+
+function goto_disband() {
+ game.state = "disband"
+ if (!has_friendly_lord_who_must_disband())
+ end_disband()
+}
+
+function disband_lord(lord, permanently = false) {
+ let here = get_lord_locale(lord)
+ let turn = current_turn()
+
+ if (permanently) {
+ log(`Removed L${lord}.`)
+ set_lord_locale(lord, NOWHERE)
+ set_lord_service(lord, NEVER)
+ } else if (get_lord_service(lord) < current_turn()) {
+ log(`Disbanded L${lord} beyond Service limit.`)
+ set_lord_locale(lord, NOWHERE)
+ set_lord_service(lord, NEVER)
+ } else {
+ if (is_levy_phase())
+ set_lord_cylinder_on_calendar(lord, turn + data.lords[lord].service)
+ else
+ set_lord_cylinder_on_calendar(lord, turn + data.lords[lord].service + 1)
+ set_lord_service(lord, NEVER)
+ log(`Disbanded L${lord} to ${get_lord_calendar(lord)}.`)
+ }
+
+ if (game.scenario === "Pleskau" || game.scenario === "Pleskau (Quickstart)") {
+ if (is_russian_lord(lord))
+ game.pieces.elr1 ++
+ else
+ game.pieces.elr2 ++
+ }
+
+ remove_lieutenant(lord)
+
+ // Smerdi - serfs go back to card
+ game.pieces.smerdi += get_lord_forces(lord, SERFS)
+
+ discard_lord_capability_n(lord, 0)
+ discard_lord_capability_n(lord, 1)
+ game.pieces.assets[lord] = 0
+ game.pieces.forces[lord] = 0
+ game.pieces.routed[lord] = 0
+
+ set_lord_besieged(lord, 0)
+ set_lord_moved(lord, 0)
+
+ for (let v of data.lords[lord].vassals)
+ game.pieces.vassals[v] = VASSAL_UNAVAILABLE
+}
+
+states.disband = {
+ inactive: "Disband",
+ prompt() {
+ view.prompt = "Disband: You must Disband Lords at their Service limit."
+
+ prompt_held_event()
+
+ let done = true
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (is_lord_on_map(lord) && get_lord_service(lord) <= current_turn()) {
+ gen_action_lord(lord)
+ gen_action_service_bad(lord)
+ done = false
+ }
+ }
+ if (done)
+ view.actions.end_disband = 1
+ },
+ service_bad(lord) {
+ this.lord(lord)
+ },
+ lord(lord) {
+ if (is_lord_besieged(lord) && can_ransom_lord_siege(lord)) {
+ clear_undo()
+ goto_ransom(lord)
+ } else {
+ push_undo()
+ disband_lord(lord)
+ }
+ },
+ end_disband() {
+ end_disband()
+ },
+ card: action_held_event,
+}
+
+function end_ransom_disband() {
+ // do nothing
+}
+
+function end_disband() {
+ clear_undo()
+
+ if (is_campaign_phase()) {
+ if (check_campaign_victory())
+ return
+ }
+
+ set_active_enemy()
+ if (is_campaign_phase()) {
+ if (is_active_command())
+ goto_remove_markers()
+ else
+ goto_feed()
+ } else {
+ if (game.active === P1)
+ goto_levy_muster()
+ else
+ goto_feed()
+ }
+}
+
+// === LEVY & CAMPAIGN: RANSOM ===
+
+function enemy_has_ransom() {
+ if (game.active === TEUTONS)
+ return has_global_capability(AOW_RUSSIAN_RANSOM)
+ else
+ return has_global_capability(AOW_TEUTONIC_RANSOM)
+}
+
+function can_ransom_lord_siege(lord) {
+ return enemy_has_ransom() && has_enemy_lord(get_lord_locale(lord))
+}
+
+function has_enemy_lord_in_battle() {
+ for (let lord = first_enemy_lord; lord <= last_enemy_lord; ++lord)
+ if (get_lord_moved(lord))
+ return true
+ return false
+}
+
+function can_ransom_lord_battle() {
+ return enemy_has_ransom() && has_enemy_lord_in_battle()
+}
+
+function goto_ransom(lord) {
+ clear_undo()
+ set_active_enemy()
+ push_state("ransom")
+ game.who = lord
+ game.count = data.lords[lord].service
+ if (is_teutonic_lord(lord))
+ log(`L${lord} C${AOW_RUSSIAN_RANSOM}.`)
+ else
+ log(`L${lord} C${AOW_TEUTONIC_RANSOM}.`)
+}
+
+function end_ransom() {
+ let here = get_lord_locale(game.who)
+ if (game.battle)
+ disband_lord(game.who, true)
+ else
+ disband_lord(game.who, false)
+ pop_state()
+
+ set_active_enemy()
+ switch (game.state) {
+ case "disband": return end_ransom_disband()
+ case "sack": return end_ransom_sack()
+ case "battle_remove": return end_ransom_battle_remove()
+ case "battle_losses_remove": return end_ransom_battle_losses_remove()
+ }
+}
+
+states.ransom = {
+ inactive: "Ransom",
+ prompt() {
+ if (game.active === TEUTONS)
+ view.prompt = `Ransom ${lord_name[game.who]}: Add ${game.count} Coin to a Teutonic Lord.`
+ else
+ view.prompt = `Ransom ${lord_name[game.who]}: Add ${game.count} Coin to a Russian Lord.`
+ if (game.battle) {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_fought(lord))
+ gen_action_lord(lord)
+ } else {
+ let here = get_lord_locale(game.who)
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (get_lord_locale(lord) === here)
+ gen_action_lord(lord)
+ }
+ },
+ lord(lord) {
+ add_lord_assets(lord, COIN, game.count)
+ end_ransom()
+ },
+}
+
+// === CAMPAIGN: REMOVE MARKERS ===
+
+function goto_remove_markers() {
+ clear_lords_moved()
+ goto_command_activation()
+}
+
+// === END CAMPAIGN: GROWTH ===
+
+function count_enemy_ravaged() {
+ let n = 0
+ for (let loc of game.pieces.ravaged)
+ if (is_friendly_territory(loc))
+ ++n
+ return n
+}
+
+function goto_grow() {
+ game.count = count_enemy_ravaged() >> 1
+ log_br()
+ if (game.active === TEUTONS)
+ log("Teutonic Growth")
+ else
+ log("Russian Growth")
+ if (game.count === 0) {
+ logi("Nothing")
+ end_growth()
+ } else {
+ game.state = "growth"
+ }
+}
+
+function end_growth() {
+ set_active_enemy()
+ if (game.active === P2)
+ goto_grow()
+ else
+ goto_game_end()
+}
+
+states.growth = {
+ inactive: "Grow",
+ prompt() {
+ view.prompt = `Grow: Remove ${game.count} enemy Ravaged markers.`
+ if (game.count > 0) {
+ for (let loc of game.pieces.ravaged)
+ if (is_friendly_territory(loc))
+ gen_action_locale(loc)
+ } else {
+ view.actions.end_growth = 1
+ }
+ },
+ locale(loc) {
+ push_undo()
+ logi(`%${loc}`)
+ remove_ravaged_marker(loc)
+ game.count--
+ },
+ end_growth() {
+ clear_undo()
+ end_growth()
+ },
+}
+
+// === END CAMPAIGN: GAME END ===
+
+function check_campaign_victory_p1() {
+ for (let lord = first_p2_lord; lord <= last_p2_lord; ++lord)
+ if (is_lord_on_map(lord))
+ return false
+ return true
+}
+
+function check_campaign_victory_p2() {
+ for (let lord = first_p1_lord; lord <= last_p1_lord; ++lord)
+ if (is_lord_on_map(lord))
+ return false
+ return true
+}
+
+function check_campaign_victory() {
+ if (check_campaign_victory_p1()) {
+ goto_game_over(P1, `${P1} won a Campaign Victory!`)
+ return true
+ }
+ if (check_campaign_victory_p2()) {
+ goto_game_over(P2, `${P2} won a Campaign Victory!`)
+ return true
+ }
+ return false
+}
+
+function goto_end_campaign() {
+ log_h1("End Campaign")
+
+ set_active(P1)
+
+ if (current_turn() === 8 || current_turn() === 16) {
+ goto_grow()
+ } else {
+ goto_game_end()
+ }
+}
+
+function count_vp1() {
+ let vp = game.pieces.elr1 << 1
+ vp += game.pieces.castles1.length << 1
+ for (let loc of game.pieces.conquered)
+ if (is_p2_locale(loc))
+ vp += data.locales[loc].vp << 1
+ for (let loc of game.pieces.ravaged)
+ if (is_p2_locale(loc))
+ vp += 1
+ return vp
+}
+
+function count_vp2() {
+ let vp = game.pieces.elr2 << 1
+ vp += game.pieces.veche_vp << 1
+ vp += game.pieces.castles2.length << 1
+ for (let loc of game.pieces.conquered)
+ if (is_p1_locale(loc))
+ vp += data.locales[loc].vp << 1
+ for (let loc of game.pieces.ravaged)
+ if (is_p1_locale(loc))
+ vp += 1
+ return vp
+}
+
+function goto_game_end() {
+ // GAME END
+ if (current_turn() === scenario_last_turn[game.scenario]) {
+ let vp1 = count_vp1()
+ let vp2 = count_vp2()
+
+ if (game.scenario === "Watland") {
+ if (vp1 < 14)
+ goto_game_over(P2, `Russians won \u2014 Teutons had less than 7 VP.`)
+ else if (vp1 < vp2 * 2)
+ goto_game_over(P2, `Russians won \u2014 Teutons had less than double Russian VP.`)
+ else
+ goto_game_over(P1, `Teutons won with ${frac(vp1)} VP vs ${frac(vp2)} VP.`)
+ return
+ }
+
+ if (vp1 > vp2)
+ goto_game_over(P1, `${P1} won with ${frac(vp1)} VP vs ${frac(vp2)} VP.`)
+ else if (vp2 > vp1)
+ goto_game_over(P2, `${P2} won with ${frac(vp2)} VP vs ${frac(vp1)} VP.`)
+ else
+ goto_game_over("Draw", "The game ended in a draw.")
+ } else {
+ goto_plow_and_reap()
+ }
+}
+
+// === END CAMPAIGN: PLOW AND REAP ===
+
+function goto_plow_and_reap() {
+ let turn = current_turn()
+ end_plow_and_reap()
+}
+
+function end_plow_and_reap() {
+ goto_wastage()
+}
+
+// === END CAMPAIGN: WASTAGE ===
+
+function goto_wastage() {
+ clear_lords_moved()
+
+ let done = true
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (check_lord_wastage(lord)) {
+ set_lord_moved(lord, 3)
+ done = false
+ }
+ }
+
+ if (done)
+ end_wastage()
+ else
+ game.state = "wastage"
+}
+
+function check_lord_wastage(lord) {
+ if (get_lord_assets(lord, PROV) > 1)
+ return true
+ if (get_lord_assets(lord, COIN) > 1)
+ return true
+ if (get_lord_assets(lord, CART) > 1)
+ return true
+ if (get_lord_assets(lord, SHIP) > 1)
+ return true
+ if (get_lord_capability(lord, 0) !== NOTHING && get_lord_capability(lord, 1) !== NOTHING)
+ return true
+ return false
+}
+
+function prompt_wastage(lord) {
+ if (get_lord_assets(lord, PROV) > 0)
+ gen_action_prov(lord)
+ if (get_lord_assets(lord, COIN) > 0)
+ gen_action_coin(lord)
+ if (get_lord_assets(lord, CART) > 0)
+ gen_action_cart(lord)
+ if (get_lord_assets(lord, SHIP) > 0)
+ gen_action_ship(lord)
+ for (let i = 0; i < 2; ++i) {
+ let c = get_lord_capability(lord, i)
+ if (c !== NOTHING)
+ gen_action_card(c)
+ }
+}
+
+function action_wastage(lord, type) {
+ push_undo()
+ set_lord_moved(lord, 0)
+ add_lord_assets(lord, type, -1)
+}
+
+function find_lord_with_capability_card(c) {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ if (lord_has_capability_card(lord, c))
+ return lord
+ return NOBODY
+}
+
+states.wastage = {
+ inactive: "Wastage",
+ prompt() {
+ let done = true
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (get_lord_moved(lord)) {
+ prompt_wastage(lord)
+ done = false
+ }
+ }
+ if (done) {
+ view.prompt = "Wastage: All done."
+ view.actions.end_wastage = 1
+ } else {
+ view.prompt = "Wastage: Discard one Asset or Capability from each affected Lord."
+ }
+ },
+ card(c) {
+ push_undo()
+ let lord = find_lord_with_capability_card(c)
+ set_lord_moved(lord, 0)
+ discard_lord_capability(lord, c)
+ },
+ prov(lord) { action_wastage(lord, PROV) },
+ coin(lord) { action_wastage(lord, COIN) },
+ cart(lord) { action_wastage(lord, CART) },
+ ship(lord) { action_wastage(lord, SHIP) },
+ end_wastage() {
+ end_wastage()
+ },
+}
+
+function end_wastage() {
+ push_undo()
+ goto_reset()
+}
+
+// === END CAMPAIGN: RESET (DISCARD ARTS OF WAR) ===
+
+function goto_reset() {
+ game.state = "reset"
+
+ // Unstack Lieutenants and Lower Lords
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ remove_lieutenant(lord)
+
+ // Remove all Serfs to the Smerdi card
+ if (game.active === RUSSIANS) {
+ for (let lord = first_p2_lord; lord <= last_p2_lord; ++lord)
+ set_lord_forces(lord, SERFS, 0)
+ if (has_global_capability(AOW_RUSSIAN_SMERDI))
+ game.pieces.smerdi = 6
+ else
+ game.pieces.smerdi = 0
+ }
+
+ // Discard "This Campaign" events from play.
+ discard_friendly_events("this_campaign")
+}
+
+states.reset = {
+ inactive: "Reset",
+ prompt() {
+ view.prompt = "Reset: You may discard any Arts of War cards desired."
+ if (game.active === P1) {
+ for (let c = first_p1_card; c <= last_p1_card; ++c)
+ if (can_discard_card(c))
+ gen_action_card(c)
+ }
+ if (game.active === P2) {
+ for (let c = first_p2_card; c <= last_p2_card; ++c)
+ if (can_discard_card(c))
+ gen_action_card(c)
+ }
+ view.actions.end_discard = 1
+ },
+ card(c) {
+ push_undo()
+ if (has_global_capability(c)) {
+ log(`Discarded C${c}.`)
+ discard_global_capability(c)
+ } else if (set_has(game.hand1, c)) {
+ log("Discarded Held card.")
+ set_delete(game.hand1, c)
+ } else if (set_has(game.hand2, c)) {
+ log("Discarded Held card.")
+ set_delete(game.hand2, c)
+ } else {
+ let lord = find_lord_with_capability_card(c)
+ if (lord !== NOBODY) {
+ discard_lord_capability(lord, c)
+ }
+ }
+ },
+ end_discard() {
+ end_reset()
+ },
+}
+
+function end_reset() {
+ clear_undo()
+ set_active_enemy()
+ if (game.active === P2)
+ goto_plow_and_reap()
+ else
+ goto_advance_campaign()
+}
+
+// === END CAMPAIGN: RESET (ADVANCE CAMPAIGN) ===
+
+function goto_advance_campaign() {
+ game.turn++
+
+ log_h1("Levy " + current_turn_name())
+
+ // First turns of late winter
+ if (current_turn() === 5 || current_turn() === 13)
+ goto_discard_crusade_late_winter()
+ else
+ goto_levy_arts_of_war()
+}
+
+// === GAME OVER ===
+
+function goto_game_over(result, victory) {
+ game.state = "game_over"
+ game.active = "None"
+ game.result = result
+ game.victory = victory
+ log_h1("Game Over")
+ log(game.victory)
+ return true
+}
+
+states.game_over = {
+ get inactive() {
+ return game.victory
+ },
+ prompt() {
+ view.prompt = game.victory
+ },
+}
+
+exports.resign = function (state, current) {
+ load_state(state)
+ if (game.state !== "game_over") {
+ for (let opponent of exports.roles) {
+ if (opponent !== current) {
+ goto_game_over(opponent, current + " resigned.")
+ break
+ }
+ }
+ }
+ return game
+}
+
+// === UNCOMMON TEMPLATE ===
+
+function log_br() {
+ if (game.log.length > 0 && game.log[game.log.length - 1] !== "")
+ game.log.push("")
+}
+
+function log(msg) {
+ game.log.push(msg)
+}
+
+function logevent(cap) {
+ game.log.push(`E${cap}.`)
+}
+
+function logcap(cap) {
+ game.log.push(`C${cap}.`)
+}
+
+function logi(msg) {
+ game.log.push(">" + msg)
+}
+
+function logii(msg) {
+ game.log.push(">>" + msg)
+}
+
+function log_h1(msg) {
+ log_br()
+ log(".h1 " + msg)
+ log_br()
+}
+
+function log_h2(msg) {
+ log_br()
+ if (game.active === TEUTONS)
+ log(".h2t " + msg)
+ else
+ log(".h2r " + msg)
+ log_br()
+}
+
+function log_h3(msg) {
+ log_br()
+ if (game.active === TEUTONS)
+ log(".h3t " + msg)
+ else
+ log(".h3r " + msg)
+ log_br()
+}
+
+function log_h4(msg) {
+ log_br()
+ log(".h4 " + msg)
+}
+
+function log_h5(msg) {
+ log_br()
+ log(".h5 " + msg)
+}
+
+function gen_action(action, argument) {
+ if (!(action in view.actions))
+ view.actions[action] = []
+ set_add(view.actions[action], argument)
+}
+
+function gen_action_card_if_held(c) {
+ if (has_card_in_hand(c))
+ gen_action_card(c)
+}
+
+function prompt_select_lord_on_calendar(lord) {
+ if (lord !== game.who) {
+ if (is_lord_on_calendar(lord))
+ gen_action_lord(lord)
+ else
+ gen_action_service(lord)
+ }
+}
+
+function prompt_select_lord(lord) {
+ if (lord !== game.who)
+ gen_action_lord(lord)
+}
+
+function prompt_select_service(lord) {
+ if (lord !== game.who)
+ gen_action_service(lord)
+}
+
+function action_select_lord(lord) {
+ if (game.who === lord)
+ game.who = NOBODY
+ else
+ game.who = lord
+}
+
+function gen_action_calendar(calendar) {
+ if (calendar < 0)
+ calendar = 0
+ if (calendar > 17)
+ calendar = 17
+ gen_action("calendar", calendar)
+}
+
+function gen_action_way(way) {
+ gen_action("way", way)
+}
+
+function gen_action_locale(locale) {
+ gen_action("locale", locale)
+}
+
+function gen_action_laden_march(locale) {
+ gen_action("laden_march", locale)
+}
+
+function gen_action_lord(lord) {
+ gen_action("lord", lord)
+}
+
+function gen_action_array(pos) {
+ gen_action("array", pos)
+}
+
+function gen_action_service(service) {
+ gen_action("service", service)
+}
+
+function gen_action_service_bad(service) {
+ gen_action("service_bad", service)
+}
+
+function gen_action_vassal(vassal) {
+ gen_action("vassal", vassal)
+}
+
+function gen_action_card(c) {
+ gen_action("card", c)
+}
+
+function gen_action_plan(lord) {
+ gen_action("plan", lord)
+}
+
+function gen_action_prov(lord) {
+ gen_action("prov", lord)
+}
+
+function gen_action_coin(lord) {
+ gen_action("coin", lord)
+}
+
+function gen_action_cart(lord) {
+ gen_action("cart", lord)
+}
+
+function gen_action_ship(lord) {
+ gen_action("ship", lord)
+}
+
+function gen_action_knights(lord) {
+ gen_action("knights", lord)
+}
+
+function gen_action_sergeants(lord) {
+ gen_action("sergeants", lord)
+}
+
+function gen_action_light_horse(lord) {
+ gen_action("light_horse", lord)
+}
+
+function gen_action_asiatic_horse(lord) {
+ gen_action("asiatic_horse", lord)
+}
+
+function gen_action_men_at_arms(lord) {
+ gen_action("men_at_arms", lord)
+}
+
+function gen_action_militia(lord) {
+ gen_action("militia", lord)
+}
+
+function gen_action_serfs(lord) {
+ gen_action("serfs", lord)
+}
+
+function gen_action_routed_knights(lord) {
+ gen_action("routed_knights", lord)
+}
+
+function gen_action_routed_sergeants(lord) {
+ gen_action("routed_sergeants", lord)
+}
+
+function gen_action_routed_light_horse(lord) {
+ gen_action("routed_light_horse", lord)
+}
+
+function gen_action_routed_asiatic_horse(lord) {
+ gen_action("routed_asiatic_horse", lord)
+}
+
+function gen_action_routed_men_at_arms(lord) {
+ gen_action("routed_men_at_arms", lord)
+}
+
+function gen_action_routed_militia(lord) {
+ gen_action("routed_militia", lord)
+}
+
+function gen_action_routed_serfs(lord) {
+ gen_action("routed_serfs", lord)
+}
+
+const P1_LORD_MASK = (1|2|4|8|16|32)
+const P2_LORD_MASK = (1|2|4|8|16|32) << 6
+
+exports.view = function (state, current) {
+ load_state(state)
+
+ view = {
+ prompt: null,
+ actions: null,
+ log: game.log,
+ reveal: 0,
+
+ scenario: (scenario_first_turn[game.scenario] << 5) + (scenario_last_turn[game.scenario]),
+ turn: game.turn,
+ events: game.events,
+ capabilities: game.capabilities,
+ pieces: game.pieces,
+ battle: game.battle,
+
+ held1: game.hand1.length,
+ held2: game.hand2.length,
+
+ command: game.command,
+ hand: null,
+ plan: null,
+ }
+
+ if (!game.hidden)
+ view.reveal = -1
+
+ if (current === P1) {
+ view.hand = game.hand1
+ view.plan = game.plan1
+ if (game.hidden)
+ view.reveal |= P1_LORD_MASK
+ }
+ if (current === P2) {
+ view.hand = game.hand2
+ view.plan = game.plan2
+ if (game.hidden)
+ view.reveal |= P2_LORD_MASK
+ }
+
+ if (game.battle) {
+ if (game.battle.array) {
+ for (let lord of game.battle.array)
+ if (lord !== NOBODY)
+ view.reveal |= (1 << lord)
+ }
+ for (let lord of game.battle.reserves)
+ view.reveal |= (1 << lord)
+ }
+
+ if (game.state === "game_over") {
+ view.prompt = game.victory
+ } else if (current === "Observer" || (game.active !== current && game.active !== BOTH)) {
+ let inactive = states[game.state].inactive || game.state
+ view.prompt = `Waiting for ${game.active} \u2014 ${inactive}.`
+ } else {
+ view.actions = {}
+ view.who = game.who
+ if (states[game.state])
+ states[game.state].prompt(current)
+ else
+ view.prompt = "Unknown state: " + game.state
+ if (view.actions.undo === undefined) {
+ if (game.undo && game.undo.length > 0)
+ view.actions.undo = 1
+ else
+ view.actions.undo = 0
+ }
+ }
+ return view
+}
+
+exports.action = function (state, current, action, arg) {
+ load_state(state)
+ // Object.seal(game) // XXX: don't allow adding properties
+ let S = states[game.state]
+ if (S && action in S) {
+ S[action](arg, current)
+ } else {
+ if (action === "undo" && game.undo && game.undo.length > 0)
+ pop_undo()
+ else
+ throw new Error("Invalid action: " + action)
+ }
+ return game
+}
+
+exports.is_checkpoint = function (a, b) {
+ return a.turn !== b.turn
+}
+
+// === COMMON TEMPLATE ===
+
+// Packed array of small numbers in one word
+
+function pack1_get(word, n) {
+ return (word >>> n) & 1
+}
+
+function pack2_get(word, n) {
+ n = n << 1
+ return (word >>> n) & 3
+}
+
+function pack4_get(word, n) {
+ n = n << 2
+ return (word >>> n) & 15
+}
+
+function pack1_set(word, n, x) {
+ return (word & ~(1 << n)) | (x << n)
+}
+
+function pack2_set(word, n, x) {
+ n = n << 1
+ return (word & ~(3 << n)) | (x << n)
+}
+
+function pack4_set(word, n, x) {
+ n = n << 2
+ return (word & ~(15 << n)) | (x << n)
+}
+
+// === COMMON LIBRARY ===
+
+function clear_undo() {
+ if (game.undo.length > 0)
+ game.undo = []
+}
+
+function push_undo_without_who() {
+ let save_who = game.who
+ game.who = NOBODY
+ push_undo()
+ game.who = save_who
+}
+
+function push_undo() {
+ let copy = {}
+ for (let k in game) {
+ let v = game[k]
+ if (k === "undo")
+ continue
+ else if (k === "log")
+ v = v.length
+ else if (typeof v === "object" && v !== null)
+ v = object_copy(v)
+ copy[k] = v
+ }
+ game.undo.push(copy)
+}
+
+function pop_undo() {
+ let save_log = game.log
+ let save_undo = game.undo
+ game = save_undo.pop()
+ save_log.length = game.log
+ game.log = save_log
+ game.undo = save_undo
+}
+
+function random(range) {
+ // An MLCG using integer arithmetic with doubles.
+ // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf
+ // m = 2**35 − 31
+ return (game.seed = (game.seed * 200105) % 34359738337) % range
+}
+
+// Fast deep copy for objects without cycles
+function object_copy(original) {
+ if (Array.isArray(original)) {
+ let n = original.length
+ let copy = new Array(n)
+ for (let i = 0; i < n; ++i) {
+ let v = original[i]
+ if (typeof v === "object" && v !== null)
+ copy[i] = object_copy(v)
+ else
+ copy[i] = v
+ }
+ return copy
+ } else {
+ let copy = {}
+ for (let i in original) {
+ let v = original[i]
+ if (typeof v === "object" && v !== null)
+ copy[i] = object_copy(v)
+ else
+ copy[i] = v
+ }
+ return copy
+ }
+}
+
+// Array remove and insert (faster than splice)
+
+function array_remove_item(array, item) {
+ let n = array.length
+ for (let i = 0; i < n; ++i)
+ if (array[i] === item)
+ return array_remove(array, i)
+}
+
+function array_remove(array, index) {
+ let n = array.length
+ for (let i = index + 1; i < n; ++i)
+ array[i - 1] = array[i]
+ array.length = n - 1
+}
+
+function array_insert(array, index, item) {
+ for (let i = array.length; i > index; --i)
+ array[i] = array[i - 1]
+ array[index] = item
+}
+
+function array_remove_pair(array, index) {
+ let n = array.length
+ for (let i = index + 2; i < n; ++i)
+ array[i - 2] = array[i]
+ array.length = n - 2
+}
+
+function array_insert_pair(array, index, key, value) {
+ for (let i = array.length; i > index; i -= 2) {
+ array[i] = array[i - 2]
+ array[i + 1] = array[i - 1]
+ }
+ array[index] = key
+ array[index + 1] = value
+}
+
+// Set as plain sorted array
+
+function set_has(set, item) {
+ let a = 0
+ let b = set.length - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = set[m]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else
+ return true
+ }
+ return false
+}
+
+function set_add(set, item) {
+ let a = 0
+ let b = set.length - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = set[m]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else
+ return
+ }
+ array_insert(set, a, item)
+}
+
+function set_delete(set, item) {
+ let a = 0
+ let b = set.length - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = set[m]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else {
+ array_remove(set, m)
+ return
+ }
+ }
+}
+
+function set_toggle(set, item) {
+ let a = 0
+ let b = set.length - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = set[m]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else {
+ array_remove(set, m)
+ return
+ }
+ }
+ array_insert(set, a, item)
+}
+
+// Map as plain sorted array of key/value pairs
+
+function map_has(map, key) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m << 1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else
+ return true
+ }
+ return false
+}
+
+function map_get(map, key, missing) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m << 1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else
+ return map[(m << 1) + 1]
+ }
+ return missing
+}
+
+function map_set(map, key, value) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m << 1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else {
+ map[(m << 1) + 1] = value
+ return
+ }
+ }
+ array_insert_pair(map, a << 1, key, value)
+}
+
+function map_delete(map, item) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m << 1]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else {
+ array_remove_pair(map, m << 1)
+ return
+ }
+ }
+}