From fd070d47b2e05d3a142493f58baab0a5f3f58221 Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Wed, 17 Apr 2024 15:43:30 +0200 Subject: Typescript. --- rules.ts | 11937 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 11937 insertions(+) create mode 100644 rules.ts (limited to 'rules.ts') diff --git a/rules.ts b/rules.ts new file mode 100644 index 0000000..675f2b9 --- /dev/null +++ b/rules.ts @@ -0,0 +1,11937 @@ +"use strict" + +// TODO: "approach" pause when about to move into intercept range? +// TODO: log end victory conditions at scenario start +// Check all push/clear_undo +// TODO: check flank attack + +// TODO: clean up use of who/what/which/where -- explicit selected_lord, selected_vassal, etc? +// TODO: remove unneccessary game.who = NOBODY etc +// TODO: call end_influence_check earlier when possible? + +// TODO: verify all push_state uses + +let game = null +let view = null +let states = {} + +function check_london_protected() { +// TODO IF HENRY/MARGARET ARE MUSTERED IT DOES NOT CHANGE FAVOUR +// ONLY L17/L18 and Pillage will cancel that event +//(it is annuled when london go to neutral + if (game.state === "pillage") { + return false + } + if (game.flags.london_for_york === 1 && game.where === LOC_LONDON) { + return true + } + else { + return false + } +} + +// === CONSTANTS === + +const data = require("./data.js") + +function find_card(name) { + let ix = data.cards.findIndex(x => x.name === name) + if (ix < 0) + throw "CANNOT FIND CARD: " + name + return ix +} + +function find_lord(name) { + let ix = data.lords.findIndex(x => x.name === name) + if (ix < 0) + throw "CANNOT FIND LORD: " + name + return ix +} + +function find_locale(name) { + let ix = data.locales.findIndex(x => x.name === name) + if (ix < 0) + throw "CANNOT FIND LOCALE: " + name + return ix +} + +function find_vassal(name) { + let ix = data.vassals.findIndex(x => x.name === name) + if (ix < 0) + throw "CANNOT FIND VASSAL: " + name + return ix +} + +const BOTH = "Both" +const LANCASTER = "Lancaster" +const YORK = "York" + +exports.roles = [ LANCASTER, YORK ] + +const NOBODY = -1 +const NOWHERE = -1 +const NOTHING = -1 + +const CALENDAR = 100 + +const VASSAL_READY = 29 +const VASSAL_CALENDAR = 30 +const VASSAL_OUT_OF_PLAY = 31 + +const SUMMER = 0 +const SPRING = 1 +const WINTER = 2 +const AUTUMN = 3 + +const SEASONS = [ + null, + WINTER, + SPRING, + SUMMER, + AUTUMN, + WINTER, + WINTER, + SPRING, + SUMMER, + AUTUMN, + WINTER, + WINTER, + SPRING, + SUMMER, + AUTUMN, + WINTER, + null, +] + +const INFLUENCE_TURNS = [ 1, 4, 6, 9, 11, 14 ] +const GROW_TURNS = [ 4, 9, 14 ] +const WASTE_TURNS = [ 5, 10 ] + +const HIT = [ "0", "\u2776", "\u2777", "\u2778", "\u2779", "\u277A", "\u277B" ] +const MISS = [ "0", "\u2460", "\u2461", "\u2462", "\u2463", "\u2464", "\u2465" ] + +// unit types +const RETINUE = 0 +const VASSAL = 1 +const MEN_AT_ARMS = 2 +const LONGBOWMEN = 3 +const MILITIA = 4 +const BURGUNDIANS = 5 +const MERCENARIES = 6 + +const FORCE_TYPE_COUNT = 7 +const FORCE_TYPE_NAME = [ "Retinue", "Vassal", "Men-at-Arms", "Longbowmen", "Militia", "Burgundians", "Mercenary" ] +const FORCE_PROTECTION = [ 4, 4, 3, 1, 1, 3, 3 ] + +// asset types +const PROV = 0 +const COIN = 1 +const CART = 2 +const SHIP = 3 + +const ASSET_TYPE_NAME = [ "Provender", "Coin", "Cart", "Ship" ] +const ASSET_TYPE_COUNT = 4 + +// battle array +const A1 = 0 // attackers +const A2 = 1 +const A3 = 2 +const D1 = 3 // defenders +const D2 = 4 +const D3 = 5 + +const lord_name = data.lords.map(lord => lord.name) + +const lord_count = data.lords.length +const vassal_count = data.vassals.length + +const first_lord = 0 +const last_lord = lord_count - 1 + +const first_locale = 0 +const last_locale = data.locales.length - 1 + +const first_vassal = 0 +const last_vassal = vassal_count - 1 + +const first_york_card = 0 +const last_york_card = 36 +const first_lancaster_card = 37 +const last_lancaster_card = 73 + +function is_york_card(c) { + return c >= first_york_card && c <= last_york_card +} + +function is_lancaster_card(c) { + return c >= first_lancaster_card && c <= last_lancaster_card +} + +const first_york_lord = 0 +const last_york_lord = 13 +const first_lancaster_lord = 14 +const last_lancaster_lord = 27 + +const YORK_LORD_MASK = 0x1fff +const LANCASTER_LORD_MASK = YORK_LORD_MASK << 14 + +function is_york_lord(lord) { + return lord >= first_york_lord && lord <= last_york_lord +} + +function is_lancaster_lord(lord) { + return lord >= first_lancaster_lord && lord <= last_lancaster_lord +} + +function is_marshal(lord) { + switch (lord) { + case LORD_MARGARET: + return true + case LORD_HENRY_VI: + return true + case LORD_HENRY_TUDOR: + return true + case LORD_EDWARD_IV: + return true + case LORD_GLOUCESTER_2: + return true + case LORD_RICHARD_III: + return true + case LORD_YORK: + return true + default: + return false + } +} + +function is_lieutenant(lord) { + switch (lord) { + case LORD_WARWICK_L: + return true + case LORD_SOMERSET_1: + return true + case LORD_GLOUCESTER_1: + return true + case LORD_WARWICK_Y: + return true + default: + return false + } +} + +function is_seaport(loc) { + return set_has(data.seaports, loc) +} + +function is_stronghold(loc) { + return data.locales[loc].type !== "exile" +} + +function is_exile(loc) { + return data.locales[loc].type === "exile" +} + +function is_city(loc) { + return data.locales[loc].type === "city" +} + +function is_town(loc) { + return data.locales[loc].type === "town" +} + +function is_wales(loc) { + return data.locales[loc].region === "Wales" +} + +function is_south(loc) { + return data.locales[loc].region === "South" +} + +function is_north(loc) { + return data.locales[loc].region === "North" +} + +function is_fortress(loc) { + return data.locales[loc].type === "fortress" +} + +function is_sea(loc) { + return data.locales[loc].type === "sea" +} + +function find_ports(here) { + if ((lord_has_capability(game.group, AOW_YORK_GREAT_SHIPS) || lord_has_capability(game.group, AOW_LANCASTER_GREAT_SHIPS))) + return data.all_ports + if (here === data.sea_1) return data.port_1 + if (here === data.sea_2) return data.port_2 + if (here === data.sea_3) return data.port_3 + if (here === data.exile_1) return data.port_1 + if (here === data.exile_2) return data.port_2 + if (here === data.exile_3) return data.port_3 + if (here === data.exile_4) return data.port_1 + if (set_has(data.port_1, here)) return data.port_1 + if (set_has(data.port_2, here)) return data.port_2 + if (set_has(data.port_3, here)) return data.port_3 + return null +} + +function find_sail_locales(here) { + if (here === data.sea_1) return data.way_sea_1 + if (here === data.sea_2) return data.way_sea_2 + if (here === data.sea_3) return data.way_sea_3 + if (here === data.exile_1) return data.way_exile_1 + if (here === data.exile_2) return data.way_exile_2 + if (here === data.exile_3) return data.way_exile_3 + if (set_has(data.port_1, here)) return data.way_port_1 + if (set_has(data.port_2, here)) return data.way_port_2 + if (set_has(data.port_3, here)) return data.way_port_3 + return null +} + +function make_locale_list(pred) { + let list = [] + for (let loc = first_locale; loc <= last_locale; ++loc) + if (pred(loc)) + list.push(loc) + return list +} + +const all_north_locales = make_locale_list(is_north) +const all_south_locales = make_locale_list(is_south) +const all_wales_locales = make_locale_list(is_wales) +const all_city_locales = make_locale_list(is_city) +const all_town_locales = make_locale_list(is_town) +const all_fortress_locales = make_locale_list(is_fortress) + +const Y1 = find_card("Y1") +const Y2 = find_card("Y2") +const Y3 = find_card("Y3") +const Y4 = find_card("Y4") +const Y5 = find_card("Y5") +const Y6 = find_card("Y6") +const Y7 = find_card("Y7") +const Y8 = find_card("Y8") +const Y9 = find_card("Y9") +const Y10 = find_card("Y10") +const Y11 = find_card("Y11") +const Y12 = find_card("Y12") +const Y13 = find_card("Y13") +const Y14 = find_card("Y14") +const Y15 = find_card("Y15") +const Y16 = find_card("Y16") +const Y17 = find_card("Y17") +const Y18 = find_card("Y18") +const Y19 = find_card("Y19") +const Y20 = find_card("Y20") +const Y21 = find_card("Y21") +const Y22 = find_card("Y22") +const Y23 = find_card("Y23") +const Y24 = find_card("Y24") +const Y25 = find_card("Y25") +const Y26 = find_card("Y26") +const Y27 = find_card("Y27") +const Y28 = find_card("Y28") +const Y29 = find_card("Y29") +const Y30 = find_card("Y30") +const Y31 = find_card("Y31") +const Y32 = find_card("Y32") +const Y33 = find_card("Y33") +const Y34 = find_card("Y34") +const Y35 = find_card("Y35") +const Y36 = find_card("Y36") +const Y37 = find_card("Y37") + +const L1 = find_card("L1") +const L2 = find_card("L2") +const L3 = find_card("L3") +const L4 = find_card("L4") +const L5 = find_card("L5") +const L6 = find_card("L6") +const L7 = find_card("L7") +const L8 = find_card("L8") +const L9 = find_card("L9") +const L10 = find_card("L10") +const L11 = find_card("L11") +const L12 = find_card("L12") +const L13 = find_card("L13") +const L14 = find_card("L14") +const L15 = find_card("L15") +const L16 = find_card("L16") +const L17 = find_card("L17") +const L18 = find_card("L18") +const L19 = find_card("L19") +const L20 = find_card("L20") +const L21 = find_card("L21") +const L22 = find_card("L22") +const L23 = find_card("L23") +const L24 = find_card("L24") +const L25 = find_card("L25") +const L26 = find_card("L26") +const L27 = find_card("L27") +const L28 = find_card("L28") +const L29 = find_card("L29") +const L30 = find_card("L30") +const L31 = find_card("L31") +const L32 = find_card("L32") +const L33 = find_card("L33") +const L34 = find_card("L34") +const L35 = find_card("L35") +const L36 = find_card("L36") +const L37 = find_card("L37") + +const LORD_YORK = find_lord("York") +const LORD_MARCH = find_lord("March") +const LORD_EDWARD_IV = find_lord("Edward IV") +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_Y1 = find_lord("Northumberland Y1") +const LORD_NORTHUMBERLAND_Y2 = find_lord("Northumberland Y2") +const LORD_GLOUCESTER_1 = find_lord("Gloucester 1") +const LORD_GLOUCESTER_2 = find_lord("Gloucester 2") +const LORD_RICHARD_III = find_lord("Richard III") +const LORD_NORFOLK = find_lord("Norfolk") +const LORD_WARWICK_Y = find_lord("Warwick Y") + +const LORD_HENRY_VI = find_lord("Henry VI") +const LORD_MARGARET = find_lord("Margaret") +const LORD_SOMERSET_1 = find_lord("Somerset 1") +const LORD_SOMERSET_2 = find_lord("Somerset 2") +const LORD_EXETER_1 = find_lord("Exeter 1") +const LORD_EXETER_2 = find_lord("Exeter 2") +const LORD_BUCKINGHAM = find_lord("Buckingham") +const LORD_CLARENCE = find_lord("Clarence") +const LORD_NORTHUMBERLAND_L = find_lord("Northumberland L") + +const LORD_JASPER_TUDOR_1 = find_lord("Jasper Tudor 1") +const LORD_JASPER_TUDOR_2 = find_lord("Jasper Tudor 2") +const LORD_HENRY_TUDOR = find_lord("Henry Tudor") +const LORD_OXFORD = find_lord("Oxford") +const LORD_WARWICK_L = find_lord("Warwick L") + +const LOC_BAMBURGH = find_locale("Bamburgh") +const LOC_NEWCASTLE = find_locale("Newcastle") +const LOC_APPLEBY = find_locale("Appleby") +const LOC_HEXHAM = find_locale("Hexham") +const LOC_CARLISLE = find_locale("Carlisle") +const LOC_HARLECH = find_locale("Harlech") +const LOC_PEMBROKE = find_locale("Pembroke") +const LOC_CARDIFF = find_locale("Cardiff") +const LOC_HEREFORD = find_locale("Hereford") +const LOC_LUDLOW = find_locale("Ludlow") +const LOC_SHREWSBURY = find_locale("Shrewsbury") +const LOC_SALISBURY = find_locale("Salisbury") +const LOC_WINCHESTER = find_locale("Winchester") +const LOC_GUILDFORD = find_locale("Guildford") +const LOC_ARUNDEL = find_locale("Arundel") +const LOC_SOUTHAMPTON = find_locale("Southampton") +const LOC_ROCHESTER = find_locale("Rochester") +const LOC_DOVER = find_locale("Dover") +const LOC_CANTERBURY = find_locale("Canterbury") +const LOC_HASTINGS = find_locale("Hastings") +const LOC_DORCHESTER = find_locale("Dorchester") +const LOC_EXETER = find_locale("Exeter") +const LOC_PLYMOUTH = find_locale("Plymouth") +const LOC_LAUNCESTON = find_locale("Launceston") +const LOC_TRURO = find_locale("Truro") +const LOC_WELLS = find_locale("Wells") +const LOC_BRISTOL = find_locale("Bristol") +const LOC_GLOUCESTER = find_locale("Gloucester") +const LOC_OXFORD = find_locale("Oxford") +const LOC_NEWBURY = find_locale("Newbury") +const LOC_LONDON = find_locale("London") +const LOC_ST_ALBANS = find_locale("St Albans") +const LOC_BEDFORD = find_locale("Bedford") +const LOC_CAMBRIDGE = find_locale("Cambridge") +const LOC_BURY_ST_EDMUNDS = find_locale("Bury St Edmunds") +const LOC_IPSWICH = find_locale("Ipswich") +const LOC_NORWICH = find_locale("Norwich") +const LOC_LYNN = find_locale("Lynn") +const LOC_ELY = find_locale("Ely") +const LOC_PETERBOROUGH = find_locale("Peterborough") +const LOC_NORTHAMPTON = find_locale("Northampton") +const LOC_COVENTRY = find_locale("Coventry") +const LOC_LEICESTER = find_locale("Leicester") +const LOC_LICHFIELD = find_locale("Lichfield") +const LOC_DERBY = find_locale("Derby") +const LOC_NOTTINGHAM = find_locale("Nottingham") +const LOC_WORCESTER = find_locale("Worcester") +const LOC_CHESTER = find_locale("Chester") +const LOC_LANCASTER = find_locale("Lancaster") +const LOC_LINCOLN = find_locale("Lincoln") +const LOC_YORK = find_locale("York") +const LOC_CALAIS = find_locale("Calais") +const LOC_FRANCE = find_locale("France") +const LOC_SCOTLAND = find_locale("Scotland") +const LOC_IRELAND = find_locale("Ireland") +const LOC_BURGUNDY = find_locale("Burgundy") +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") + +const VASSAL_BEAUMONT = find_vassal("Beaumont") +const VASSAL_BONVILLE = find_vassal("Bonville") +const VASSAL_DEVON = find_vassal("Devon") +const VASSAL_DUDLEY = find_vassal("Dudley") +const VASSAL_ESSEX = find_vassal("Essex") +const VASSAL_FAUCONBERG = find_vassal("Fauconberg") +const VASSAL_NORFOLK = find_vassal("Norfolk") +const VASSAL_OXFORD = find_vassal("Oxford") +const VASSAL_SHREWSBURY = find_vassal("Shrewsbury") +const VASSAL_STANLEY = find_vassal("Stanley") +const VASSAL_SUFFOLK = find_vassal("Suffolk") +const VASSAL_WESTMORLAND = find_vassal("Westmoreland") +const VASSAL_WORCESTER = find_vassal("Worcester") +const VASSAL_CLIFFORD = find_vassal("Clifford") +const VASSAL_EDWARD = find_vassal("Edward") +const VASSAL_HASTINGS = find_vassal("Hastings") +const VASSAL_MONTAGU = find_vassal("Montagu") +const VASSAL_THOMAS_STANLEY = find_vassal("Thomas Stanley") +const VASSAL_TROLLOPE = find_vassal("Trollope") + +const AOW_LANCASTER_CULVERINS_AND_FALCONETS = [L1 , L2] +const AOW_LANCASTER_MUSTERD_MY_SOLDIERS = L3 +const AOW_LANCASTER_HERALDS = L4 +const AOW_LANCASTER_CHURCH_BLESSINGS = L5 +const AOW_LANCASTER_GREAT_SHIPS = L6 // TODO Resolve bug about not correctly targeting ports on other seats and further +const AOW_LANCASTER_HARBINGERS = L7 +const AOW_LANCASTER_HAY_WAINS = L8 +const AOW_LANCASTER_QUARTERMASTERS = L9 +const AOW_LANCASTER_CHAMBERLAINS = L10 +const AOW_LANCASTER_IN_THE_NAME_OF_THE_KING = L11 +const AOW_LANCASTER_COMMISION_OF_ARRAY = L12 +const AOW_LANCASTER_EXPERT_COUNSELLORS = L13 +const AOW_LANCASTER_PERCYS_POWER = L14 +const AOW_LANCASTER_KINGS_PARLEY = L15 +const AOW_LANCASTER_NORTHMEN = L16 +const AOW_LANCASTER_MARGARET = L17 +const AOW_LANCASTER_COUNCIL_MEMBER = L18 +const AOW_LANCASTER_ANDREW_TROLLOPE = L19 +const AOW_LANCASTER_VETERAN_OF_FRENCH_WARS = L20 +const AOW_LANCASTER_MY_FATHERS_BLOOD = L21 +const AOW_LANCASTER_STAFFORD_ESTATES = L22 +const AOW_LANCASTER_MONTAGU = L23 +const AOW_LANCASTER_MARRIED_TO_A_NEVILLE = L24 +const AOW_LANCASTER_WELSH_LORD = L25 +const AOW_LANCASTER_EDWARD = L26 +const AOW_LANCASTER_BARDED_HORSE = L27 +const AOW_LANCASTER_LOYAL_SOMERSET = L28 +const AOW_LANCASTER_HIGH_ADMIRAL = L29 +const AOW_LANCASTER_MERCHANTS = L30 +const AOW_LANCASTER_YEOMEN_OF_THE_CROWN = L31 +const AOW_LANCASTER_TWO_ROSES = L32 +const AOW_LANCASTER_PHILIBERT_DE_CHANDEE = L33 +const AOW_LANCASTER_PIQUIERS = L34 +const AOW_LANCASTER_THOMAS_STANLEY = L35 +const AOW_LANCASTER_CHEVALIERS = L36 +const AOW_LANCASTER_MADAME_LA_GRANDE = L37 + +const AOW_YORK_CULVERINS_AND_FALCONETS = [Y1, Y2] +const AOW_YORK_MUSTERD_MY_SOLDIERS = Y3 +const AOW_YORK_WE_DONE_DEEDS_OF_CHARITY = Y4 +const AOW_YORK_THOMAS_BOURCHIER = Y5 +const AOW_YORK_GREAT_SHIPS = Y6 +const AOW_YORK_HARBINGERS = Y7 +const AOW_YORK_ENGLAND_IS_MY_HOME = Y8 +const AOW_YORK_BARRICADES = Y9 +const AOW_YORK_AGITATORS = Y10 +const AOW_YORK_YORKISTS_NEVER_WAIT = Y11 +const AOW_YORK_SOLDIERS_OF_FORTUNE = Y12 +const AOW_YORK_SCOURERS = Y13 +const AOW_YORK_BURGUNDIANS = [ Y14, Y23 ] +const AOW_YORK_NAVAL_BLOCKADE = Y15 // TODO DEBUG pop_state() going into parley and not after province selection +const AOW_YORK_BELOVED_WARWICK = Y16 +const AOW_YORK_ALICE_MONTAGU = Y17 +const AOW_YORK_IRISHMEN = Y18 +const AOW_YORK_WELSHMEN = Y19 +const AOW_YORK_YORKS_FAVOURED_SON = Y20 +const AOW_YORK_SOUTHERNERS = Y21 +const AOW_YORK_FAIR_ARBITER = Y22 +const AOW_YORK_HASTINGS = Y24 +const AOW_YORK_PEMBROKE = Y25 +const AOW_YORK_FALLEN_BROTHER = Y26 +const AOW_YORK_PERCYS_NORTH1 = Y27 +const AOW_YORK_FIRST_SON = Y28 +const AOW_YORK_STAFFORD_BRANCH = Y29 +const AOW_YORK_CAPTAIN = Y30 +const AOW_YORK_WOODWILLES = Y31 +const AOW_YORK_FINAL_CHARGE = Y32 // TODO AFTER ALL OTHER BATTLE PROMPTS TO SEE WHERE TO PLACE IT +const AOW_YORK_BLOODY_THOU_ART = Y33 +const AOW_YORK_SO_WISE_SO_YOUNG = Y34 +const AOW_YORK_KINGDOM_UNITED = Y35 +const AOW_YORK_VANGUARD = Y36 // TODO AFTER ALL OTHER BATTLE PROMPTS TO SEE WHERE TO PLACE IT +const AOW_YORK_PERCYS_NORTH2 = Y37 + +const EVENT_LANCASTER_LEEWARD_BATTLE_LINE = L1 +const EVENT_LANCASTER_FLANK_ATTACK = L2 +const EVENT_LANCASTER_ESCAPE_SHIP = L3 +const EVENT_LANCASTER_BE_SENT_FOR = L4 +const EVENT_LANCASTER_SUSPICION = L5 +const EVENT_LANCASTER_SEAMANSHIP = L6 +const EVENT_LANCASTER_FOR_TRUST_NOT_HIM = L7 +const EVENT_LANCASTER_FORCED_MARCHES = L8 +const EVENT_LANCASTER_RISING_WAGES = L9 +const EVENT_LANCASTER_NEW_ACT_OF_PARLIAMENT = L10 + +const EVENT_LANCASTER_BLOCKED_FORD = L11 +const EVENT_LANCASTER_RAVINE = L12 +const EVENT_LANCASTER_ASPIELLES = L13 +const EVENT_LANCASTER_SCOTS = L14 +const EVENT_LANCASTER_HENRY_PRESSURES_PARLIAMENT = L15 +const EVENT_LANCASTER_WARDEN_OF_THE_MARCHES = L16 +const EVENT_LANCASTER_MY_CROWN_IS_IN_MY_HEART = L17 +const EVENT_LANCASTER_PARLIAMENT_VOTES = L18 +const EVENT_LANCASTER_HENRYS_PROCLAMATION = L19 +const EVENT_LANCASTER_PARLIAMENTS_TRUCE = L20 +const EVENT_LANCASTER_FRENCH_FLEET = L21 +const EVENT_LANCASTER_FRENCH_TROOPS = L22 +const EVENT_LANCASTER_WARWICKS_PROPAGANDA = L23 +const EVENT_LANCASTER_WARWICKS_PROPAGANDA2 = L24 +const EVENT_LANCASTER_WELSH_REBELLION = L25 +const EVENT_LANCASTER_HENRY_RELEASED = L26 +const EVENT_LANCASTER_LUNIVERSELLE_ARAGNE = L27 +const EVENT_LANCASTER_REBEL_SUPPLY_DEPOT = L28 +const EVENT_LANCASTER_TO_WILFUL_DISOBEDIANCE = L29 +const EVENT_LANCASTER_FRENCH_WAR_LOANS = L30 +const EVENT_LANCASTER_ROBINS_REBELLION = L31 +const EVENT_LANCASTER_TUDOR_BANNERS = L32 +const EVENT_LANCASTER_SURPRISE_LANDING = L33 +const EVENT_LANCASTER_BUCKINGHAMS_PLOT = L34 +const EVENT_LANCASTER_MARGARET_BEAUFORT = L35 +const EVENT_LANCASTER_TALBOT_TO_THE_RESCUE = L36 +const EVENT_LANCASTER_THE_EARL_OF_RICHMOND = L37 + +const EVENT_YORK_LEEWARD_BATTLE_LINE = Y1 +const EVENT_YORK_FLANK_ATTACK = Y2 +const EVENT_YORK_ESCAPE_SHIP = [Y3 , Y9] +const EVENT_YORK_JACK_CADE = Y4 +const EVENT_YORK_SUSPICION = Y5 +const EVENT_YORK_SEAMANSHIP = Y6 +const EVENT_YORK_YORKISTS_BLOCK_PARLIAMENT = Y7 +const EVENT_YORK_EXILE_PACT = Y8 +const EVENT_YORK_TAX_COLLECTORS = Y10 + +const EVENT_YORK_BLOCKED_FORD = Y11 +const EVENT_YORK_PARLIAMENTS_TRUCE = Y12 +const EVENT_YORK_ASPIELLES = Y13 +const EVENT_YORK_RICHARD_OF_YORK = Y14 +const EVENT_YORK_LONDON_FOR_YORK = Y15 // TODO PROTECTION AND UI +const EVENT_YORK_THE_COMMONS = Y16 +const EVENT_YORK_SHEWOLF_OF_FRANCE = Y17 +const EVENT_YORK_SUCCESSION = Y18 +const EVENT_YORK_CALTROPS = Y19 +const EVENT_YORK_YORKIST_PARADE = Y20 +const EVENT_YORK_SIR_RICHARD_LEIGH = Y21 +const EVENT_YORK_LOYALTY_AND_TRUST = Y22 +const EVENT_YORK_CHARLES_THE_BOLD = Y23 +const EVENT_YORK_SUN_IN_SPLENDOUR = Y24 +const EVENT_YORK_OWAIN_GLYNDWR = Y25 +const EVENT_YORK_DUBIOUS_CLARENCE = Y26 +const EVENT_YORK_YORKIST_NORTH = Y27 +const EVENT_YORK_GLOUCESTER_AS_HEIR = Y28 +const EVENT_YORK_DORSET = Y29 +const EVENT_YORK_REGROUP = Y30 // TODO +// Play in Battle at Battle Array step. +// If played when Yorkist active they pay click on that event to make the lord recover his TROOPS (no vassals/retinue) +// for recover per armour +const EVENT_YORK_EARL_RIVERS = Y31 +const EVENT_YORK_THE_KINGS_NAME = Y32 // TODO IMPROVE PROMPTS/QOL +const EVENT_YORK_EDWARD_V = Y33 +const EVENT_YORK_AN_HONEST_TALE_SPEEDS_BEST = Y34 +const EVENT_YORK_PRIVY_COUNCIL = Y35 +const EVENT_YORK_SWIFT_MANEUVER = Y36 +const EVENT_YORK_PATRICK_DE_LA_MOTE = Y37 + +// === STATE: ACTIVE PLAYER === + +var P1 = null +var P2 = null + +var first_friendly_lord = 0 +var last_friendly_lord = 13 +var first_enemy_lord = 14 +var last_enemy_lord = 27 + +function update_aliases() { + if (game.active === YORK) { + first_friendly_lord = 0 + last_friendly_lord = 13 + first_enemy_lord = 14 + last_enemy_lord = 27 + } else if (game.active === LANCASTER) { + 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 + } + if (game.rebel === YORK) { + P1 = YORK + P2 = LANCASTER + } else { + P1 = LANCASTER + P2 = YORK + } +} + +function load_state(state) { + if (game !== state) { + game = state + update_aliases() + } +} + +function set_active(new_active) { + if (game.active !== new_active) { + clear_undo() + game.active = new_active + update_aliases() + } +} + +function set_active_enemy() { + if (game.active === YORK) + set_active(LANCASTER) + else + set_active(YORK) +} + +function set_active_command() { + if (is_york_lord(game.command)) + set_active(YORK) + else + set_active(LANCASTER) +} + +// === STATE: TURN === + +function current_turn() { + return game.turn >> 1 +} + +function current_season() { + return SEASONS[game.turn >> 1] +} + +function is_campaign_phase() { + return (game.turn & 1) === 1 +} + +function is_levy_phase() { + return (game.turn & 1) === 0 +} + +function current_turn_name() { + return String(game.turn >> 1) +} + +function max_plan_length() { + switch (current_season()) { + case SUMMER: + return 7 + case SPRING: + return 6 + case WINTER: + return 4 + case AUTUMN: + return 6 + } +} + +// === STATE: CARDS === + +function current_hand() { + if (game.active === YORK) + return game.hand_y + return game.hand_l +} + +function is_friendly_card(c) { + if (game.active === YORK) + return is_york_card(c) + return is_lancaster_card(c) +} + +function has_card_in_hand(c) { + if (game.active === YORK) + return set_has(game.hand_y, c) + return set_has(game.hand_l, c) +} + +function could_play_card(c) { + if (!game.hidden) { + // TODO: check capabilities on lords revealed in battle if hidden + if (map_has_value(game.pieces.capabilities, c)) + return false + } + if (set_has(game.events, c)) + return false + if (is_york_card(c)) + return game.hand_y.length > 0 + if (is_lancaster_card(c)) + return game.hand_l.length > 0 + return true +} + +function count_cards_in_plan(plan, lord) { + let n = 0 + for (let c of plan) + if (c === lord) + ++n + return n +} + +function is_card_in_use(c) { + if (set_has(game.hand_y, c)) + return true + if (set_has(game.hand_l, c)) + return true + if (set_has(game.events, c)) + return true + if (map_has_value(game.pieces.capabilities, c)) + return true + return false +} + +function list_deck() { + let deck = [] + let first_card = game.active === YORK ? first_york_card : first_lancaster_card + let last_card = game.active === YORK ? last_york_card : last_lancaster_card + for (let c = first_card; c <= last_card; ++c) + if (!is_card_in_use(c) && is_card_in_scenario(c)) + deck.push(c) + return deck +} + +function draw_card(deck) { + clear_undo() + let i = random(deck.length) + let c = deck[i] + set_delete(deck, c) + return c +} + +function draw_two_cards() { + let deck = list_deck() + return [ draw_card(deck), draw_card(deck) ] +} + +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_extra_levy_events() { + for (let i = 0; i < game.events.length; ) { + let c = game.events[i] + if (c === Y20) + 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 + } +} + +function is_event_in_play(c) { + return set_has(game.events, c) +} + +// === STATE: LORD === + +function get_lord_locale(lord) { + return map_get(game.pieces.locale, lord, NOWHERE) +} + +function set_lord_locale(lord, locale) { + if (locale === NOWHERE) + map_delete(game.pieces.locale, lord) + else + map_set(game.pieces.locale, lord, locale) +} + +function get_lord_calendar(lord) { + if (is_lord_on_calendar(lord)) + return get_lord_locale(lord) - CALENDAR + return 0 +} + +function set_lord_calendar(lord, turn) { + if (turn < 1) + turn = 1 + if (turn > 16) + turn = 16 + set_lord_locale(lord, CALENDAR + turn) +} + +function set_lord_in_exile(lord) { + game.pieces.in_exile = pack1_set(game.pieces.in_exile, lord, 1) +} + +function get_lord_in_exile(lord) { + return pack1_get(game.pieces.in_exile, lord) +} + +function remove_lord_from_exile(lord) { + game.pieces.in_exile = pack1_set(game.pieces.in_exile, lord, 0) +} + +function get_lord_capability(lord, n) { + return map2_get(game.pieces.capabilities, lord, n, NOTHING) +} + +function set_lord_capability(lord, n, x) { + if (x === NOTHING) + map2_delete(game.pieces.capabilities, lord, n) + else + map2_set(game.pieces.capabilities, lord, n, x) +} + +function get_lord_assets(lord, n) { + return map_get_pack4(game.pieces.assets, lord, n) +} + +function set_lord_assets(lord, n, x) { + if (x < 0) + x = 0 + if (x > 15) + x = 15 + map_set_pack4(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 drop_prov(lord) { + add_lord_assets(lord, PROV, -1) +} + +function drop_cart(lord) { + add_lord_assets(lord, CART, -1) +} + +function get_lord_forces(lord, n) { + return map_get_pack4(game.pieces.forces, lord, n) +} + +function set_lord_forces(lord, n, x) { + if (x < 0) + x = 0 + if (x > 15) + x = 15 + map_set_pack4(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 get_lord_routed_forces(lord, n) { + return map_get_pack4(game.pieces.routed, lord, n) +} + +function set_lord_routed_forces(lord, n, x) { + if (x < 0) + x = 0 + if (x > 15) + x = 15 + map_set_pack4(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 get_lord_moved(lord) { + return map_get(game.pieces.moved, lord, 0) +} + +function set_lord_moved(lord, x) { + map_set(game.pieces.moved, lord, x) +} + +function clear_lords_moved() { + map_clear(game.pieces.moved) +} + +function set_lord_fought(lord) { + set_lord_moved(lord, 1) + // TODO: is this needed? + game.battle.fought = pack1_set(game.battle.fought, lord, 1) +} + +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(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 pay_lord(lord) { + // reuse "moved" flag for pay + let n = get_lord_moved(lord) - 1 + set_lord_moved(lord, n) +} + +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_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_friendly_lord(lord) { + return lord >= first_friendly_lord && lord <= last_friendly_lord +} + +function is_enemy_lord(lord) { + return lord >= first_enemy_lord && lord <= last_enemy_lord +} + +function count_lord_all_forces(lord) { + return ( + 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_ships(lord) { + return get_lord_assets(lord, SHIP) +} + +function count_lord_transport(lord) { + return get_lord_assets(lord, CART) +} + +function lord_has_unrouted_units(lord) { + for (let x = 0; x < FORCE_TYPE_COUNT; ++x) + if (get_lord_forces(lord, x) > 0) + return true + let result = false + for_each_vassal_with_lord(lord, v => { + if (!set_has(game.battle.routed_vassals, v)) + result = true + }) + return result +} + +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 +} + +function get_force_name(lord, n, x) { + if (n === RETINUE) + return `${lord_name[lord]}'s Retinue` + if (n === VASSAL) + return `Vassal ${data.vassals[x].name}` + return FORCE_TYPE_NAME[n] +} + +// === STATE: LORD (SHARED) === + +function get_shared_assets(loc, what) { + let m = 0 + 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) + if (game.state === "supply_source" && lord_has_capability(lord, AOW_LANCASTER_HAY_WAINS) && what === CART) { + m = get_lord_assets(lord, CART) + n += m + } + if (game.state === "supply_source" && (lord_has_capability(lord, AOW_YORK_GREAT_SHIPS) || lord_has_capability(lord, AOW_YORK_GREAT_SHIPS)) && what === SHIP) { + m = get_lord_assets(lord, SHIP) + n += m + } + } + return n +} + +function count_group_assets(type, group = game.group) { + let n = 0 + for (let lord of group) { + n += get_lord_assets(lord, type) + if (type === CART) { + if (game.state === "command" || game.state === "march_laden") + if (lord_has_capability(lord, AOW_LANCASTER_HAY_WAINS)) + n += get_lord_assets(lord, CART) + } + } + return n +} + +function count_group_ships() { + let n = 0 + for (let lord of game.group) { + n += count_lord_ships(lord) + if (lord_has_capability(lord, AOW_YORK_GREAT_SHIPS) || lord_has_capability(lord, AOW_LANCASTER_GREAT_SHIPS)) + n += count_lord_ships(lord) + } + return n +} + +function count_group_transport(group = game.group) { + let n = 0 + for (let lord of group) + n += count_lord_transport(lord) + return n +} + +function count_group_lords() { + return game.group.length +} + +// === STATE: VASSAL === + +function set_vassal_lord_and_service(vassal, lord, service) { + game.pieces.vassals[vassal] = lord + (service << 5) +} + +function get_vassal_lord(vassal) { + return game.pieces.vassals[vassal] & 31 +} + +function get_vassal_service(vassal) { + return game.pieces.vassals[vassal] >> 5 +} + +function setup_vassals(excludes = []) { + for (let x = first_vassal; x <= last_vassal; x++) { + if (!excludes.includes(x) && data.vassals[x].capability === undefined) { + set_vassal_lord_and_service(x, VASSAL_READY, 0) + } + } +} + +function is_vassal_ready(x) { + return get_vassal_lord(x) === VASSAL_READY +} + +function is_vassal_mustered_with(x, lord) { + return get_vassal_lord(x) === lord +} + +function is_vassal_mustered_with_friendly_lord(x) { + return is_friendly_lord(get_vassal_lord(x)) +} + +function is_vassal_mustered_with_york_lord(x) { + return is_york_lord(get_vassal_lord(x)) +} + +function for_each_vassal_with_lord(lord, f) { + for (let x = first_vassal; x <= last_vassal; x++) + if (is_vassal_mustered_with(x, lord)) + f(x) +} + +function count_vassals_with_lord(lord) { + let n = 0 + for (let x = first_vassal; x <= last_vassal; x++) + if (is_vassal_mustered_with(x, lord)) + ++n + return n +} + +function muster_vassal(vassal, lord) { + if (data.vassals[vassal].service !== 0) { + let service = current_turn() + data.vassals[vassal].service + if (lord_has_capability(lord, AOW_YORK_ALICE_MONTAGU)) + service += 1 + set_vassal_lord_and_service(vassal, lord, service) + } else { + set_vassal_lord_and_service(vassal, lord, 0) + } +} + +function disband_vassal(vassal) { + if (data.vassals[vassal].service > 0) { + let new_turn = current_turn() + (6 - data.vassals[vassal].service) + set_vassal_lord_and_service(vassal, VASSAL_CALENDAR, new_turn) + log(`Disbanded V${vassal} to turn ${current_turn() + (6 - data.vassals[vassal].service)}.`) + } else { + // TODO: special vassals with no service marker!? + set_vassal_lord_and_service(vassal, VASSAL_OUT_OF_PLAY, 0) + log(`Disbanded V${vassal}.`) + } + if (vassal === VASSAL_HASTINGS) { + discard_card_capability(AOW_YORK_HASTINGS) + logi(`Hastings Discarded`) + } +} + +function pay_vassal(vassal) { + if (current_turn() < 15) + set_vassal_lord_and_service(vassal, get_vassal_lord(vassal), current_turn() + 1) +} + +function rout_vassal(_lord, vassal) { + set_add(game.battle.routed_vassals, vassal) +} + +// === STATE: LOCALE === + +function is_favour_friendly(loc) { + if (game.active === YORK) + return has_favoury_marker(loc) + else + return has_favourl_marker(loc) +} + +function is_favour_enemy(loc, side) { + if (side === LANCASTER) + return has_favoury_marker(loc) + else + return has_favourl_marker(loc) +} + +function has_favourl_marker(loc) { + return set_has(game.pieces.favourl, loc) +} + +function add_favourl_marker(loc) { + set_add(game.pieces.favourl, loc) +} + +function remove_favourl_marker(loc) { + set_delete(game.pieces.favourl, loc) +} + +function has_favoury_marker(loc) { + return set_has(game.pieces.favoury, loc) +} + +function add_favoury_marker(loc) { + set_add(game.pieces.favoury, loc) +} + +function remove_favoury_marker(loc) { + set_delete(game.pieces.favoury, loc) +} + +function shift_favour_away(loc) { + if (game.active === YORK) + shift_favour_toward_lancaster(loc) + else + shift_favour_toward_york(loc) +} + +function shift_favour_toward(loc) { + if (game.active === YORK) + shift_favour_toward_york(loc) + else + shift_favour_toward_lancaster(loc) +} + +function shift_favour_toward_york(loc) { + if (has_favourl_marker(loc)) + remove_favourl_marker(loc) + else + add_favoury_marker(loc) +} + +function shift_favour_toward_lancaster(loc) { + if (has_favoury_marker(loc)) + remove_favoury_marker(loc) + else + add_favourl_marker(loc) +} + +function set_favour_enemy(loc) { + if (game.active === YORK) { + remove_favoury_marker(loc) + add_favourl_marker(loc) + } else { + remove_favourl_marker(loc) + add_favoury_marker(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 has_depleted_marker(loc) { + return set_has(game.pieces.depleted, loc) +} + +function add_depleted_marker(loc) { + set_add(game.pieces.depleted, loc) +} + +function remove_depleted_marker(loc) { + set_delete(game.pieces.depleted, loc) +} + +function remove_exhausted_marker(loc) { + set_delete(game.pieces.exhausted, loc) +} + +function refresh_locale(locale) { + if (has_depleted_marker(locale)) { + remove_depleted_marker(locale) + } + if (has_exhausted_marker(locale)) { + remove_exhausted_marker(locale) + add_depleted_marker(locale) + } +} + +function deplete_locale(loc) { + if (has_depleted_marker(loc)) { + remove_depleted_marker(loc) + add_exhausted_marker(loc) + } else { + add_depleted_marker(loc) + } +} + +function is_neutral_locale(loc) { + return !has_favourl_marker(loc) && !has_favoury_marker(loc) +} + +function has_favour_in_locale(side, loc) { + if (side === YORK) + return has_favoury_marker(loc) + else + return has_favourl_marker(loc) +} + +function is_friendly_locale(loc) { + if (loc !== NOWHERE && loc < CALENDAR) { + if (has_enemy_lord(loc)) + return false + if (is_favour_friendly(loc)) + return true + } + return false +} + +function is_adjacent_friendly_port_english_channel(loc) { + for (let next of data.locales[loc].adjacent) { + if (is_friendly_locale(next) && data.port_2.includes(next)) + return true + } +} + +// === STATE: LORD & LOCALE === + +function is_lord_at_friendly_locale(lord) { + return is_friendly_locale(get_lord_locale(lord)) +} + +function is_lord_in_wales(lord) { + return is_wales(get_lord_locale(lord)) +} + +function is_lord_in_south(lord) { + return is_south(get_lord_locale(lord)) +} + +function is_lord_in_north(lord) { + return is_north(get_lord_locale(lord)) +} + +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_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) + return true + return false +} + +function is_lord_in_or_adjacent_to_north(lord) { + let here = get_lord_locale(lord) + if (is_north(here)) + return true + for (let loc of data.locales[here].adjacent) + if (is_north(loc)) + return true + return false +} + +function is_lord_in_or_adjacent_to_south(lord) { + let here = get_lord_locale(lord) + if (is_south(here)) + return true + for (let loc of data.locales[here].adjacent) + if (is_south(loc)) + return true + return false +} + +function is_lord_in_or_adjacent_to_wales(lord) { + let here = get_lord_locale(lord) + if (is_wales(here)) + return true + for (let loc of data.locales[here].adjacent) + if (is_wales(loc)) + return true + return false +} + +// Captain capability (lieutenant/marshall only if no other) +function other_marshal_or_lieutenant(loc) { + let here = loc + let n = 0 + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) + if (lord !== game.command) { + if (get_lord_locale(lord) === here && (is_marshal(lord) || is_lieutenant(lord))) + n += 1 + } + if (n === 0) + return false + else + return true +} + +function has_adjacent_enemy(loc) { + for (let next of data.locales[loc].adjacent) + if (has_unbesieged_enemy_lord(next)) + return true + return false +} + +function has_adjacent_friendly(loc) { + for (let next of data.locales[loc].adjacent) + if (has_friendly_lord(next)) + return true + return false +} + +// === 1.4 INFLUENCE === + +function reduce_influence(amt) { + if (game.active === YORK) + reduce_york_influence(amt) + else + reduce_lancaster_influence(amt) +} + +function reduce_york_influence(amt) { + game.influence += amt +} + +function increase_york_influence(amt) { + game.influence -= amt +} + +function reduce_lancaster_influence(amt) { + game.influence -= amt +} + +function increase_lancaster_influence(amt) { + game.influence += amt +} + +// Bonus score but still maxed at 5 +function influence_capabilities(lord, score) { + let here = get_lord_locale(game.group) + if (game.active === YORK && is_event_in_play(EVENT_YORK_YORKIST_PARADE)) + score += 2 + if (game.active === YORK && is_event_in_play(EVENT_YORK_PRIVY_COUNCIL)) + score += 1 + if (game.state === "parley" && ((is_event_in_play(EVENT_YORK_RICHARD_OF_YORK) && game.active === YORK) || lord_has_capability(game.group, AOW_LANCASTER_IN_THE_NAME_OF_THE_KING))) + score += 1 + if (get_lord_locale(LORD_MARGARET) === here && lord_has_capability(game.group, AOW_LANCASTER_LOYAL_SOMERSET)) + score += 1 + if (lord_has_capability(lord, AOW_YORK_YORKS_FAVOURED_SON)) + score += 1 + if ( + get_lord_locale(LORD_WARWICK_L) === here && + lord_has_capability(game.group, AOW_LANCASTER_MARRIED_TO_A_NEVILLE) && + is_friendly_locale(here) + ) + score += 2 + if (has_favoury_marker(here) && lord_has_capability(lord, AOW_YORK_FAIR_ARBITER)) + score += 1 + if (lord_has_capability(lord, AOW_YORK_FALLEN_BROTHER) && !is_lord_in_play(LORD_CLARENCE)) + score += 2 + + return score +} + +// Cards that allows automatic success +function automatic_success(lord, score) { + + if (lord_has_capability(lord, AOW_LANCASTER_TWO_ROSES)) + score = 6 + if (game.active === LANCASTER + && is_event_in_play(EVENT_LANCASTER_THE_EARL_OF_RICHMOND) + && game.state === "levy_muster_vassal") + score = 6 + if (game.active === LANCASTER + && game.flags.parliament_votes === 1 + && game.state === "parley") + score = 6 + if (game.active === YORK + && game.flags.jack_cade > 0 + && game.state === "parley") + score = 6 + if (game.active === YORK + && game.flags.succession === 1 + && game.state === "parley") + score = 6 + if (is_campaign_phase() + && game.command === LORD_DEVON + && get_lord_locale(LORD_DEVON) === LOC_EXETER + && is_event_in_play(EVENT_YORK_DORSET) + && game.state === "parley") + score = 6 + + return score +} + +// Initiate influence check with cards influencing the cost overriding all others +// (even automatic at no cost) +function init_influence_check(lord) { + game.check = [] + game.check.push({ cost: 1, modifier: 0, source: "base" }) + game.check.push({ cost: 0, modifier: data.lords[lord].influence, source: "lord" }) + if (game.active === LANCASTER + && is_event_in_play(EVENT_YORK_AN_HONEST_TALE_SPEEDS_BEST) + && game.state === "parley"){ + game.check.push({ cost: 1, modifier: 0, source:"An Honest tale speeds best"}) + } + if (game.active === LANCASTER + && is_event_in_play(EVENT_LANCASTER_PARLIAMENT_VOTES) + && game.flags.parliament_votes === 1 + && game.state === "parley") { + game.check.push({ cost: -1, modifier: 0, source:"Parliament Votes"}) + } + if (game.active === YORK + && is_event_in_play(EVENT_YORK_SUCCESSION) + && game.flags.succession === 1 + && game.state === "parley") { + game.check.push({ cost: -1, modifier: 0, source:"Succession"}) + } +} + +function end_influence_check() { + game.check = 0 +} + +function count_influence_score() { + let score = game.check.reduce((p, c) => p + c.modifier, 0) + + let lord = 0 + if (is_levy_phase()) + lord = game.who + if (is_campaign_phase()) + lord = game.command + + if (score > 5) + score = 5 + if (score < 1) + score = 1 + + score = automatic_success(lord, score) + + return score +} + +function count_influence_cost() { + if (game.state === "parley") { + if (is_campaign_phase()) { + if (game.command === LORD_DEVON && get_lord_locale(LORD_DEVON) === LOC_EXETER && is_event_in_play(EVENT_YORK_DORSET)) + return 0 + } + if (is_levy_phase()) { + if (game.flags.jack_cade > 0) + return 0 + } + } + return game.check.reduce((p, c) => p + c.cost, 0) +} + +function do_influence_check() { + reduce_influence(count_influence_cost()) + let rating = count_influence_score() + let roll = roll_die() + let success + + if (roll === 1 || rating === 6) + success = true + else if (roll === 6) + success = false + else + success = roll <= rating + + // TODO: print log message here instead of returning object + return { success: success, rating: rating, roll: roll } +} + +function add_influence_check_modifier_1() { + game.check.push({ cost: 1, modifier: 1, source: "add" }) +} + +function add_influence_check_modifier_2() { + game.check.push({ cost: 3, modifier: 2, source: "add" }) +} + +function add_influence_check_distance(distance) { + let idx = game.check.findIndex(i => i.source === "distance") + + if (idx !== NOTHING) + game.check.splice(idx, 1) + + game.check.push({ cost: distance, modifier: 0, source: "distance" }) +} + +function prompt_influence_check() { + if (!game.check.some(c => c.source === "add")) { + view.actions.spend1 = 1 + view.actions.spend3 = 1 + } + if (game.where !== NOWHERE) + gen_action_locale(game.where) + view.actions.check = 1 + + view.prompt += `Cost: ${count_influence_cost()} - Range (${range(count_influence_score())})` +} + +// === 2.0 SETUP === + +function goto_setup_lords() { + // setup will be used in some scenarios + end_setup_lords() +} + +states.setup_lords = { + inactive: "Set up Lords", + prompt() { + view.prompt = "Set up your Lords." + let done = true + if (done) { + view.prompt += " All done." + view.actions.end_setup = 1 + } + }, + end_setup() { + end_setup_lords() + }, +} + +function end_setup_lords() { + clear_lords_moved() + set_active_enemy() + if (game.active === P1) { + log_h1("Levy " + current_turn_name()) + goto_levy_arts_of_war_first() + } +} + +// === 3.1 LEVY: ARTS OF WAR (FIRST TURN) === + +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 === YORK) + log_h2("York Arts of War") + else + log_h2("Lancaster 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 + 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.` + } + }, + lord(lord) { + push_undo() + let c = game.what.shift() + log(`${game.active} deployed Capability.`) + add_lord_capability(lord, c) + capability_muster_effects(lord, 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() { + game.what = NOTHING + set_active_enemy() + if (game.active === P2) + goto_levy_arts_of_war_first() + else + goto_levy_muster() +} + +// === 3.1 LEVY: ARTS OF WAR === + +function goto_levy_arts_of_war() { + if (game.active === YORK) + log_h2("York Arts of War") + else + log_h2("Lancaster 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 === YORK) + set_add(game.hand_y, c) + else + set_add(game.hand_l, 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() +} + +// === 3.2 LEVY: PAY === + +function reset_unpaid_lords() { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; lord++) { + if (is_lord_unfed(lord)) { + set_lord_unfed(lord, Math.ceil(count_lord_all_forces(lord) / 6)) + } + } +} + +function goto_pay() { + log_br() + let n = 0 + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + let here = get_lord_locale(lord) + if (is_lord_on_map(lord) && + !is_lord_on_calendar(lord) && + lord_has_capability(lord, AOW_LANCASTER_MADAME_LA_GRANDE) && + (((is_friendly_locale(here)) && data.port_2.includes(here)) || + is_adjacent_friendly_port_english_channel(here))) { + add_lord_assets(lord, COIN, 1) + } + if ( + game.active === LANCASTER && + is_lord_on_map(lord) && + lord_has_capability(LORD_NORTHUMBERLAND_L, AOW_LANCASTER_PERCYS_POWER) && + is_lord_in_north(LORD_NORTHUMBERLAND_L) && + is_lord_in_north(lord) + ) { + set_lord_unfed(lord, 0) + } else { + n = Math.ceil(count_lord_all_forces(lord) / 6) + set_lord_unfed(lord, n) + } + } + game.state = "pay" +} + +states.pay = { + inactive: "Pay", + prompt() { + view.prompt = "Pay: You must Pay your Lord's Troops" + let done = true + + // Pay 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, COIN) > 0) { + gen_action_coin(lord) + done = false + } + } + } + } + + // Sharing + if (done) { + view.prompt = "Pay: You must Pay Lords with Shared Coin." + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (is_lord_unfed(lord) && can_pay_from_shared(lord)) { + gen_action_lord(lord) + done = false + } + } + } + + // Pillage + if (done) { + view.prompt = "Pay: You must Pillage and/or Disband." + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (is_lord_unfed(lord)) { + view.actions.pillage = 1 + done = false + } + } + } + + // All done! + if (done) { + view.prompt = "Pay: All done." + view.actions.end_pay = 1 + } + }, + coin(lord) { + push_undo() + add_lord_assets(lord, COIN, -1) + pay_lord(lord) + }, + lord(lord) { + push_undo() + game.who = lord + game.state = "pay_lord_shared" + }, + pillage() { + push_undo() + reset_unpaid_lords() + goto_pillage_coin() + }, + end_pay() { + end_pay() + }, + card: action_held_event, +} + +states.pay_lord_shared = { + inactive: "Pay", + prompt() { + view.prompt = `Pay: You must Feed ${lord_name[game.who]} with Shared Coin.` + 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, COIN) > 0) + gen_action_coin(lord) + } + } + }, + coin(lord) { + push_undo() + add_lord_assets(lord, COIN, -1) + pay_lord(game.who) + resume_pay_lord_shared() + }, +} + +function resume_pay_lord_shared() { + if (!is_lord_unfed(game.who) || !can_pay_from_shared(game.who)) { + game.who = NOBODY + game.state = "pay" + } +} + +function end_pay() { + game.who = NOBODY + set_active_enemy() + if (game.active === P2) { + goto_pay() + } else + goto_pay_lords() +} + +// === 3.2.1 PAY TROOPS (PILLAGE) === + +function goto_pillage_food() { + push_state("pillage") + game.what = PROV +} + +function goto_pillage_coin() { + push_state("pillage") + game.what = COIN +} + +function can_pillage(loc) { + return !is_exile(loc) && !has_exhausted_marker(loc) +} + +states.pillage = { + inactive: "Pillage", + prompt() { + view.prompt = `Pillage: Pillage the locales where your ${game.what === PROV ? "unfed" : "unpaid"} lords are.` + + let done = true + for (let x = first_friendly_lord; x <= last_friendly_lord; x++) { + if (is_lord_on_map(x) && is_lord_unfed(x) && can_pillage(get_lord_locale(x))) { + gen_action_locale(get_lord_locale(x)) + done = false + } + } + + if (done) { + view.prompt = `Pillage: Unable to Pillage, you must disband your ${game.what === PROV ? "unfed" : "unpaid"} lords.` + for (let x = first_friendly_lord; x <= last_friendly_lord; x++) { + if (is_lord_on_map(x) && is_lord_unfed(x)) { + gen_action_lord(x) + done = false + } + } + } + + if (done) { + view.prompt = `Pillage: Done.` + view.actions.done = 1 + } + }, + locale(loc) { + game.where = loc + game.state = "pillage_locale" + }, + lord(lord) { + disband_influence_penalty(lord) + disband_lord(lord) + }, + done() { + pop_state() + }, +} + +states.pillage_locale = { + inactive: "Pillage", + prompt() { + view.prompt = `Pillage: Choose Lord to Pillage ${data.locales[game.where].name}.` + + for (let x = first_friendly_lord; x <= last_friendly_lord; x++) { + if (get_lord_locale(x) === game.where && is_lord_unfed(x)) { + gen_action_lord(x) + } + } + }, + lord(lord) { + // Pillage + // Same values as Taxing. + let num = get_tax_amount(game.where, lord) + add_lord_assets(lord, COIN, num) + add_lord_assets(lord, PROV, num) + reduce_influence(4 * num) + + add_exhausted_marker(game.where) + set_favour_enemy(game.where) + for (let next of data.locales[game.where].adjacent) + shift_favour_away(next) + + game.state = "pillage" + game.where = NOWHERE + }, +} + +// === 3.2.2 PAY LORDS === + +function has_friendly_lord_who_must_pay_troops() { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) + if (is_lord_unfed(lord)) + return true + return false +} + +function goto_pay_lords() { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; lord++) { + if (is_lord_on_map(lord)) + set_lord_unfed(lord, 1) + } + + if (has_friendly_lord_who_must_pay_troops()) { + game.count = 0 + game.who = NOBODY + game.state = "pay_lords" + } else { + end_pay_lords() + } +} + +function end_pay_lords() { + set_active_enemy() + + if (game.active === P2) + goto_pay_lords() + else + goto_pay_vassals() +} + +states.pay_lords = { + inactive: "Pay Lords", + prompt() { + view.prompt = "Pay Lords in Influence or Disband them." + prompt_held_event() + if (game.who === NOBODY) { + let done = true + for (let lord = first_friendly_lord; lord <= last_friendly_lord; lord++) { + if (is_lord_on_map(lord) && is_lord_unfed(lord)) { + gen_action_lord(lord) + done = false + } + } + if (!done) + view.actions.pay_all = 1 + if (done) + view.actions.done = 1 + } else { + view.actions.disband = 1 + view.actions.pay = 1 + } + }, + lord(lord) { + push_undo() + game.who = lord + }, + disband() { + disband_lord(game.who) + game.who = NOBODY + }, + pay() { + reduce_influence(is_exile(get_lord_locale(game.who)) ? 2 : 1) + set_lord_moved(game.who, 0) + game.who = NOBODY + }, + pay_all() { + push_undo() + for (let lord = first_friendly_lord; lord <= last_friendly_lord; lord++) { + if (is_lord_on_map(lord) && is_lord_unfed(lord)) { + reduce_influence(is_exile(get_lord_locale(lord)) ? 2 : 1) + set_lord_moved(lord, 0) + } + } + }, + card: action_held_event, + done() { + end_pay_lords() + }, +} + +// === 3.2.3 PAY VASSALS === + +function goto_pay_vassals() { + let vassal_to_pay = false + + for (let v = first_vassal; v <= last_vassal; v++) { + if ( + is_vassal_mustered_with_friendly_lord(v) && + get_vassal_service(v) === current_turn() + ) { + vassal_to_pay = true + } + } + if (vassal_to_pay) { + game.state = "pay_vassals" + game.what = NOBODY + } else { + end_pay_vassals() + } +} + +function end_pay_vassals() { + set_active_enemy() + + if (game.active === P1) { + goto_muster_exiles() + } else { + goto_pay_vassals() + } +} + +states.pay_vassals = { + inactive: "Pay Vassals", + prompt() { + let done = true + view.prompt = "You may pay or disband vassals in the next calendar box." + if (game.what === NOBODY) { + for (let v = first_vassal; v <= last_vassal; v++) { + if ( + is_vassal_mustered_with_friendly_lord(v) && + get_vassal_service(v) === current_turn() + ) { + gen_action_vassal(v) + done = false + } + } + + if (done) { + view.actions.done = 1 + } + if (!done) + view.actions.pay_all = 1 + } else { + view.actions.pay = 1 + view.actions.disband = 1 + } + }, + vassal(v) { + push_undo() + game.what = v + }, + pay() { + push_undo() + pay_vassal(game.what) + reduce_influence(1) + game.what = NOBODY + }, + pay_all() { + push_undo() + for (let v = first_vassal; v <= last_vassal; v++) { + if (is_vassal_mustered_with_friendly_lord(v) + && get_vassal_service(v) === current_turn()) { + pay_vassal(v) + reduce_influence(1) + game.what = NOBODY + } + } + }, + disband() { + push_undo() + disband_vassal(game.what) + game.what = NOBODY + }, + done() { + end_pay_vassals() + }, +} + +// === 3.2.4 DISBAND === + +function disband_lord(lord, permanently = false) { + let turn = current_turn() + let extra = 6 + + if (permanently) { + log(`Removed L${lord}.`) + set_lord_locale(lord, NOWHERE) + } else if (lord_has_capability(lord, AOW_YORK_ENGLAND_IS_MY_HOME)) { + set_lord_calendar(lord, turn + (extra - data.lords[lord].influence)) + log(`Disbanded L${lord} to turn ${current_turn() + 1}.`) + } + else { + set_lord_calendar(lord, turn + (extra - data.lords[lord].influence)) + log(`Disbanded L${lord} to turn ${get_lord_calendar(lord)}.`) + } + + discard_lord_capability_n(lord, 0) + discard_lord_capability_n(lord, 1) + + for (let x = 0; x < ASSET_TYPE_COUNT; ++x) + set_lord_assets(lord, x, 0) + + for (let x = 0; x < FORCE_TYPE_COUNT; ++x) { + set_lord_forces(lord, x, 0) + if (get_lord_routed_forces(lord, x) > 0) { + set_lord_routed_forces(lord, x, 0) + } + } + + set_lord_moved(lord, 0) + + for_each_vassal_with_lord(lord, v => { + disband_vassal(v) + }) +} + +// === 3.3.1 MUSTER EXILES === + +function goto_muster_exiles() { + for (let x = first_friendly_lord; x <= last_friendly_lord; x++) { + if ((get_lord_locale(x) === current_turn() + CALENDAR && get_lord_in_exile(x)) + || (is_lancaster_lord(x) && is_lord_on_calendar((get_lord_locale(x)) && get_lord_in_exile(x) && is_event_in_play(EVENT_LANCASTER_BE_SENT_FOR)))) { + game.state = "muster_exiles" + return + } + } + end_muster_exiles() +} + +function end_muster_exiles() { + set_active_enemy() + + if (game.active === P1) { + if (!check_disband_victory()) { + goto_ready_vassals() + } + } else { + goto_muster_exiles() + } +} + +states.muster_exiles = { + inactive: "Muster Exiles", + prompt() { + view.prompt = "Muster Exiled Lords." + let done = true + + if (game.who === NOBODY) { + for (let x = first_friendly_lord; x <= last_friendly_lord; x++) { + if ((get_lord_locale(x) === current_turn() + CALENDAR && get_lord_in_exile(x)) + || (is_lancaster_lord(x) && is_lord_on_calendar((get_lord_locale(x)) && get_lord_in_exile(x) && is_event_in_play(EVENT_LANCASTER_BE_SENT_FOR)))) { + gen_action_lord(x) + done = false + } + } + } else { + for (let loc of data.exile_boxes) + if (has_favour_in_locale(game.active, loc)) + gen_action_locale(loc) + } + + if (done) { + view.actions.done = true + } + }, + lord(lord) { + game.who = lord + }, + locale(loc) { + muster_lord_in_exile(game.who, loc) + game.who = NOBODY + }, + done() { + end_muster_exiles() + }, +} + +function muster_lord_in_exile(lord, exile_box) { + remove_lord_from_exile(lord) + muster_lord(lord, exile_box) +} + +// === 3.3.2 READY VASSALS === + +function goto_ready_vassals() { + for (let vassal = first_vassal; vassal <= last_vassal; vassal++) { + if (get_vassal_service(vassal) === current_turn()) { + set_vassal_lord_and_service(vassal, VASSAL_READY, 0) + } + } + + goto_levy_muster() +} + +// === 3.4 MUSTER === + +function reset_flags_for_muster() { + // to avoid some flags affecting campaign + game.flags.jack_cade = 0 + game.flags.parliament_votes = 0 +} + +function goto_levy_muster() { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + clear_lords_moved() + // additionnal free specific actions + if (lord_has_capability(lord, AOW_LANCASTER_THOMAS_STANLEY)) + game.flags.free_levy = 1 + if (is_event_in_play(EVENT_LANCASTER_MY_CROWN_IS_IN_MY_HEART)) + game.flags.free_parley_henry = 2 + if (is_event_in_play(EVENT_YORK_GLOUCESTER_AS_HEIR)) + game.flags.free_parley_gloucester = 3 + if (is_event_in_play(EVENT_YORK_LOYALTY_AND_TRUST)) + game.flags.loyalty_and_trust = 1 + } + if (game.active === YORK) + log_h2("York Muster") + else + log_h2("Lancaster 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_discard_events() +} + +function can_lord_muster(lord) { + // already mustered (except free levy)! TODO : re-check parley henry if ships are levied and at exile + if (get_lord_moved(lord) + && (game.flags.free_levy !== 1 || lord !== LORD_HENRY_TUDOR) + && (game.flags.free_parley_henry === 0 || lord !== LORD_HENRY_VI || (lord === LORD_HENRY_VI && !can_action_parley_levy())) + && (game.flags.free_parley_gloucester === 0 || (lord !== LORD_GLOUCESTER_1 || lord !== LORD_GLOUCESTER_2))) + return false + + // must be on map + if (is_lord_on_map(lord)) { + // can use lordship + if (is_lord_at_friendly_locale(lord)) + return true + // can only parley + if (can_parley_at(get_lord_locale(lord))) + return true + } + return false +} + +function has_locale_to_muster(lord) { + // Can muster at own seat without enemy lord. + if (!has_enemy_lord(data.lords[lord].seat)) + return true + + // Else, can muster at any friendly seat (of a friendly lord who is also in play) + for (let other = first_friendly_lord; other <= last_friendly_lord; other++) + if (is_lord_in_play(other) && is_friendly_locale(data.lords[other].seat)) + return true + + // Tough luck! + return false +} + +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 (can_lord_muster(lord)) { + gen_action_lord(lord) + done = false // ??? + } + } + if (done) { + view.prompt += "" + 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 + reset_flags_for_muster() + lordship_effects(lord) + }, + end_muster() { + end_levy_muster() + }, + + card: action_held_event, +} + +function resume_levy_muster_lord() { + game.state = "levy_muster_lord" + --game.count + + // muster over only if the lord has not spend their free levy actions + if (game.count === 0 && game.flags.jack_cade === 0 && game.flags.free_levy === 0 && can_add_troops(game.who, get_lord_locale(game.who))) { + 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.` + + let here = get_lord_locale(game.who) + + if (is_friendly_locale(here)) { + if (game.count > 0) { + // Roll to muster Ready Lord at Seat + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (is_lord_ready(lord) && has_locale_to_muster(lord)) + gen_action_lord(lord) + } + + // Muster Ready Vassal Forces + for (let vassal = first_vassal; vassal <= last_vassal; vassal++) + if (eligible_vassal(vassal)) + gen_action_vassal(vassal) + + // Add Transport + if (is_seaport(here) && get_lord_assets(game.who, SHIP) < 2) + view.actions.take_ship = 1 + + if (can_add_transport(game.who, CART)) + view.actions.take_cart = 1 + + if (can_add_troops(game.who, here)) + view.actions.levy_troops = 1 + + // Add Capability + if (can_add_lord_capability(game.who)) + view.actions.capability = 1 + + if (can_action_parley_levy()) + view.actions.parley = 1 + + if (can_add_troops_beloved_warwick(game.who, here)) + view.actions.levy_beloved_warwick = 1 + + if (can_add_troops_irishmen(game.who, here)) + view.actions.levy_irishmen = 1 + + if (can_add_troops_sof(game.who, here)) + view.actions.soldiers_of_fortune = 1 + + if (can_add_troops_coa(game.who, here)) + view.actions.commission_of_array = 1 + } + + if (game.count === 0 && lord_has_capability(game.who, AOW_LANCASTER_THOMAS_STANLEY) && can_add_troops(game.who, here)) { + view.actions.levy_troops = 1 + } + // Rising wages event + if (is_event_in_play(EVENT_LANCASTER_RISING_WAGES) && !can_pay_from_shared(game.who)) { + view.actions.levy_troops = 0 + } + if (game.count === 0 && game.flags.free_parley_henry > 0 && game.who === LORD_HENRY_VI) { + view.actions.parley = 1 + } + if (game.count === 0 && game.flags.free_parley_gloucester > 0 && (game.who === LORD_GLOUCESTER_2 || game.who === LORD_GLOUCESTER_1)) { + view.actions.parley = 1 + } + if (game.count === 0 && game.flags.jack_cade > 0) { + view.actions.parley = 1 + } + + } else { + // Can only Parley if locale is not friendly. + if (game.count > 0) { + if (can_action_parley_levy()) + view.actions.parley = 1 + } + } + + if (is_event_in_play(EVENT_YORK_LOYALTY_AND_TRUST) && game.flags.loyalty_and_trust) { + view.actions.loyalty_and_trust = 1 + } + + view.actions.done = 1 + }, + + lord(other) { + push_undo() + goto_levy_muster_lord_attempt(other) + }, + + vassal(vassal) { + push_undo() + game.which = vassal + goto_levy_muster_vassal(vassal) + }, + + take_ship() { + push_undo() + if (check_naval_blockade("levy ship", get_lord_locale(game.who))) { + roll_naval_blockade() + } + else { + push_the_kings_name() + add_lord_assets(game.who, SHIP, 1) + goto_the_kings_name("Levy Ship") + } + }, + + take_cart() { + push_undo() + push_the_kings_name() + add_lord_assets(game.who, CART, 2) + goto_the_kings_name("Levy Cart") + }, + + levy_troops() { + push_undo() + push_the_kings_name() + + if (is_event_in_play(EVENT_LANCASTER_RISING_WAGES) && game.active === YORK) { + goto_rising_wages() + return + } + + do_levy_troops() + }, + + levy_beloved_warwick() { + push_undo() + push_the_kings_name() + add_lord_forces(game.who, MILITIA, 5) + goto_the_kings_name("Beloved Warwick") + }, + + levy_irishmen() { + push_undo() + push_the_kings_name() + add_lord_forces(game.who, MILITIA, 5) + goto_the_kings_name("Irishmen") + }, + + soldiers_of_fortune() { + push_undo() + push_the_kings_name() + set_lord_unfed(game.who, 1) + push_state("soldiers_of_fortune") + }, + + commission_of_array() { + push_undo() + push_the_kings_name() + game.state = "commission_of_array" + }, + + capability() { + push_undo() + push_the_kings_name() + game.state = "muster_capability" + }, + + parley() { + push_undo() + push_the_kings_name() + goto_parley() + }, + + loyalty_and_trust() { + push_undo() + game.count += 3 + game.flags.loyalty_and_trust = 0 + }, + + done() { + set_lord_moved(game.who, 1) + pop_state() + }, +} + +// Check if the levy troops is at vassal seat +function chamberlains_eligible_levy(locale) { + for (let vassal = first_vassal; vassal <= last_vassal; ++vassal) + if (is_vassal_mustered_with(vassal, game.who) && lord_has_capability(game.who, AOW_LANCASTER_CHAMBERLAINS)) { + if (locale === data.vassals[vassal].seat) + return true + } +} + +function do_levy_troops() { + let here = get_lord_locale(game.who) + if ( + !lord_has_capability(game.who, AOW_LANCASTER_QUARTERMASTERS) && + !lord_has_capability(game.who, AOW_YORK_WOODWILLES) && + !chamberlains_eligible_levy(here) + ) + deplete_locale(here) + + let here_type = data.locales[here].type + switch (here_type) { + case "calais": + add_lord_forces(game.who, MEN_AT_ARMS, 2) + add_lord_forces(game.who, LONGBOWMEN, 1) + break + case "london": + add_lord_forces(game.who, MEN_AT_ARMS, 1) + add_lord_forces(game.who, LONGBOWMEN, 1) + add_lord_forces(game.who, MILITIA, 1) + break + case "harlech": + add_lord_forces(game.who, MEN_AT_ARMS, 1) + add_lord_forces(game.who, LONGBOWMEN, 2) + break + case "city": + add_lord_forces(game.who, LONGBOWMEN, 1) + add_lord_forces(game.who, MILITIA, 1) + break + case "town": + add_lord_forces(game.who, MILITIA, 2) + break + case "fortress": + add_lord_forces(game.who, MEN_AT_ARMS, 1) + add_lord_forces(game.who, MILITIA, 1) + break + } + if (game.flags.free_levy === 1) { + ++game.count + game.flags.free_levy = 0 + } + + if (is_event_in_play(EVENT_YORK_THE_COMMONS) && is_york_lord(game.who)) { + goto_the_commons() + } else { + goto_the_kings_name("Levy Troops") + } +} + +// === 3.4.2 LEVY LORD === + +function goto_levy_muster_lord_attempt(lord) { + game.what = lord + push_state("levy_muster_lord_attempt") + init_influence_check(game.who) +} + +states.levy_muster_lord_attempt = { + inactive: "Levy Lord", + prompt() { + view.prompt = `Levy Lord ${lord_name[game.what]}. ` + + prompt_influence_check() + }, + spend1: add_influence_check_modifier_1, + spend3: add_influence_check_modifier_2, + check() { + let results = do_influence_check() + end_influence_check() + + log(`Attempt to levy L${game.what} ${results.success ? "Successful" : "Failed"}: (${range(results.rating)}) ${results.success ? HIT[results.roll] : MISS[results.roll]}`) + + if (results.success) { + game.who = game.what + pop_state() + push_state("muster_lord_at_seat") + } + else { + pop_state() + resume_levy_muster_lord() + } + }, +} + +states.muster_lord_at_seat = { + inactive: "Muster", + prompt() { + view.prompt = `Muster: Select Locale for ${lord_name[game.who]}.` + let found = false + let seat = data.lords[game.who].seat + if (!has_enemy_lord(seat)) { + gen_action_locale(seat) + found = true + } + + if (!found) { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; lord++) { + if ((is_lord_on_map(lord) || is_lord_on_calendar(lord)) && is_friendly_locale(data.lords[lord].seat)) { + gen_action_locale(data.lords[lord].seat) + } + } + } + }, + locale(loc) { + push_undo() + + set_lord_moved(game.who, 1) + muster_lord(game.who, loc) + if (game.active === YORK) { + add_favoury_marker(loc) + remove_favourl_marker(loc) + } else { + add_favourl_marker(loc) + remove_favoury_marker(loc) + } + + pop_state() + goto_the_kings_name("Levy Lord") + }, +} + +// === 3.4.3 LEVY VASSAL === + +function eligible_vassal(vassal) { + if (!is_vassal_ready(vassal)) { + return false + } + if ( + !is_favour_friendly(data.vassals[vassal].seat) && + (game.who !== LORD_HENRY_TUDOR || !is_event_in_play(EVENT_LANCASTER_MARGARET_BEAUFORT)) + ) { + return false + } + if (!is_favour_friendly(data.vassals[vassal].seat)) + return false + if ( + game.active === LANCASTER && + is_event_in_play(EVENT_YORK_YORKISTS_BLOCK_PARLIAMENT) && + !(is_event_in_play(EVENT_LANCASTER_MARGARET_BEAUFORT) && !is_event_in_play(EVENT_LANCASTER_THE_EARL_OF_RICHMOND)) + ) { + return false + } + return true +} + +function goto_levy_muster_vassal(vassal) { + game.where = NOWHERE + let influence_cost = 0 + if (game.active === YORK && is_event_in_play(EVENT_LANCASTER_BUCKINGHAMS_PLOT)) + influence_cost += 2 + + push_state("levy_muster_vassal") + init_influence_check(game.who) + game.check.push({ + cost: influence_cost, + modifier: data.vassals[vassal].influence * (game.active === LANCASTER ? -1 : 1), + source: "vassal", + }) +} + +states.levy_muster_vassal = { + inactive: "Levy Vassal", + prompt() { + view.prompt = `Levy Vassal ${data.vassals[game.which].name}. ` + prompt_influence_check() + }, + spend1: add_influence_check_modifier_1, + spend3: add_influence_check_modifier_2, + check() { + let results = do_influence_check() + end_influence_check() + + if (lord_has_capability(game.who, AOW_LANCASTER_TWO_ROSES)) { + log(`Automatic Success. C${AOW_LANCASTER_TWO_ROSES}.`) + } + else if (game.active === LANCASTER && is_event_in_play(EVENT_LANCASTER_THE_EARL_OF_RICHMOND) && game.state === "levy_muster_vassal") { + log(`Automatic Success. C${EVENT_LANCASTER_THE_EARL_OF_RICHMOND}.`) + } + else { + log(`Attempt to levy V${game.which} ${results.success ? "Successful" : "Failed"}: (${range(results.rating)}) ${results.success ? HIT[results.roll] : MISS[results.roll]}`) + } + + if (results.success) { + muster_vassal(game.which, game.who) + pop_state() + goto_the_kings_name("Levy Vassal") + } else { + pop_state() + resume_levy_muster_lord() + } + }, +} + +// === 3.4.4 LEVY TROOPS === + +function can_add_troops(_lordwho, locale) { + if (!has_exhausted_marker(locale) && !is_exile(locale)) + return true + return false +} + +function can_add_troops_coa(lordwho, locale) { + for (let next of data.locales[locale].adjacent) { + if (is_friendly_locale(next) && lord_has_capability(lordwho, AOW_LANCASTER_COMMISION_OF_ARRAY) && (!has_exhausted_marker(locale) && !is_exile(locale))) + return true + } + return false +} + +function can_add_troops_beloved_warwick(lordwho, locale) { + return ( + lord_has_capability(lordwho, AOW_YORK_BELOVED_WARWICK) && + !has_exhausted_marker(locale) && + !is_exile(locale) + ) +} + +function can_add_troops_irishmen(lordwho, locale) { + return ( + lord_has_capability(lordwho, AOW_YORK_IRISHMEN) && + !has_exhausted_marker(locale) && + (locale === LOC_IRELAND || data.port_3.includes(locale)) + ) +} + +function can_add_troops_sof(lordwho, locale) { + if ( + lord_has_capability(lordwho, AOW_YORK_SOLDIERS_OF_FORTUNE) && + !has_exhausted_marker(locale) && + !is_exile(locale) && + get_shared_assets(locale, COIN) > 0 + ) { + let number = 6 + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) + number -= get_lord_forces(lord, MERCENARIES) + if (number >= 1) + return true + } + return false +} + +// === 3.4.5 LEVY TRANSPORT + +function can_add_transport(who, what) { + return get_lord_assets(who, what) < 100 +} + +// === 3.4.6 LEVY CAPABILITY === + +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 forbidden_levy_capabilities(c) { + // Some capabilities override the forbidden levy vassals + if (lord_has_capability(game.who, AOW_LANCASTER_TWO_ROSES)) { + if (c === AOW_LANCASTER_THOMAS_STANLEY || AOW_LANCASTER_MY_FATHERS_BLOOD) { + return true + } + } + // Forbids levy vassals, even through capabilities + if (is_event_in_play(EVENT_YORK_YORKISTS_BLOCK_PARLIAMENT)) { + if (c === AOW_LANCASTER_THOMAS_STANLEY + || c === AOW_LANCASTER_EDWARD + || c === AOW_LANCASTER_MONTAGU + || c === AOW_LANCASTER_MY_FATHERS_BLOOD + || c === AOW_LANCASTER_ANDREW_TROLLOPE) { + return false + } + } + return true +} + +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") +} + +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 (!data.cards[c].lords || set_has(data.cards[c].lords, game.who)) { + if (!lord_already_has_capability(game.who, c) && forbidden_levy_capabilities(c)) + gen_action_card(c) + } + } + }, + card(c) { + add_lord_capability(game.who, c) + capability_muster_effects(game.who, c) + goto_the_kings_name("Capability C${c}") + }, +} + +// === 3.4 MUSTER - DISCARD EVENTS === + +function goto_levy_discard_events() { + // Discard "This Levy" events from play. + discard_events("this_levy") + discard_extra_levy_events() + goto_campaign_plan() +} + +// === 4.1 CAMPAIGN: PLAN === + +function goto_campaign_plan() { + game.turn++ + + log_h1("Campaign " + current_turn_name()) + + set_active(BOTH) + game.state = "campaign_plan" + game.plan_y = [] + game.plan_l = [] +} + +states.campaign_plan = { + inactive: "Plan", + prompt(current) { + let plan = current === YORK ? game.plan_y : game.plan_l + let first = current === YORK ? first_york_lord : first_lancaster_lord + let last = current === YORK ? last_york_lord : last_lancaster_lord + view.plan = plan + view.actions.plan = [] + + if (plan.length === max_plan_length()) + view.prompt = "Plan: All done." + else + view.prompt = "Plan: Build a Plan." + + if (plan.length < max_plan_length()) { + view.actions.end_plan = 0 + if (count_cards_in_plan(plan, NOBODY) < 7) + 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 + } + + if (plan.length > 0) + view.actions.undo = 1 + else + view.actions.undo = 0 + }, + plan(lord, current) { + if (current === YORK) + game.plan_y.push(lord) + else + game.plan_l.push(lord) + }, + undo(_, current) { + if (current === YORK) { + game.plan_y.pop() + } else { + game.plan_l.pop() + } + }, + end_plan(_, current) { + if (game.active === BOTH) { + if (current === YORK) + set_active(LANCASTER) + else + set_active(YORK) + } else { + end_campaign_plan() + } + }, +} + +function end_campaign_plan() { + set_active(P1) + goto_command_activation() +} + +// === 4.2 CAMPAIGN: COMMAND === + +// First action vs actions that take full command card +function is_first_action() { + return game.flags.first_action +} + +// If march on a highway, set the flag so the lord can go through +// a second highway at no cost +function is_first_march_highway() { + if (game.flags.first_march_highway === 1) + return true + else + return false +} + +function goto_command_activation() { + if (game.plan_y.length === 0 && game.plan_l.length === 0) { + game.command = NOBODY + goto_end_campaign() + return + } + + if (check_campaign_victory()) + return + + if (game.plan_l.length > game.plan_y.length) { + set_active(LANCASTER) + game.command = game.plan_l.shift() + } else if (game.plan_l.length < game.plan_y.length) { + set_active(YORK) + game.command = game.plan_y.shift() + } else { + set_active(P1) + if (P1 === LANCASTER) + game.command = game.plan_l.shift() + else + game.command = game.plan_y.shift() + } + + if (game.command === NOBODY) { + log_h2("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() + } +} + +function goto_command() { + game.actions = data.lords[game.command].command + if (lord_has_capability(game.command, AOW_YORK_THOMAS_BOURCHIER) && is_city(get_lord_locale(game.command))) + game.actions += 1 + if (lord_has_capability(game.command, AOW_YORK_YORKS_FAVOURED_SON)) + game.actions += 1 + if (lord_has_capability(game.command, AOW_YORK_HASTINGS)) + game.actions += 1 + + game.group = [ game.command ] + + game.flags.surprise_landing = 0 + game.flags.first_action = 1 + game.flags.first_march_highway = 0 + game.flags.march_to_port = 0 + game.flags.sail_to_port = 0 + + resume_command() +} + +function resume_command() { + game.state = "command" +} + +// Spending an action reset some flags +function spend_action(cost) { + game.flags.surprise_landing = 0 + game.flags.first_action = 0 + game.flags.first_march_highway = 0 + game.actions -= cost +} + +function spend_march_action(cost) { + game.flags.surprise_landing = 0 + game.flags.first_action = 0 + game.flags.first_march_highway = 0 + game.actions -= cost +} + +function spend_all_actions() { + game.flags.surprise_landing = 0 + game.flags.first_action = 0 + game.flags.first_march_highway = 0 + game.actions = 0 +} + +function end_command() { + log_br() + + game.group = 0 + game.flags.first_action = 0 + game.flags.first_march_highway = 0 + game.flags.famine = 0 + + // NOTE: Feed currently acting side first for expedience. + set_active_command() + goto_feed() +} + +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) || + (lord_has_capability(game.command, AOW_YORK_CAPTAIN) && !other_marshal_or_lieutenant(here)) + ) { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) + if (lord !== game.command) + if (get_lord_locale(lord) === here) + gen_action_lord(lord) + } + + // Lieutenant may not take marshall + if (is_lieutenant(game.command)) { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) + if (lord !== game.command) + if (get_lord_locale(lord) === here && !is_marshal(lord)) { + 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_forage()) + view.actions.forage = 1 + if (can_action_tax()) + view.actions.tax = 1 + if (can_action_sail()) + view.actions.sail = 1 + if (can_action_parley_command()) + view.actions.parley = 1 + if (can_action_heralds()) + view.actions.heralds = 1 + if (can_action_merchants()) + view.actions.merchants = 1 + if (can_action_agitators()) + view.actions.agitators = 1 + + if (is_york_lord(game.command)) + if (can_action_exile_pact()) + view.actions.exile_pact = 1 + }, + + pass() { + push_undo() + log("Passed.") + spend_all_actions() + }, + + end_command() { + push_undo() + end_command() + }, + + forage: goto_forage, + supply: goto_supply, + tax: goto_tax, + sail: goto_sail, + heralds: goto_heralds, + merchants: goto_merchants, + agitators: goto_agitators, + exile_pact: goto_exile_pact, + + locale: goto_march, + + lord(lord) { + set_toggle(game.group, lord) + }, + + card: action_held_event, + + parley() { + push_undo() + goto_parley() + }, +} + +// === 4.5 ACTION: SUPPLY (SEARCH) === + +function can_supply_at(loc, ships) { + // if theoretically possible to supply (does not check carts or ships) + if (is_stronghold(loc) && is_friendly_locale(loc)) { + if (ships > 0 && is_seaport(loc)) + return true + if (!has_exhausted_marker(loc)) + return true + } + return false +} + +function search_supply_by_way(result, start, carts, ships) { + search_dist.fill(0) + search_seen.fill(0) + search_seen[start] = 1 + + let queue = [ start ] + while (queue.length > 0) { + let here = queue.shift() + let dist = search_dist[here] + + if (can_supply_at(here, ships)) { + if (result) + map_set(result, here, dist) + else + return true + } + + if (is_friendly_locale(here)) { + let next_dist = dist + 1 + if (next_dist <= carts) { + for (let next of data.locales[here].adjacent) { + if (!search_seen[next]) { + search_seen[next] = 1 + search_dist[next] = next_dist + queue.push(next) + } + } + } + } + } + + if (result) + return result + return false +} + +function search_supply_by_sea(result, here) { + // Search via sea from Exile box. + if (is_friendly_locale(here)) { + for (let next of find_ports(here)) { + if (can_supply_at(next, 1)) { + if (result) + map_set(result, next, 0) + else + return true + } + } + } + if (result) + return result + return false +} + +function search_supply(result) { + let here = get_lord_locale(game.command) + let carts = get_shared_assets(here, CART) + let ships = get_shared_assets(here, SHIP) + if (ships > 0 && is_exile(here)) + result = search_supply_by_sea(result, here) + result = search_supply_by_way(result, here, carts, ships) + return result +} + +// === 4.5 ACTION: SUPPLY === + +function command_has_harbingers() { + return ( + lord_has_capability(game.command, AOW_LANCASTER_HARBINGERS) || + lord_has_capability(game.command, AOW_YORK_HARBINGERS) + ) +} + +function chamberlains_eligible_supply(source) { + for (let vassal = first_vassal; vassal <= last_vassal; ++vassal) + if ( + is_vassal_mustered_with(vassal, game.command) && + lord_has_capability(game.command, AOW_LANCASTER_CHAMBERLAINS) + ) { + if (source === data.vassals[vassal].seat) + return true + } +} + +function lord_has_stafford_branch(loc, lord) { + if (lord_has_capability(lord, AOW_YORK_STAFFORD_BRANCH)) { + return ( + loc === LOC_EXETER || + loc === LOC_LAUNCESTON || + loc === LOC_PLYMOUTH || + loc === LOC_WELLS || + loc === LOC_DORCHESTER + ) + } + return false +} + +function init_supply() { + game.supply = search_supply([]) +} + +function can_action_supply() { + if (game.actions < 1) + return false + return search_supply(false) +} + +function goto_supply() { + push_undo() + log(`Supplied`) + game.state = "supply_source" + init_supply() +} + +function modify_supply(loc, supply) { + let here = get_lord_locale(game.command) + let carts = get_shared_assets(here, CART) + + // Must carry supply over land with one cart per provender per way + let distance = map_get(game.supply, loc, 0) + if (distance > 0) + supply = Math.min(supply, Math.floor(carts / distance)) + + // Harbingers event doubles supply received + if (command_has_harbingers()) + supply = supply * 2 + + return supply +} + +function get_port_supply_amount(loc) { + if (is_seaport(loc)) { + let here = get_lord_locale(game.command) + let ships = get_shared_assets(here, SHIP) + return modify_supply(loc, ships) + } + return 0 +} + +function get_stronghold_supply_amount(loc) { + if (!has_exhausted_marker(loc)) { + let supply + + if (loc === LOC_LONDON || loc === LOC_CALAIS) + supply = 3 + else if (is_city(loc)) + supply = 2 + else + supply = 1 + + if (lord_has_stafford_branch(loc, game.command)) + supply += 1 + + return modify_supply(loc, supply) + } + return 0 +} + +states.supply_source = { + inactive: "Supply", + prompt() { + view.prompt = "Supply: Select Supply Source." + + let here = get_lord_locale(game.command) + let carts = get_shared_assets(here, CART) + let ships = get_shared_assets(here, SHIP) + + if (carts > 0) + view.prompt += ` ${carts} Cart.` + if (ships > 0) + view.prompt += ` ${ships} Ship.` + + for (let i = 0; i < game.supply.length; i += 2) + gen_action_locale(game.supply[i]) + }, + locale(loc) { + push_undo() + + let port_supply = get_port_supply_amount(loc) + let stronghold_supply = get_stronghold_supply_amount(loc) + + if (stronghold_supply > 0 && port_supply === 0) { + use_stronghold_supply(loc, stronghold_supply) + return + } + + if (port_supply > 0 && stronghold_supply === 0) { + use_port_supply(loc, port_supply) + return + } + + game.where = loc + game.state = "select_supply_type" + }, +} + +function quartermasters_eligible_supply(source) { + for (let vassal = first_vassal; vassal <= last_vassal; ++vassal) + if ( + is_vassal_mustered_with(vassal, game.command) && + lord_has_capability(game.command, AOW_LANCASTER_CHAMBERLAINS) + ) { + if (source === data.vassals[vassal].seat) + return true + } +} + +function use_stronghold_supply(source, amount) { + logi(`${amount} from Stronghold at %${source}`) + add_lord_assets(game.command, PROV, amount) + if (chamberlains_eligible_supply(source)) { + end_supply() + } + else { + deplete_locale(source) + end_supply() + } +} + +function use_port_supply(source, amount) { + logi(`${amount} from Port at %${source}`) + add_lord_assets(game.command, PROV, amount) + end_supply() +} + +function end_supply() { + spend_action(1) + resume_command() + game.supply = 0 + game.where = NOWHERE +} + +states.select_supply_type = { + inactive: "Supply", + prompt() { + let port = get_port_supply_amount(game.where) + let stronghold = get_stronghold_supply_amount(game.where) + + view.prompt = `Supply: ${stronghold} from Stronghold or ${port} from Port?` + view.actions.stronghold = 1 + view.actions.port = 1 + }, + stronghold() { + use_stronghold_supply(game.where, get_stronghold_supply_amount(game.where)) + }, + port() { + if (check_naval_blockade("supply", game.where)) { + roll_naval_blockade() + } + else { + use_port_supply(game.where, get_port_supply_amount(game.where)) + } + }, +} + +// === 4.6.1 ACTION: SAIL === + +function has_enough_available_ships_for_army() { + let ships = count_group_ships() + let army = count_lord_all_forces(game.group) + let needed_ships = army / 6 + return needed_ships <= ships +} + +function is_seamanship_in_play() { + if (game.active === LANCASTER && is_event_in_play(EVENT_LANCASTER_SEAMANSHIP)) + return true + if (game.active === YORK && is_event_in_play(EVENT_YORK_SEAMANSHIP)) + return true + return false +} + +function can_sail_to(to) { + if (is_wales_forbidden(to)) + return false + if (has_enemy_lord(to)) { + if (is_truce_in_effect()) + return false + if (!lord_has_capability(game.command, AOW_LANCASTER_HIGH_ADMIRAL)) + return false + } + return true +} + +function can_action_sail() { + // Must use whole action except if seamanship in play + + if (is_lancaster_lord(game.command)) { + if (!is_first_action() && !is_event_in_play(EVENT_LANCASTER_SEAMANSHIP)) + return false + } + + if (is_york_lord(game.command)) { + if ((is_event_in_play(EVENT_LANCASTER_FRENCH_FLEET) || !is_first_action() && !is_event_in_play(EVENT_YORK_SEAMANSHIP))) + return false + } + + if (game.actions === 0) + return false + + // at a seaport + let here = get_lord_locale(game.command) + if (!is_seaport(here)) + return false + + // with enough ships to carry all the army + if (!has_enough_available_ships_for_army()) + return false + + // and a valid destination + for (let to of find_sail_locales(here)) { + if (to === here) + continue + if (can_sail_to(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 prov = count_group_assets(PROV) + let cart = count_group_assets(CART) + + let overflow_prov = (prov / 2 - ships) * 2 + let overflow_cart = (cart / 2 - ships) * 2 + + if (overflow_prov <= 0 && overflow_cart <= 0) { + view.prompt = `Sail: Select a destination Port.` + for (let to of find_sail_locales(here)) { + if (to === here) + continue + if (can_sail_to(to)) + gen_action_locale(to) + } + } else if (overflow_cart > 0) { + view.prompt = `Sailing with ${ships} Ships. Please discard ${overflow_cart} Cart` + if (cart > 0) { + for (let lord of game.group) { + if (get_lord_assets(lord, CART) > 0) + gen_action_cart(lord) + } + } + } else if (overflow_prov > 0) { + view.prompt = `Sailing with ${ships} Ships. Please discard ${overflow_prov} Provender` + if (prov > 0) { + for (let lord of game.group) { + if (get_lord_assets(lord, PROV) > 0) + gen_action_prov(lord) + } + } + } else { + view.prompt = "ERROR" + } + }, + prov: drop_prov, + cart: drop_cart, + locale(to) { + if (check_naval_blockade("sail", get_lord_locale(game.command)) || check_naval_blockade("sail", to)) { + roll_naval_blockade() + game.where = to + } + else { + do_sail(to) + } + }, +} + +function do_sail(to) { + log(`Sailed to %${to}${format_group_move()}.`) + + game.flags.march_to_port = 0 + if (is_seaport(to)) + game.flags.sail_to_port = 1 + else + game.flags.sail_to_port = 0 + + for (let lord of game.group) { + set_lord_locale(lord, to) + set_lord_moved(lord, 1) + levy_burgundians(lord) + } + + if (is_seamanship_in_play()) + spend_action(1) + else + spend_all_actions() + + // you can go to unbesieged enemy lord with norfolk capability + if (has_unbesieged_enemy_lord(to)) + goto_confirm_approach_sail() + else { + resume_command() + } +} + +function goto_confirm_approach_sail() { + game.state = "confirm_approach_sail" +} + +states.confirm_approach_sail = { + inactive: "Sail", + prompt() { + view.prompt = `Sail: Confirm Approach to enemy Lord.` + view.group = game.group + view.actions.approach = 1 + }, + approach() { + push_undo() + goto_battle() + }, +} + +// === 4.6.2 ACTION: FORAGE === + +function can_action_forage() { + if (game.actions < 1) + return false + let here = get_lord_locale(game.command) + if (has_exhausted_marker(here) || is_sea(here)) + return false + return true +} + +function goto_forage() { + push_undo() + let here = get_lord_locale(game.command) + if (!has_adjacent_enemy(here) && is_neutral_locale(here)) { + let die = roll_die() + if (die <= 4) { + add_lord_assets(game.command, PROV, 1) + log(`${HIT[die]}, Foraged at %${here}`) + deplete_locale(here) + } else { + log(`${MISS[die]}, Forage Failure`) + } + } else if (has_adjacent_enemy(here) || is_favour_enemy(here, game.active)) { + let die = roll_die() + if (die <= 3) { + add_lord_assets(game.command, PROV, 1) + log(`${HIT[die]}, Foraged at %${here}`) + deplete_locale(here) + } else { + log(`${MISS[die]}, Forage Failure`) + } + } else { + add_lord_assets(game.command, PROV, 1) + log(`Foraged at %${here}`) + deplete_locale(here) + } + if (lord_has_capability(game.command, AOW_YORK_SCOURERS)) { + add_lord_assets(game.command, PROV, 1) + log(`1 Extra Provender (Scourers)`) + } + + spend_action(1) + resume_command() +} + +// === 4.6.3 ACTION: TAX === + +function can_tax_at(here, lord) { + if (is_friendly_locale(here) && !has_exhausted_marker(here)) { + // London, Calais, and Harlech + if (here === LOC_LONDON || here === LOC_CALAIS || here === LOC_HARLECH) + return true + + // Own seat + if (here === data.lords[lord].seat) + return true + + // vassal seats + for (let vassal = first_vassal; vassal <= last_vassal; ++vassal) + if (is_vassal_mustered_with(vassal, lord)) + if (here === data.vassals[vassal].seat) + return true + } + return false +} + +// adjacent friendly locales to an eligible stronghold (can_tax_at) +function search_tax(result, start, lord) { + let ships = get_shared_assets(start, SHIP) + + search_seen.fill(0) + search_seen[start] = 1 + + let queue = [ start ] + while (queue.length > 0) { + let here = queue.shift() + + if (can_tax_at(here, lord)) { + if (result) + set_add(result, here) + else + return true + } + + if (is_friendly_locale(here)) { + for (let next of data.locales[here].adjacent) { + if (!search_seen[next]) { + search_seen[next] = 1 + queue.push(next) + } + } + if (ships > 0 && is_seaport(here)) { + for (let next of find_ports(here)) { + if (!search_seen[next]) { + search_seen[next] = 1 + queue.push(next) + } + } + } + } + } + if (result) + return result + else + return false +} + +function can_action_tax() { + if (game.actions < 1) + return false + let here = get_lord_locale(game.command) + if (can_tax_at(here, game.command)) + return true + return search_tax(false, here, game.command) +} + +function goto_tax() { + push_undo() + game.state = "tax" + game.where = NOWHERE + init_influence_check(game.command) +} + +function end_tax() { + game.where = NOWHERE + spend_action(1) + resume_command() +} + +function get_tax_amount(loc, lord) { + let tax + + if (loc === LOC_LONDON || loc === LOC_CALAIS) + tax = 3 + else if (is_city(loc)) + tax = 2 + else + tax = 1 + + if (lord_has_stafford_branch(loc, lord)) { + log(`C${AOW_YORK_STAFFORD_BRANCH}.`) + tax += 1 + } + + if (lord_has_capability(lord, AOW_YORK_SO_WISE_SO_YOUNG)) { + log(`C${AOW_YORK_SO_WISE_SO_YOUNG}.`) + tax += 1 + } + + if ( + lord === LORD_DEVON && ( + loc === LOC_EXETER || + loc === LOC_LAUNCESTON || + loc === LOC_PLYMOUTH || + loc === LOC_WELLS || + loc === LOC_DORCHESTER + ) + ) { + // TODO: log which ability + tax += 1 + } + + return tax +} + +states.tax = { + inactive: "Tax", + prompt() { + view.prompt = "Tax: Select the location to tax." + if (game.where === NOWHERE) { + for (let loc of search_tax([], get_lord_locale(game.command), game.command)) + gen_action_locale(loc) + } else { + view.prompt = `Tax: Attempting to tax ${data.locales[game.where].name}. ` + prompt_influence_check() + } + }, + locale(loc) { + game.where = loc + if (loc === data.lords[game.command].seat) { + // Auto succeed without influence check at Lords seat. + deplete_locale(game.where) + + log(`Taxed automatically successful at %${game.where}.`) + add_lord_assets(game.command, COIN, get_tax_amount(game.where, game.command)) + end_tax() + } + }, + spend1: add_influence_check_modifier_1, + spend3: add_influence_check_modifier_2, + check() { + let results = do_influence_check() + logi(`Tax : ${results.success ? "Successful" : "Failed"}: (${range(results.rating)}) ${results.success ? HIT[results.roll] : MISS[results.roll]}`) + + if (results.success) { + deplete_locale(game.where) + + log(`Taxed %${game.where}.`) + add_lord_assets(game.command, COIN, get_tax_amount(game.where, game.command)) + } else { + log(`Tax of %${game.where} failed.`) + } + end_tax() + }, +} + +// === 4.6.4 ACTION: PARLEY === + +// TODO : FIX Parley through strongholds overseas +function can_parley_at(loc) { + return !is_exile(loc) && !is_friendly_locale(loc) && !has_enemy_lord(loc) && !is_sea(loc) +} + +var search_seen = new Array(last_locale + 1) +var search_dist = new Array(last_locale + 1) + +function search_parley(result, start) { + let ships = get_shared_assets(start, SHIP) + + search_dist.fill(0) + search_seen.fill(0) + search_seen[start] = 1 + + let queue = [ start ] + while (queue.length > 0) { + let here = queue.shift() + let dist = search_dist[here] + let next_dist = dist + 1 + + if (can_parley_at(here)) { + if (result) + map_set(result, here, dist) + else + return true + } + + if (is_friendly_locale(here)) { + for (let next of data.locales[here].adjacent) { + if (!search_seen[next]) { + search_seen[next] = 1 + search_dist[next] = next_dist + queue.push(next) + } + } + if (ships > 0 && is_seaport(here)) { + for (let next of find_ports(here)) { + if (!search_seen[next]) { + search_seen[next] = 1 + search_dist[next] = next_dist + queue.push(next) + } + } + } + } + } + + if (result) + return result + else + return false +} + +function can_action_parley_command() { + if (game.actions <= 0) + return false + + if (is_lord_at_sea(game.command)) + return false + + if (!is_first_action() && game.active === YORK && is_event_in_play(EVENT_LANCASTER_NEW_ACT_OF_PARLIAMENT)) + return false + + let here = get_lord_locale(game.command) + + if (can_parley_at(here)) + return true + + for (let next of data.locales[here].adjacent) + if (can_parley_at(next)) + return true + + if (is_seaport(here) && get_shared_assets(here, SHIP) > 0) + for (let next of find_ports(here)) + if (can_parley_at(next)) + return true + + return false +} + +function list_parley_command() { + let result = [] + + let here = get_lord_locale(game.command) + if (can_parley_at(here)) + map_set(result, here, 0) + + if (is_friendly_locale(here)) { + for (let next of data.locales[here].adjacent) + if (can_parley_at(next)) + map_set(result, next, 1) + + if (is_seaport(here) && get_shared_assets(here, SHIP) > 0) + for (let next of find_ports(here)) + if (can_parley_at(next)) + map_set(result, next, 1) + } + + return result +} + +function can_action_parley_levy() { + if (game.count <= 0 + && (game.who !== LORD_HENRY_VI || game.flags.free_parley_henry === 0) + && ((game.who !== LORD_GLOUCESTER_1 && game.who !== LORD_GLOUCESTER_2) || game.flags.free_parley_gloucester === 0) + && (!game.flags.jack_cade)) + return true + let here = get_lord_locale(game.who) + if (can_parley_at(here)) + return true + return search_parley(false, here) +} + +function list_parley_levy() { + let here = get_lord_locale(game.who) + return search_parley([], here) +} + +function goto_parley() { + push_state("parley") + + if (is_levy_phase()) { + init_influence_check(game.who) + game.parley = list_parley_levy() + } else { + init_influence_check(game.command) + game.parley = list_parley_command() + + // Campaign phase, and current location is no cost (except some events), and always successful. + if (game.parley.length === 2 && get_lord_locale(game.command) === game.parley[0]) { + log(`Parley at ${data.locales[get_lord_locale(game.command)]}`) + shift_favour_toward(game.parley[0]) + if (is_lancaster_card(game.command) && is_event_in_play(EVENT_YORK_AN_HONEST_TALE_SPEEDS_BEST)) { + reduce_lancaster_influence(1) + } + end_parley(true) + return + } + } + + if (game.parley.length === 2) { + game.where = game.parley[0] + add_influence_check_distance(game.parley[1]) + } else { + game.where = NOWHERE + } +} + +function end_parley(success) { + pop_state() + game.flags.naval_blockade = 0 + game.where = NOWHERE + game.parley = NOTHING + if (game.flags.free_parley_henry > 0 && game.who === LORD_HENRY_VI) { + --game.flags.free_parley_henry + ++game.count + } + if (game.flags.free_parley_gloucester > 0 && (game.who === LORD_GLOUCESTER_1 || game.who === LORD_GLOUCESTER_2)) { + --game.flags.free_parley_gloucester + ++game.count + } + if (game.flags.jack_cade > 0) { + --game.flags.jack_cade + ++game.count + } + end_influence_check() + + if (is_campaign_phase()) { + if (game.active === YORK && is_event_in_play(EVENT_LANCASTER_NEW_ACT_OF_PARLIAMENT)) + spend_all_actions() + else + spend_action(1) + resume_command() + } else { + if (success) + goto_the_kings_name("Parley") + else + resume_levy_muster_lord() + } +} + +states.parley = { + inactive: "Parley", + prompt() { + view.prompt = "Parley: Choose a Locale to Parley." + if (game.where === NOTHING) { + for (let i = 0; i < game.parley.length; i += 2) + gen_action_locale(game.parley[i]) + } else { + view.prompt = "Parley: " + prompt_influence_check() + } + }, + locale(loc) { + push_undo() + game.where = loc + add_influence_check_distance(map_get(game.parley, loc, 0)) + if (is_levy_phase() && check_naval_blockade("levy parley", loc)) { + roll_naval_blockade() + } + if (is_campaign_phase() && check_naval_blockade("campaign parley", loc)) + roll_naval_blockade() + }, + spend1: add_influence_check_modifier_1, + spend3: add_influence_check_modifier_2, + check() { + let results = do_influence_check() + + if (game.flags.parliament_votes === 1) { + log(`Parley at ${data.locales[game.where].name}. Automatic Success.`) + logevent(EVENT_LANCASTER_PARLIAMENT_VOTES) + game.flags.parliament_votes = 0 + } + else if (game.flags.jack_cade > 0) { + log(`Parley at ${data.locales[game.where].name}. Automatic Success.`) + logevent(EVENT_YORK_JACK_CADE) + } + else if (game.flags.succession === 1) { + log(`Parley at ${data.locales[game.where].name}. Automatic Success.`) + logevent(EVENT_YORK_SUCCESSION) + game.flags.succession = 0 + } + else if (is_campaign_phase() + && game.command === LORD_DEVON + && get_lord_locale(LORD_DEVON) === LOC_EXETER + && is_event_in_play(EVENT_YORK_DORSET) + && game.state === "parley") { + log(`Parley at ${data.locales[game.where].name}. Automatic Success. C${EVENT_YORK_DORSET}.`) + } + else + log(`Attempt to Parley at %${game.where} ${results.success ? "Successful" : "Failed"}: (${range(results.rating)}) ${results.success ? HIT[results.roll] : MISS[results.roll]}`) + if (results.success) { + shift_favour_toward(game.where) + end_parley(true) + } else { + end_parley(false) + } + }, +} + +// === 4.3 ACTION: MARCH === + +function get_way_type(from, to) { + return map_get(data.ways[from], to, undefined) +} + +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 "" +} + +// Wales forbidden to the lancastrians for march, sail, intercept +function is_wales_forbidden(loc) { + if (game.active === LANCASTER && is_event_in_play(EVENT_YORK_OWAIN_GLYNDWR) && is_wales(loc)) + return true + return false +} + +function is_wales_forbidden_to_enemy(loc) { + if (game.active !== LANCASTER && is_event_in_play(EVENT_YORK_OWAIN_GLYNDWR) && is_wales(loc)) + return true + return false +} + +function can_march_to(to) { + if (is_wales_forbidden(to)) + return false + if (is_truce_in_effect() && has_enemy_lord(to)) + return false + return true +} + +function prompt_march() { + let from = get_lord_locale(game.command) + + if (is_first_action()) { + for (let to of data.locales[from].paths) { + if (can_march_to(to)) + gen_action_locale(to) + } + } + if (game.actions > 0 || game.flags.surprise_landing === 2) { + for (let to of data.locales[from].roads) { + if (can_march_to(to)) + gen_action_locale(to) + + } + for (let to of data.locales[from].highways) { + if (can_march_to(to)) + gen_action_locale(to) + } + } else if ((game.actions === 0 && is_first_march_highway()) || game.flags.surprise_landing === 2) { + for (let to of data.locales[from].highways) { + if (can_march_to(to)) + gen_action_locale(to) + } + } + if ( + (lord_has_capability(game.command, AOW_YORK_YORKISTS_NEVER_WAIT) || (is_event_in_play(EVENT_LANCASTER_FORCED_MARCHES) && game.active === LANCASTER)) && + game.actions === 0 && + is_first_march_highway() && + count_group_lords() === 1 + ) { + for (let to of data.locales[from].roads) { + if (can_march_to(to)) + gen_action_locale(to) + } + } +} + +function goto_march(to) { + push_undo() + let from = get_lord_locale(game.command) + game.march = { from, to, avoid: -1 } + march_with_group_1() +} + +function march_with_group_1() { + let transport = count_group_assets(CART) + let prov = count_group_assets(PROV) + if (prov > transport) + game.state = "march_laden" + else + march_with_group_2() +} + +// No laden but re-used to discard extra provender +states.march_laden = { + inactive: "March", + prompt() { + let to = game.march.to + let transport = count_group_assets(CART) + let prov = count_group_assets(PROV) + + view.group = game.group + view.prompt = `March: Unladen. ` + + if (prov > transport) { + let overflow_prov = prov - transport + view.prompt += `Please discard ${overflow_prov} Provender` + for (let lord of game.group) { + if (prov > transport) { + if (get_lord_assets(lord, PROV) > 0) { + gen_action_prov(lord) + } + } + } + } else { + view.actions.march = 1 + gen_action_locale(to) + } + }, + 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 = game.march.from + let to = game.march.to + let type = get_way_type(from, to) + let alone = count_group_lords() === 1 + + switch (type) { + case "highway": + if (is_first_march_highway() || game.flags.surprise_landing === 2) { + spend_march_action(0) + } else { + spend_march_action(1) + game.flags.first_march_highway = 1 + } + break + + case "road": + if ((alone && is_first_march_highway()) || game.flags.surprise_landing === 2) { + spend_march_action(0) + } else { + spend_march_action(1) + if (alone && (lord_has_capability(game.command, AOW_YORK_YORKISTS_NEVER_WAIT) || (is_event_in_play(EVENT_LANCASTER_FORCED_MARCHES) && game.active === LANCASTER))) + game.flags.first_march_highway = 1 + } + break + + case "path": + spend_all_actions() + break + } + + log(`Marched to %${to}${format_group_move()}.`) + + for (let lord of game.group) { + set_lord_locale(lord, to) + // Note: We flag the lords moved and levy burgundians after king's parley and parliament's truce have resolved. + // See end_kings_parley. + } + + goto_intercept() +} + +function end_march() { + // Disbanded in battle! + if (!is_lord_on_map(game.command)) { + game.where = NOWHERE + game.march = 0 + spend_all_actions() + resume_command() + return + } + + let here = get_lord_locale(game.command) + if (is_seaport(here)) + game.flags.march_to_port = 1 + else + game.flags.march_to_port = 0 + game.flags.sail_to_port = 0 + + game.march = 0 + resume_command() +} + +// === 4.3.4 INTERCEPT === + +function can_intercept_to(to) { + // TODO: forbid lancaster intercept into york moving to york, and vice versa + if (is_truce_in_effect()) + return false + if (is_wales_forbidden_to_enemy(to)) + return false + return true +} + +function goto_intercept() { + let here = get_lord_locale(game.command) + if (can_intercept_to(here)) { + for (let next of data.locales[here].not_paths) { + if (has_enemy_lord(next)) { + game.state = "intercept" + set_active_enemy() + game.intercept_group = [] + game.who = NOBODY + return + } + } + } + end_intercept() +} + +function end_intercept() { + game.intercept_group = 0 + game.who = NOBODY + goto_kings_parley() +} + +function can_play_held_event_intercept(c) { + switch (c) { + case EVENT_LANCASTER_FLANK_ATTACK: + return can_play_flank_attack() + case EVENT_YORK_FLANK_ATTACK: + return can_play_flank_attack() + } + return false +} + +function prompt_held_event_intercept() { + for (let c of current_hand()) + if (can_play_held_event_intercept(c)) + gen_action_card(c) +} + +states.intercept = { + inactive: "Intercept", + prompt() { + view.prompt = `Choose lord to intercept moving lords?` + let to = get_lord_locale(game.command) + + prompt_held_event_intercept() + + if (game.who === NOBODY) { + for (let next of data.locales[to].not_paths) + for_each_friendly_lord_in_locale(next, gen_action_lord) + } else { + gen_action_lord(game.who) + if (is_marshal(game.who) || is_lieutenant(game.who)) { + for_each_friendly_lord_in_locale(get_lord_locale(game.who), lord => { + if (!is_marshal(lord)) + gen_action_lord(lord) + }) + } + view.actions.intercept = 1 + } + + view.actions.pass = 1 + view.group = game.intercept_group + }, + lord(lord) { + if (game.who === NOBODY) { + game.who = lord + set_toggle(game.intercept_group, lord) + } else if (lord === game.who) { + game.who = NOBODY + game.intercept_group = [] + } else { + set_toggle(game.intercept_group, lord) + } + }, + card(c) { + push_undo() + play_held_event(c) + }, + pass() { + set_active_enemy() + end_intercept() + }, + intercept() { + let valour = data.lords[game.who].valour + let success = false + if (is_event_in_play(EVENT_LANCASTER_FLANK_ATTACK || is_event_in_play(EVENT_YORK_FLANK_ATTACK))) { + success = true + // FIXME: swap_battle_attacker = 1 ??? + } + else { + let roll = roll_die() + success = roll <= valour + log(`Intercept ${success ? "Succeeded." : "Failed."} (${range(valour)}): ${success ? HIT[roll] : MISS[roll]}`) + } + if (success) { + goto_intercept_march() + } else { + set_active_enemy() + end_intercept() + } + }, +} + +function goto_intercept_march() { + if (count_group_transport(game.intercept_group) >= count_group_assets(PROV, game.intercept_group)) { + do_intercept_march() + } else { + game.state = "intercept_march" + } +} + +function do_intercept_march() { + for (let lord of game.intercept_group) { + set_lord_locale(lord, get_lord_locale(game.command)) + set_lord_moved(lord, 1) + levy_burgundians(lord) + } + end_intercept_march() +} + +function end_intercept_march() { + // back to originally marching lord + set_active_enemy() + end_intercept() +} + +states.intercept_march = { + inactive: "Intercept", + prompt() { + let to = game.march.to + let transport = count_group_transport(game.intercept_group) + let prov = count_group_assets(PROV, game.intercept_group) + + view.group = game.intercept_group + + view.prompt = `Intercept: Unladen.` + + if (prov > transport) { + view.prompt = `Intercept: Hindered with ${prov} Provender, and ${transport} Transport.` + for (let lord of game.intercept_group) { + if (get_lord_assets(lord, PROV) > 0) { + view.prompt += " Discard Provender." + gen_action_prov(lord) + } + } + } else { + view.actions.intercept = 1 + gen_action_locale(to) + } + }, + prov: drop_prov, + intercept: do_intercept_march, + locale: do_intercept_march, +} + +function for_each_friendly_lord_in_locale(loc, f) { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; lord++) + if (get_lord_locale(lord) === loc) + f(lord) +} + +// === MARCH EVENT: FLANK ATTACK === + +function can_play_flank_attack() { + return game.state === "intercept" && game.who !== NOBODY && !is_truce_in_effect() +} + +// === MARCH EVENT: KING'S PARLEY === + +function goto_kings_parley() { + // If Henry VI in space, with King's Parley capability + // TODO: ... unless he intercepted York lords already present ? + // TODO: ... or remove possibility for above case. + if (game.active === YORK) { + if (get_lord_locale(LORD_HENRY_VI) === game.march.to) { + if (lord_has_capability(LORD_HENRY_VI, AOW_LANCASTER_KINGS_PARLEY)) { + set_active_enemy() + game.state = "kings_parley" + return + } + } + } + end_kings_parley() +} + +states.kings_parley = { + inactive: "King's Parley?", + prompt() { + view.prompt = "You may discard King's Parley to cancel Yorkist approach." + gen_action_card(AOW_LANCASTER_KINGS_PARLEY) + view.actions.pass = 1 + }, + card(_) { + push_undo() + discard_lord_capability(LORD_HENRY_VI, AOW_LANCASTER_KINGS_PARLEY) + + // Cancel approach! + for (let lord of game.group) + set_lord_locale(lord, game.march.from) + + set_active_enemy() + end_march() + }, + pass() { + set_active_enemy() + end_kings_parley() + }, +} + +function end_kings_parley() { + goto_parliaments_truce() +} + +// === MARCH EVENT: PARLIAMENT'S TRUCE === + +function can_play_parliaments_truce() { + return game.state === "campaign" +} + +function is_truce_in_effect() { + return ( + is_event_in_play(EVENT_YORK_PARLIAMENTS_TRUCE) || + is_event_in_play(EVENT_LANCASTER_PARLIAMENTS_TRUCE) + ) +} + +function goto_parliaments_truce() { + // The non-active player can cancel approach with parliament's truce + + // We don't allow the active player to cancel an intercept -- if they want to cancel + // an interception, they should have played the event before marching. + + let here = get_lord_locale(game.command) + if ( + has_enemy_lord(here) && ( + (game.active === YORK && could_play_card(EVENT_LANCASTER_PARLIAMENTS_TRUCE)) || + (game.active === LANCASTER && could_play_card(EVENT_YORK_PARLIAMENTS_TRUCE)) + ) + ) { + set_active_enemy() + game.state = "parliaments_truce" + return + } + + end_parliaments_truce() +} + +states.parliaments_truce = { + inactive: "Parliament's Truce?", + prompt() { + view.prompt = "You may play Parliament's Truce to cancel approach." + if (game.active === YORK) + gen_action_card_if_held(EVENT_YORK_PARLIAMENTS_TRUCE) + else + gen_action_card_if_held(EVENT_LANCASTER_PARLIAMENTS_TRUCE) + view.actions.pass = 1 + }, + card(c) { + play_held_event(c) + + // Cancel approach! + for (let lord of game.group) + set_lord_locale(lord, game.march.from) + + set_active_enemy() + end_march() + }, + pass() { + set_active_enemy() + end_parliaments_truce() + }, +} + +function end_parliaments_truce() { + + // Note: we flag the lords moved and levy burgundians after king's parley and parliament's truce have resolved + for (let lord of game.group) { + set_lord_moved(lord, 1) + levy_burgundians(lord) + } + + goto_blocked_ford() +} + +// === MARCH EVENT: BLOCKED FORD === + +function goto_blocked_ford() { + let here = get_lord_locale(game.command) + + // The marching lord can now play blocked ford to prevent enemy going into exile. + if ( + has_enemy_lord(here) && ( + (game.active === YORK && could_play_card(EVENT_YORK_BLOCKED_FORD)) || + (game.active === LANCASTER && could_play_card(EVENT_LANCASTER_BLOCKED_FORD)) + ) + ) { + game.state = "blocked_ford" + return + } + + goto_choose_exile() +} + +states.blocked_ford = { + inactive: "Blocked Ford?", + prompt() { + view.prompt = "You may play Blocked Ford." + + if (game.active === YORK) + gen_action_card_if_held(EVENT_YORK_BLOCKED_FORD) + else + gen_action_card_if_held(EVENT_LANCASTER_BLOCKED_FORD) + + view.actions.pass = 1 + }, + card(c) { + play_held_event(c) + game.where = get_lord_locale(game.command) + goto_battle() + }, + pass() { + goto_choose_exile() + }, +} + +// === 4.3.5 APPROACH - CHOOSE EXILE === + +function goto_choose_exile() { + let here = get_lord_locale(game.command) + if (has_enemy_lord(here)) { + spend_all_actions() // end command upon any approach + game.where = here + game.state = "choose_exile" + set_active_enemy() + } else { + end_march() + } +} + +function end_choose_exile() { + if (has_friendly_lord(get_lord_locale(game.command))) { + // still some lords not exiled to fight. + set_active_enemy() + goto_battle() + } else { + // no one left, goto finish marching. + set_active_enemy() + end_march() + } +} + +states.choose_exile = { + inactive: "Exiles", + prompt() { + view.prompt = "Select Lords to go into Exile." + for_each_friendly_lord_in_locale(get_lord_locale(game.command), lord => { + gen_action_lord(lord) + }) + view.actions.done = 1 + }, + lord(lord) { + push_undo() + // TODO: give up assets as if spoils? + exile_lord(lord) + }, + done() { + end_choose_exile() + }, +} + +function exile_lord(lord) { + if (lord_has_capability(lord, AOW_YORK_ENGLAND_IS_MY_HOME)) { + disband_lord(lord, false) + set_lord_calendar(lord, current_turn() + 1) + } else { + set_lord_in_exile(lord) + disband_lord(lord, false) + } +} + +// === 4.3.5 APPROACH - SPOILS AFTER CHOOSING EXILE === + +// TODO: spoils after choosing exile + +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 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(CART) > 0) + view.actions.take_cart = 1 +} + +function take_spoils(type) { + add_lord_assets(game.who, type, 1) + add_spoils(type, -1) + if (!has_any_spoils()) + game.who = NOBODY +} + +// === 4.4 BATTLE === + +function get_lord_array_position(lord) { + for (let p = 0; p < 12; ++p) + if (game.battle.array[p] === lord) + return p + return -1 +} + +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 ravine_check(lord,pos) { + // TODO: if no lord in pos and no ravine? -- if (lord !== NOBODY) + if (game.battle.array[pos] === lord) + return true + return false +} + +function filled(pos) { + if (game.battle.array[pos] !== NOBODY && !ravine_check(game.battle.ravine, pos)) { + return true + } + return false +} + +const battle_strike_positions = [ D1, D2, D3, A1, A2, A3 ] + +const battle_steps = [ + { name: "Archery", hits: count_archery_hits }, + { name: "Melee", hits: count_melee_hits }, +] + +function count_archery_hits(lord) { + let hits = 0 + hits += get_lord_forces(lord, LONGBOWMEN) << 2 + hits += get_lord_forces(lord, BURGUNDIANS) << 2 + hits += get_lord_forces(lord, MILITIA) + hits += get_lord_forces(lord, MERCENARIES) + + if (is_leeward_battle_line_in_play(lord)) { + // TODO: rounding? + return hits/2 + } + + return hits +} + +function count_melee_hits(lord) { + let hits = 0 + hits += /*retinue*/ 3 << 1 + //hits += count_vassals_with_lord(lord) << 2 + if (lord_has_capability(lord, AOW_LANCASTER_CHEVALIERS)) + hits += get_lord_forces(lord, MEN_AT_ARMS) << 2 + else + hits += get_lord_forces(lord, MEN_AT_ARMS) << 1 + hits += get_lord_forces(lord, MILITIA) + hits += get_lord_forces(lord, MERCENARIES) + hits += get_lord_forces(lord, BURGUNDIANS) << 1 + + if (lord === game.battle.caltrops) { + hits += 2 + } + + return hits +} + +function count_lord_hits(lord) { + return battle_steps[game.battle.step].hits(lord) +} + +function format_strike_step() { + return battle_steps[game.battle.step].name +} + +function format_hits() { + if (game.battle.ahits > 0) { + return `${game.battle.ahits} Hit${game.battle.ahits > 1 ? "s" : ""}` + } else if (game.battle.dhits > 0) { + return `${game.battle.dhits} Hit${game.battle.dhits > 1 ? "s" : ""}` + } +} + +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 < 6; ++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_archery_step() { + return game.battle.step === 0 +} + +function is_melee_step() { + return game.battle.step === 1 +} + +function has_strike(pos) { + return game.battle.ah[pos] > 0 +} + + +// Capabilities adding troops at start of the battle +function add_battle_capability_troops() { + let here = get_lord_locale(game.command) + + for (let lord = first_york_lord; lord <= last_lancaster_lord; ++lord) { + if (lord_has_capability(lord, AOW_YORK_MUSTERD_MY_SOLDIERS) && has_favoury_marker(here)) { + add_lord_forces(lord, MEN_AT_ARMS, 2) + add_lord_forces(lord, LONGBOWMEN, 1) + } + if (lord_has_capability(lord, AOW_LANCASTER_MUSTERD_MY_SOLDIERS) && has_favourl_marker(here)) { + add_lord_forces(lord, MEN_AT_ARMS, 2) + add_lord_forces(lord, LONGBOWMEN, 1) + } + if (lord_has_capability(lord, AOW_LANCASTER_WELSH_LORD) && is_wales(here)) { + add_lord_forces(lord, LONGBOWMEN, 2) + } + if (lord_has_capability(lord, AOW_YORK_PEMBROKE) && is_wales(here)) { + add_lord_forces(lord, LONGBOWMEN, 2) + } + if (lord_has_capability(lord, AOW_YORK_PERCYS_NORTH1) && is_north(here)) { + add_lord_forces(lord, MILITIA, 4) + } + if (lord_has_capability(lord, AOW_YORK_PERCYS_NORTH2) && can_supply_at(LOC_CARLISLE, 0)) { + add_lord_forces(lord, MILITIA, 4) + } + if (lord_has_capability(lord, AOW_YORK_KINGDOM_UNITED) && (is_north(here) || is_south(here) || is_wales(here))) { + add_lord_forces(lord, MILITIA, 3) + } + + // TODO: check this condition + if ( + is_lord_on_map(lord) && + !is_lord_on_calendar(lord) && + lord_has_capability(lord, AOW_LANCASTER_PHILIBERT_DE_CHANDEE) && + ((is_friendly_locale(here) && data.port_2.includes(here)) || is_adjacent_friendly_port_english_channel(here)) + ) { + add_lord_forces(lord, MEN_AT_ARMS, 2) + } + } +} + +//... And removing them at the end of the battle +function remove_battle_capability_troops() { + let here = get_lord_locale(game.command) + + for (let lord = first_york_lord; lord <= last_lancaster_lord; ++lord) { + if (lord_has_capability(lord, AOW_YORK_MUSTERD_MY_SOLDIERS) && has_favoury_marker(here)) { + add_lord_forces(lord, MEN_AT_ARMS, -2) + add_lord_forces(lord, LONGBOWMEN, -1) + } + if (lord_has_capability(lord, AOW_LANCASTER_MUSTERD_MY_SOLDIERS) && has_favourl_marker(here)) { + add_lord_forces(lord, MEN_AT_ARMS, -2) + add_lord_forces(lord, LONGBOWMEN, -1) + } + if (lord_has_capability(lord, AOW_LANCASTER_WELSH_LORD) && is_wales(here)) { + add_lord_forces(lord, LONGBOWMEN, -2) + } + if (lord_has_capability(lord, AOW_YORK_PEMBROKE) && is_wales(here)) { + add_lord_forces(lord, LONGBOWMEN, -2) + } + if (lord_has_capability(lord, AOW_YORK_PERCYS_NORTH1) && is_north(here)) { + add_lord_forces(lord, MILITIA, -4) + } + if (lord_has_capability(lord, AOW_YORK_PERCYS_NORTH2) && can_supply_at(LOC_CARLISLE, 0)) { + add_lord_forces(lord, MILITIA, -4) + } + if (lord_has_capability(lord, AOW_YORK_KINGDOM_UNITED) && (is_north(here) || is_south(here) || is_wales(here))) { + add_lord_forces(lord, MILITIA, -3) + } + + // TODO: check this condition + if (is_lord_on_map(lord) && lord_has_capability(lord, AOW_LANCASTER_PHILIBERT_DE_CHANDEE)) { + add_lord_forces(lord, MEN_AT_ARMS, -2) + } + } +} + +function goto_battle() { + let here = get_lord_locale(game.command) + + log_h3(`Battle at %${here}`) + + game.battle = { + where: game.where, + round: 1, + step: 0, + relief: 0, + attacker: game.active, + ambush: 0, + loser: 0, + fought: 0, // flag all lords who participated + array: [ + -1, -1, -1, + -1, -1, -1, + ], + valour: Array(lord_count).fill(0), + routed_vassals: [], + engagements: [], + reserves: [], + retreated: 0, + fled: [], + routed: [], + target: NOBODY, + strikers: 0, + a_hits: 0, + d_hits: 0, + fc: -1, + } + + // Troops by capability + + add_battle_capability_troops() + + // All attacking lords to reserve + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (get_lord_locale(lord) === here) { + set_lord_fought(lord) + set_add(game.battle.reserves, lord) + if ( + lord_has_capability(lord, AOW_LANCASTER_EXPERT_COUNSELLORS) || + lord_has_capability(lord, AOW_LANCASTER_VETERAN_OF_FRENCH_WARS) + ) + game.battle.valour[lord] = data.lords[lord].valour + 2 + else if ( + lord_has_capability( + lord, + AOW_LANCASTER_ANDREW_TROLLOPE || lord_has_capability(lord, AOW_LANCASTER_MY_FATHERS_BLOOD) + ) || + lord_has_capability(lord, AOW_LANCASTER_EDWARD) || + (lord_has_capability(lord, AOW_LANCASTER_LOYAL_SOMERSET) && get_lord_locale(LORD_MARGARET) === here) + ) + game.battle.valour[lord] = data.lords[lord].valour + 1 + else + game.battle.valour[lord] = data.lords[lord].valour + } + } + + // All defending lords to reserve + for (let lord = first_enemy_lord; lord <= last_enemy_lord; ++lord) { + if (get_lord_locale(lord) === here) { + set_lord_fought(lord) + set_add(game.battle.reserves, lord) + if ( + lord_has_capability(lord, AOW_LANCASTER_EXPERT_COUNSELLORS) || + lord_has_capability(lord, AOW_LANCASTER_VETERAN_OF_FRENCH_WARS) + ) + game.battle.valour[lord] = data.lords[lord].valour + 2 + else if ( + lord_has_capability( + lord, + AOW_LANCASTER_ANDREW_TROLLOPE || lord_has_capability(lord, AOW_LANCASTER_MY_FATHERS_BLOOD) + ) || + lord_has_capability(lord, AOW_LANCASTER_EDWARD) || + (lord_has_capability(lord, AOW_LANCASTER_LOYAL_SOMERSET) && get_lord_locale(LORD_MARGARET) === here) + ) + game.battle.valour[lord] = data.lords[lord].valour + 1 + else + game.battle.valour[lord] = data.lords[lord].valour + } + } + + goto_array_defender() +} + +// === 4.4.1 BATTLE ARRAY === + +// 0) Defender decides to stand for Battle, not Exile +// 1) Defender decides how he wants to array his lords +// 2) Defender positions front D +// 3) Attacker positions front A. +// 4) Defender plays event +// 5) ATtacker plays event + +function has_friendly_reserves() { + for (let lord of game.battle.reserves) + if (is_friendly_lord(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() { + set_active_attacker() + game.state = "array_attacker" + game.who = NOBODY + let n = count_friendly_reserves() + if (n === 1) { + game.battle.array[A2] = pop_first_reserve() + end_array_attacker() + } + if (n === 0) + end_array_attacker() +} + +function goto_array_defender() { + 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 end_array_attacker() { + goto_defender_events() +} + +function end_array_defender() { + goto_array_attacker() +} + +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)) { + gen_action_lord(lord) + done = false + } + } + } + if (game.who === NOBODY && done) + view.actions.end_array = 1 + if (game.who !== NOBODY) { + prompt_array_place_opposed(A1, A2, A3, D1, D3) + } + }, + 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) { + if (array[D2] === NOBODY) { + gen_action_array(D2) + } else { + if (array[D1] === NOBODY) + gen_action_array(D1) + if (array[D3] === NOBODY) + gen_action_array(D3) + } + } + }, + array: action_array_place, + lord: action_select_lord, + end_array: end_array_defender, +} + +// === 4.4.1 BATTLE ARRAY: 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_attacker_events() +} + +function goto_attacker_events() { + set_active_attacker() + log_br() + if (can_play_battle_events()) + game.state = "attacker_events" + else + end_attacker_events() +} + +function end_attacker_events() { + goto_battle_rounds() +} + +function resume_battle_events() { + game.what = -1 + if (is_attacker()) + goto_attacker_events() + else + goto_defender_events() +} + +states.defender_events = { + inactive: "Defender Events", + prompt() { + view.prompt = "Defender may play Events." + prompt_battle_events() + + // defender only events + }, + card: action_battle_events, + done() { + end_defender_events() + }, +} + +states.attacker_events = { + inactive: "Attacker Events", + prompt() { + view.prompt = "Attacker may play Events." + prompt_battle_events() + }, + card: action_battle_events, + done() { + end_attacker_events() + }, +} + +function can_play_battle_events() { + if (game.active === LANCASTER) { + if (could_play_card(EVENT_LANCASTER_LEEWARD_BATTLE_LINE)) + return true + if (could_play_card(EVENT_LANCASTER_SUSPICION)) + return true + if (could_play_card(EVENT_LANCASTER_FOR_TRUST_NOT_HIM)) + return true + if (could_play_card(EVENT_LANCASTER_RAVINE)) + return true + } + if (game.active === YORK) { + if (could_play_card(EVENT_YORK_LEEWARD_BATTLE_LINE)) + return true + if (could_play_card(EVENT_YORK_SUSPICION)) + return true + if (could_play_card(EVENT_YORK_CALTROPS)) + return true + if (could_play_card(EVENT_YORK_REGROUP)) + return true + if (could_play_card(EVENT_YORK_SWIFT_MANEUVER)) + return true + if (could_play_card(EVENT_YORK_PATRICK_DE_LA_MOTE)) + return true + } + return false +} + +function prompt_battle_events() { + // both attacker and defender events + if (game.active === LANCASTER) { + gen_action_card_if_held(EVENT_LANCASTER_LEEWARD_BATTLE_LINE) + if (can_play_suspicion()) + gen_action_card_if_held(EVENT_LANCASTER_SUSPICION) + if (can_play_for_trust_not_him()) + gen_action_card_if_held(EVENT_LANCASTER_FOR_TRUST_NOT_HIM) + gen_action_card_if_held(EVENT_LANCASTER_RAVINE) + } + if (game.active === YORK) { + gen_action_card_if_held(EVENT_YORK_LEEWARD_BATTLE_LINE) + if (can_play_suspicion()) + gen_action_card_if_held(EVENT_YORK_SUSPICION) + gen_action_card_if_held(EVENT_YORK_CALTROPS) + gen_action_card_if_held(EVENT_YORK_REGROUP) + gen_action_card_if_held(EVENT_YORK_SWIFT_MANEUVER) + gen_action_card_if_held(EVENT_YORK_PATRICK_DE_LA_MOTE) + } + view.actions.done = 1 +} + +function action_battle_events(c) { + game.what = c + set_delete(current_hand(), c) + set_add(game.events, c) + switch (c) { + case EVENT_LANCASTER_LEEWARD_BATTLE_LINE: + // nothing to do + break + case EVENT_LANCASTER_SUSPICION: + game.state = "suspicion" + break + case EVENT_LANCASTER_FOR_TRUST_NOT_HIM: + game.state = "for_trust_not_him" + break + case EVENT_LANCASTER_RAVINE: + game.state = "ravine" + break + case EVENT_YORK_LEEWARD_BATTLE_LINE: + // nothing to do + break + case EVENT_YORK_SUSPICION: + game.state = "suspicion" + break + case EVENT_YORK_CALTROPS: + game.state = "caltrops" + break + case EVENT_YORK_REGROUP: + // nothing to do + break + case EVENT_YORK_SWIFT_MANEUVER: + // nothing to do + break + } +} + +// === BATTLE EVENT: RAVINE === + +states.ravine = { + inactive: "Ravine", + prompt() { + view.prompt = "Ravine: Select an enemy lord to ignore that Lord round 1" + for (let lord of game.battle.array) { + if (is_enemy_lord(lord)) { + gen_action_lord(lord) + } + } + for (let lord of game.battle.reserves) { + if (is_enemy_lord(lord)) { + gen_action_lord(lord) + } + } + }, + lord(lord) { + push_undo() + game.battle.ravine = lord + resume_battle_events() + logi(`${data.lords[lord].name} ignored for Engage and Strike Round 1`) + logevent(EVENT_LANCASTER_RAVINE) + }, +} + +// === BATTLE EVENT: REGROUP === + +function is_regroup_in_play() { + if (game.active === YORK) + return is_event_in_play(EVENT_YORK_REGROUP) + return false +} + +// TODO + +// === BATTLE EVENT: CALTROPS === + +function is_caltrops_in_play() { + if (game.active === YORK) + return is_event_in_play(EVENT_YORK_CALTROPS) +} + +states.caltrops = { + inactive: "Caltrops", + prompt() { + view.prompt = "Caltrops: Select a friendly lord to Add +2 Hits each Melee phase to his engagement" + for (let lord of game.battle.array) { + if (is_friendly_lord(lord)) { + gen_action_lord(lord) + } + } + }, + lord(lord) { + push_undo() + game.battle.caltrops = lord + resume_battle_events() + logi(`2 Hits added to ${data.lords[lord].name} each Melee round`) + logevent(EVENT_YORK_CALTROPS) + }, +} + +// === BATTLE EVENT: SUSPICION === + +function can_play_suspicion() { + // TODO: account for influence_capabilities + if (highest_friendly_influence() >= lowest_enemy_influence()) { + return true + } + return false +} + +function lowest_enemy_influence() { + let score = 10 + for (let lord = first_enemy_lord; lord <= last_enemy_lord; ++lord) { + if (get_lord_locale(lord) === get_lord_locale(game.command)) { + if (data.lords[lord].influence < score) { + score = data.lords[lord].influence + } + } + } + return score +} + +function highest_friendly_influence() { + let score = 0 + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (get_lord_locale(lord) === get_lord_locale(game.command)) { + if (data.lords[lord].influence > score) { + score = data.lords[lord].influence + } + } + } +} + +states.suspicion = { + inactive: "Suspicion", + prompt() { + view.prompt = "Suspicion: Check one of your lords to influence check" + for (let lord of game.battle.array) { + if (is_friendly_lord(lord)) { + gen_action_lord(lord) + } + } + for (let lord of game.battle.reserves) { + if (is_friendly_lord(lord)) { + gen_action_lord(lord) + } + } + }, + lord(lord) { + game.who = lord + push_undo() + push_state("suspicion_enemy_lord") + }, +} + +states.suspicion_enemy_lord = { + inactive: "Suspicion", + prompt() { + view.prompt = "Suspicion: Select one enemy lord to influence check" + for (let lord of game.battle.array) { + if (is_enemy_lord(lord)) { + if (suspicion_lord_score(game.who, data.lords[game.who].influence) > data.lords[lord].influence) { + gen_action_lord(lord) + } + } + } + }, + lord(lord) { + push_undo() + push_state("influence_check_suspicion") + init_influence_check(game.who) + game.who = lord + }, +} + +function suspicion_lord_score(lord, score) { + influence_capabilities(lord, score) + return score +} + +states.influence_check_suspicion = { + inactive: `Influence check`, + prompt() { + view.prompt = `Influence check : Success disbands enemy lord ` + prompt_influence_check() + }, + spend1: add_influence_check_modifier_1, + spend3: add_influence_check_modifier_2, + check() { + let lord = game.who + let results = do_influence_check() + logi(`Attempt to disband ${data.lords[lord].name} ${results.success ? "Successful" : "Failed"}: (${range(results.rating)}) ${results.success ? HIT[results.roll] : MISS[results.roll]}`) + if (results.success) { + log(`${data.lords[lord].name} disbanded`) + for (let x = 0; x < 6; x++) { + if (game.battle.array[x] === lord) { + game.battle.array[x] = NOBODY + break + } + else if (set_has(game.battle.reserves, lord)) { + array_remove(game.battle.reserves, lord) + } + } + disband_lord(lord) + game.who = NOBODY + end_influence_check() + resume_battle_events() + } else { + log(`${data.lords[lord].name} stays`) + game.who = NOBODY + end_influence_check() + resume_battle_events() + } + }, +} + +// === BATTLE EVENT: FOR TRUST NOT HIM === + +function can_play_for_trust_not_him() { + for (let vassal = first_vassal; vassal <= last_vassal; vassal++) { + if (is_vassal_mustered_with_york_lord(vassal) && get_lord_locale(get_vassal_lord(vassal)) === get_lord_locale(game.command)) { + // Hastings & Salisbury with Alice Montagu capability are immune. + if ((get_vassal_lord(vassal) !== LORD_SALISBURY || !lord_has_capability(LORD_SALISBURY, AOW_YORK_ALICE_MONTAGU)) && vassal !== VASSAL_HASTINGS) { + return true + } + } + } + return false +} + +states.for_trust_not_him = { + inactive: "For trust not him \u2014 Select Lord", + prompt() { + let done = true + game.where = NOWHERE + view.prompt = "Select a friendly lord" + for (let lord = first_lancaster_lord; lord <= last_lancaster_lord; lord++) { + if (is_lancaster_lord(lord) && get_lord_locale(lord) === game.battle.where) { + done = false + gen_action_lord(lord) + } + } + if (done) { + view.actions.done = 1 + } + }, + lord(lord) { + push_undo() + game.who = lord + game.state = "for_trust_not_him_vassal" + }, +} + +states.for_trust_not_him_vassal = { + inactive: "For trust not him \u2014 Select Vassal", + prompt() { + view.prompt = "Select an enemy Vassal" + for (let vassal = first_vassal; vassal <= last_vassal; vassal++) { + if (is_vassal_mustered_with_york_lord(vassal) && get_lord_locale(get_vassal_lord(vassal)) === get_lord_locale(game.command)) { + // Hastings & Salisbury with Alice Montagu capability are immune. + if ((get_vassal_lord(vassal) !== LORD_SALISBURY || !lord_has_capability(LORD_SALISBURY, AOW_YORK_ALICE_MONTAGU)) && vassal !== VASSAL_HASTINGS) { + gen_action_vassal(vassal) + } + } + } + }, + vassal(v) { + push_undo() + game.which = v + goto_influence_check_for_trust_not_him() + }, +} + +function goto_influence_check_for_trust_not_him() { + init_influence_check(game.who) + game.check.push({ + cost: 0, + modifier: data.vassals[game.which].influence * (game.active === LANCASTER ? -1 : 1), + source: "vassal", + }) + game.state = "for_trust_not_him_bribe" +} + +states.for_trust_not_him_bribe = { + inactive: `Influence check`, + prompt() { + view.prompt = `Influence check : Success bribes ${data.vassals[game.which].name} ` + prompt_influence_check() + }, + spend1: add_influence_check_modifier_1, + spend3: add_influence_check_modifier_2, + check() { + let results = do_influence_check() + if (game.who === LORD_HENRY_TUDOR && lord_has_capability(LORD_HENRY_TUDOR, AOW_LANCASTER_TWO_ROSES)) { + logi(`Automatic success C${AOW_LANCASTER_TWO_ROSES}`) + } + else { + logi(`Attempt to bribe ${data.vassals[game.which].name} ${results.success ? "Successful" : "Failed"}: (${range(results.rating)}) ${results.success ? HIT[results.roll] : MISS[results.roll]}`) + } + if (results.success) { + muster_vassal(game.which, game.who) + end_influence_check() + end_for_trust_not_him() + } else { + end_influence_check() + end_for_trust_not_him() + } + }, +} + +function end_for_trust_not_him() { + game.who = NOBODY + game.which = NOTHING + resume_battle_events() +} + +// === BATTLE EVENT: LEEWARD BATTLE LINE === + +function is_leeward_battle_line_in_play(lord) { + if (is_archery_step()) { + if (is_event_in_play(EVENT_LANCASTER_LEEWARD_BATTLE_LINE) + && !is_event_in_play(EVENT_YORK_LEEWARD_BATTLE_LINE) + && is_york_lord(lord)) { + logevent(EVENT_LANCASTER_LEEWARD_BATTLE_LINE) + return true + } + if (is_event_in_play(EVENT_YORK_LEEWARD_BATTLE_LINE) + && !is_event_in_play(EVENT_LANCASTER_LEEWARD_BATTLE_LINE) + && is_lancaster_lord(lord)) { + logevent(EVENT_YORK_LEEWARD_BATTLE_LINE) + return true + } + } + return false +} + +// === BATTLE EVENT: CULVERINS AND FALCONETS === + +function goto_culverins() { + let can_play = false + for (let lord of game.battle.array) { + if (is_lancaster_lord(lord) && lord_has_capability(lord, AOW_LANCASTER_CULVERINS_AND_FALCONETS)) + can_play = true + if (is_york_lord(lord) && lord_has_capability(lord, AOW_YORK_CULVERINS_AND_FALCONETS)) + can_play = true + } + if (can_play) { + set_active_defender() + game.state = "culverins_and_falconets" + game.who = NOBODY + } + else { + goto_engagement_total_hits() + } +} + +function artillery_hits(ahits) { + if (is_attacker()) { + game.battle.attacker_artillery = ahits*2 + } + if (is_defender()) { + game.battle.defender_artillery = ahits*2 + } +} + +states.culverins_and_falconets = { + inactive: "Culverins and Falconets", + prompt() { + let done = true + view.prompt = `Use Culverin and Falconets ?` + for (let lord of game.battle.array) { + if (lord !== NOBODY) { + if (is_friendly_lord(lord) && (lord_has_capability(lord, AOW_YORK_CULVERINS_AND_FALCONETS))) { + gen_action_card(AOW_YORK_CULVERINS_AND_FALCONETS) + done = false + } + if (is_friendly_lord(lord) && (lord_has_capability(lord, AOW_LANCASTER_CULVERINS_AND_FALCONETS))) { + gen_action_card(AOW_LANCASTER_CULVERINS_AND_FALCONETS) + done = false + } + } + } + if (done) { + view.prompt = "Culverins and Falconets : Done" + } + view.actions.done = 1 + }, + card(c) { + let die = roll_die() + let lord = find_lord_with_capability_card(c) + if (is_event_in_play(EVENT_YORK_PATRICK_DE_LA_MOTE) && game.active === YORK) { + let die2 = roll_die() + die += die2 + } + logi(`${data.lords[lord].name} Artillery does ${die} hits`) + artillery_hits(die) + discard_lord_capability(lord, c) + }, + done() { + if (is_defender()) { + set_active_enemy() + } + else { + goto_engagement_total_hits() + } + } +} + +// === BATTLE EVENT: SWIFT MANEUVER === + +function is_swift_maneuver_in_play() { + return is_event_in_play(EVENT_YORK_SWIFT_MANEUVER) +} + +states.swift_maneuver = { + inactive: "Swift Maneuver", + prompt() { + view.prompt = "Swift Maneuver: You may end the round now" + view.actions.end_battle_round = 1 + view.actions.pass = 1 + }, + end_battle_round() { + logevent(`${EVENT_YORK_SWIFT_MANEUVER}`) + log("Ended Action Round.") + set_active_enemy() + goto_end_battle_round() + }, + pass() { + logevent(`${EVENT_YORK_SWIFT_MANEUVER}`) + log("Passed.") + set_active_enemy() + finish_action_assign_hits(game.who) + }, +} + +// === 4.4.2 BATTLE ROUNDS === + +function goto_battle_rounds() { + set_active_defender() + log_h4(`Battle Round ${game.battle.round}`) + goto_flee() +} + +// === 4.4.2 BATTLE ROUNDS: FLEE === + +function goto_flee() { + game.state = "flee_battle" +} + +function end_flee() { + if (has_no_unrouted_forces()) { + end_battle_round() + return + } + set_active_enemy() + + if (game.active !== game.battle.attacker) { + goto_reposition_battle() + } else { + goto_flee() + } +} + +states.flee_battle = { + inactive: "Flee", + prompt() { + view.prompt = "Battle: Select Lords to Flee from the Field?" + for (let p = 0; p < 6; ++p) { + if (is_friendly_lord(game.battle.array[p])) { + gen_action_lord(game.battle.array[p]) + } + } + view.actions.done = 1 + }, + done() { + end_flee() + }, + lord(lord) { + push_undo() + log(`${lord_name[lord]} Fled the battle of %${game.battle.where}.`) + set_add(game.battle.fled, lord) + if (set_has(game.battle.reserves, lord)) { + array_remove(game.battle.reserves, lord) + } else { + for (let x = 0; x < 6; x++) { + if (game.battle.array[x] === lord) { + game.battle.array[x] = NOBODY + break + } + } + } + }, +} + +// === 4.4.2 BATTLE ROUNDS: REPOSITION === + +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("No Defenders Remain.") + } + + // If all A routed. + if (array[A1] === NOBODY && array[A2] === NOBODY && array[A3] === NOBODY) { + log("No Attackers Remain.") + } + + set_active_defender() + 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_advance() + else + goto_reposition_center() +} + +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_reposition_center() + else + goto_first_engagement() +} + +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() + }, +} + + +// === 4.4.2 BATTLE ROUNDS: ENGAGE / STRIKE === + +function determine_engagements() { + let center = [ A2, D2 ] + let engagements = [ + [ A1, D1 ], + [ A3, D3 ], + ] + let results = [] + + for (let x = 0; x < engagements.length; x++) { + let e = engagements[x] + if (filled(e[0]) && filled(e[1])) { + results.push(e) + } else if (filled(e[0])) { + set_add(center, e[0]) + } else if (filled(e[1])) { + set_add(center, e[1]) + } + } + results.unshift(center) + return results +} + +function goto_first_engagement() { + game.battle.step = 0 + game.battle.engagements = determine_engagements() + goto_engagement() +} + +function goto_next_step() { + let end = 2 + game.battle.step++ + if (game.battle.step >= end) + end_engagement() + else + goto_engagement() +} + +function goto_engagement() { + if (is_battle_over()) { + end_battle_round() + return + } + + log_h5(battle_steps[game.battle.step].name) + + // Generate hits + game.battle.ah = [ 0, 0, 0, 0, 0, 0 ] + + for (let pos of battle_strike_positions) { + let lord = game.battle.array[pos] + if (lord !== NOBODY) { + let hits = count_lord_hits(lord) + + game.battle.ah[pos] = hits + } + } + + resume_engagement() +} + +function find_engagement_index(pos) { + return game.battle.engagements.findIndex(e => e.includes(pos)) +} + +function end_engagement() { + game.battle.engagements.shift() + + if (game.battle.engagements.length > 0) { + game.battle.step = 0 + goto_engagement() + } else { + goto_end_battle_round() + } +} + +states.select_engagement = { + inactive: "Select Engagment", + prompt() { + view.prompt = `Select the next engagement to resolve.` + for (let pos of battle_strike_positions) { + if (has_strike(pos)) { + let lord = game.battle.array[pos] + if (is_friendly_lord(lord)) + gen_action_lord(lord) + } + } + }, + lord(lord) { + let idx = find_engagement_index(get_lord_array_position(lord)) + let eng = game.battle.engagements[idx] + array_remove(game.battle.engagements, idx) + game.battle.engagements.unshift(eng) + set_active_defender() + if (game.battle.round === 1 && is_archery_step()) { + goto_culverins() + } + else { + goto_engagement_total_hits() + } + }, +} + +function resume_engagement() { + if (game.battle.engagements.length === 1 || is_melee_step()) { + if (game.battle.round === 1 && is_archery_step()) { + goto_culverins() + } + else { + goto_engagement_total_hits() + } + // only one engagement, so no choices on order + } else { + set_active_attacker() + game.state = "select_engagement" + } +} + +// === 4.4.2 BATTLE ROUNDS: TOTAL HITS (ROUND UP) === + +// 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 goto_engagement_total_hits() { + let ahits = 0 + let dhits = 0 + + for (let pos of game.battle.engagements[0]) { + if (pos === A1 || pos === A2 || pos === A3) { + ahits += game.battle.ah[pos] + if (game.battle.attacker_artillery > 0) { + ahits += game.battle.attacker_artillery + } + } + else { + dhits += game.battle.ah[pos] + if (game.battle.defender_artillery > 0) { + dhits += game.battle.defender_artillery + } + } + } + if (ahits & 1) + ahits = (ahits >> 1) + 1 + else + ahits = ahits >> 1 + + if (dhits & 1) + dhits = (dhits >> 1) + 1 + else + dhits = dhits >> 1 + + game.battle.ahits = ahits + game.battle.dhits = dhits + + log_br() + log_hits(game.battle.ahits, "Hit") + game.battle.target = NOBODY + goto_defender_assign_hits() +} + +function continue_engagement() { + for (let pos of battle_strike_positions) { + let lord = game.battle.array[pos] + if (lord !== NOBODY) + if (will_lord_rout(lord)) + rout_lord(lord) + } + + end_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`) +} + +// === 4.4.2 BATTLE ROUNDS: APPLY HITS / PROTECTION / ROLL BY HIT / ROUT === + +function goto_defender_assign_hits() { + set_active_defender() + if (game.battle.ahits === 0) + return end_defender_assign_hits() + + if (no_remaining_targets()) + return end_defender_assign_hits() + + goto_assign_hits() +} + +function goto_assign_hits() { + game.state = "assign_hits" + if (game.battle.target === NOBODY) { + let targets = [] + for (let pos of game.battle.engagements[0]) { + let lord = game.battle.array[pos] + if (is_friendly_lord(lord)) { + targets.push(pos) + } + } + game.battle.target = targets + } +} + +function end_defender_assign_hits() { + log_hits(game.battle.dhits, "Hit") + game.battle.target = NOBODY + goto_attacker_assign_hits() +} + +function no_remaining_targets() { + for (let pos of game.battle.engagements[0]) { + let lord = game.battle.array[pos] + if (is_friendly_lord(lord)) + if (lord_has_unrouted_units(lord)) + return false + } + return true +} + +function goto_attacker_assign_hits() { + set_active_attacker() + if (game.battle.dhits === 0) + return end_attacker_assign_hits() + + if (no_remaining_targets()) + return end_attacker_assign_hits() + + goto_assign_hits() +} + +function end_attacker_assign_hits() { + continue_engagement() +} + +function end_assign_hits() { + for (let pos of game.battle.engagements[0]) { + game.battle.ah[pos] = 0 + } + game.battle.target = NOBODY + game.battle.ahits = 0 + game.battle.dhits = 0 + + goto_next_step() +} + +function for_each_target(fn) { + for (let target of game.battle.target) { + fn(game.battle.array[target]) + } +} + +function prompt_hit_forces() { + for_each_target(lord => { + if (get_lord_forces(lord, RETINUE) > 0) + gen_action_retinue(lord) + if (get_lord_forces(lord, BURGUNDIANS) > 0) + gen_action_burgundians(lord) + if (get_lord_forces(lord, MERCENARIES) > 0) + gen_action_mercenaries(lord) + if (get_lord_forces(lord, LONGBOWMEN) > 0) + gen_action_longbowmen(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) + + for_each_vassal_with_lord(lord, v => { + if (!set_has(game.battle.routed_vassals, v)) + gen_action_vassal(v) + }) + }) +} + +states.assign_hits = { + get inactive() { + return format_strike_step() + " \u2014 Assign " + format_hits() + }, + prompt() { + view.prompt = `${format_strike_step()}: Assign ${format_hits()} to units.` + + prompt_hit_forces() + }, + retinue(lord) { + if ((lord === LORD_MARGARET) && (lord_has_capability(lord, AOW_LANCASTER_YEOMEN_OF_THE_CROWN)) && get_lord_forces(lord, MEN_AT_ARMS) > 0) + action_assign_hits(lord, MEN_AT_ARMS) + else + action_assign_hits(lord, RETINUE) + }, + burgundians(lord) { + action_assign_hits(lord, BURGUNDIANS) + }, + mercenaries(lord) { + action_assign_hits(lord, MERCENARIES) + }, + longbowmen(lord) { + action_assign_hits(lord, LONGBOWMEN) + }, + men_at_arms(lord) { + action_assign_hits(lord, MEN_AT_ARMS) + }, + militia(lord) { + action_assign_hits(lord, MILITIA) + }, + vassal(vassal) { + let lord = get_vassal_lord(vassal) + action_assign_hits(lord, VASSAL, vassal) + }, +} + +function rout_lord(lord) { + log(`L${lord} Routed.`) + + let pos = get_lord_array_position(lord) + + // Remove from battle array + game.battle.array[pos] = NOBODY + set_add(game.battle.routed, lord) +} + +function lord_has_unrouted_troops(lord) { + // Don't check here for Retinue or Vassals. + for (let x = 2; x < FORCE_TYPE_COUNT; x++) { + if (get_lord_forces(lord, x) > 0) + return true + } + return false +} + +function lord_has_routed_troops(lord) { + // Don't check here for Retinue or Vassals. + for (let x = 2; x < FORCE_TYPE_COUNT; x++) { + if (get_lord_routed_forces(lord, x) > 0) + return true + } + return false +} + +function will_lord_rout(lord) { + if (get_lord_routed_forces(lord, RETINUE) > 0) + return true + if (!lord_has_unrouted_troops(lord)) + return true + return false +} + +function rout_unit(lord, type, special) { + if (type === VASSAL) { + rout_vassal(lord, special) + } else { + add_lord_forces(lord, type, -1) + add_lord_routed_forces(lord, type, 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 get_lord_remaining_valour(lord) { + return game.battle.valour[lord] +} + +function spend_valour(lord) { + game.battle.valour[lord] = game.battle.valour[lord] - 1 +} + +function check_protection_capabilities(protection) { + if (game.what === MEN_AT_ARMS || game.what === MILITIA) { + if (lord_has_capability(game.who, AOW_LANCASTER_PIQUIERS) && + (get_lord_routed_forces(game.who, MILITIA) + get_lord_routed_forces(game.who, MEN_AT_ARMS) < 3)) { + protection = 4 + } + } + + if (game.what === MEN_AT_ARMS) { + if (lord_has_capability(game.who, AOW_LANCASTER_CHURCH_BLESSINGS)) { + protection += 1 + } + } + if (game.what === RETINUE) { + if (lord_has_capability(game.who, AOW_LANCASTER_MONTAGU)) + protection += 1 + } + if ((game.what === RETINUE || game.what === VASSAL) && is_archery_step()) { + if (lord_has_capability(game.who, AOW_LANCASTER_BARDED_HORSE)) + protection -= 1 + } + if ((game.what === RETINUE || game.what === VASSAL) && is_melee_step()) { + if (lord_has_capability(game.who, AOW_LANCASTER_BARDED_HORSE)) + protection += 1 + } + + if (game.what === MEN_AT_ARMS) { + if (lord_has_capability(game.who, AOW_YORK_BARRICADES) && has_favoury_marker(game.battle.where)) + protection += 1 + } + if (game.what === MEN_AT_ARMS) { + if (lord_has_capability(game.who, AOW_LANCASTER_CHEVALIERS) && is_archery_step()) { + protection -= 1 + } + } + if (game.what === MILITIA || game.what === LONGBOWMEN) { + if (lord_has_capability(game.who, AOW_YORK_BARRICADES) && has_favoury_marker(game.battle.where)) + protection += 1 + } + return protection +} + +function action_assign_hits(lord, type, special) { + if (game.who !== lord) { + game.who = lord + log(`L${lord}`) + } + let protection = check_protection_capabilities(FORCE_PROTECTION[type]) + let extra = "" + + if (assign_hit_roll(get_force_name(lord, type, special), protection, extra)) { + if (get_lord_remaining_valour(lord) > 0) { + game.state = "spend_valour" + game.what = type + if (game.what === VASSAL) + game.where = special + } else { + rout_unit(lord, type, special) + + // Swift Maneuver event + if (is_swift_maneuver_in_play() && type === RETINUE) { + set_active_enemy() + game.state = "swift_maneuver" + return + } + + finish_action_assign_hits(lord) + } + } else { + finish_action_assign_hits(lord) + } +} + +function finish_action_assign_hits(lord) { + if (game.battle.ahits) + game.battle.ahits-- + else + game.battle.dhits-- + + if (!lord_has_unrouted_units(lord)) { + game.battle.target = NOBODY + } + + if (game.active === game.battle.attacker) + goto_attacker_assign_hits() + else + goto_defender_assign_hits() +} + +states.spend_valour = { + inactive: "Spend Valour", + prompt() { + view.prompt = `Spend Valour: Reroll Hit on ${get_force_name(game.who, game.what, game.where)}?` + gen_action("valour", game.who) + view.actions.pass = 1 + }, + pass() { + rout_unit(game.who, game.what, game.where) + finish_action_assign_hits(game.who) + }, + valour() { + let protection = check_protection_capabilities(FORCE_PROTECTION[game.what]) + + spend_valour(game.who) + log(`Reroll:`) + if (assign_hit_roll(get_force_name(game.who, game.what, game.where), protection, "")) { + rout_unit(game.who, game.what, game.where) + finish_action_assign_hits(game.who) + } else { + finish_action_assign_hits(game.who) + } + }, +} + +// === 4.4.2 BATTLE ROUNDS: NEW ROUND === + +function goto_end_battle_round() { + end_battle_round() +} + +function end_battle_round() { + game.battle.ravine = NOBODY + let attacker_loser = null + set_active_attacker() + if (has_no_unrouted_forces()) { + attacker_loser = game.active + } + + let defender_loser = null + set_active_defender() + if (has_no_unrouted_forces()) { + defender_loser = game.active + } + + if (attacker_loser !== null || defender_loser !== null) { + if (attacker_loser === null) + game.battle.loser = defender_loser + else if (defender_loser === null) + game.battle.loser = attacker_loser + else + game.battle.loser = BOTH + + end_battle() + return + } + + game.battle.round++ + + game.battle.ambush = 0 + + // TODO: goto_battle_rounds() instead? + set_active_defender() + goto_flee() +} + +// === 4.4.3 ENDING THE BATTLE === + +// Ending the Battle - optimized from rules as written +// Loser retreat / withdraw / remove +// Loser losses +// Loser service +// Victor losses +// Victor spoils + +function set_active_loser() { + set_active(game.battle.loser) +} + +function set_active_victor() { + if (game.battle.loser === YORK) + set_active(LANCASTER) + else + set_active(YORK) +} + +function end_battle() { + if (game.battle.loser === BOTH) + log_h4(`Both Sides Lost`) + else + log_h4(`${game.battle.loser} Lost`) + + game.battle.array = 0 + game.battle.caltrops = -1 + goto_battle_influence() +} + +function has_defeated_lords() { + for (let lord of game.battle.fled) + if (is_friendly_lord(lord)) + return true + for (let lord of game.battle.routed) + if (is_friendly_lord(lord)) + return true + return false +} + +// === 4.4.3 ENDING THE BATTLE: INFLUENCE === + +function goto_battle_influence() { + if (game.battle.loser !== BOTH) { + set_active_loser() + + let influence = 0 + for (let lord of game.battle.fled) + if (is_friendly_lord(lord)) + influence += data.lords[lord].influence + count_vassals_with_lord(lord) + for (let lord of game.battle.routed) + if (is_friendly_lord(lord)) + influence += data.lords[lord].influence + count_vassals_with_lord(lord) + + reduce_influence(influence) + goto_battle_spoils() + } else { + goto_death_or_disband() + } +} + +// === 4.4.3 ENDING THE BATTLE: LOSSES === + +function has_battle_losses() { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) + if (lord_has_routed_troops(lord)) + return true + return false +} + +function goto_battle_losses_victor() { + set_active_victor() + game.who = NOBODY + if (has_battle_losses()) + log_h4(`${game.active} Losses`) + resume_battle_losses() +} + +function resume_battle_losses() { + game.state = "battle_losses" + if (!has_battle_losses()) + goto_death_or_disband() +} + +function action_losses(lord, type) { + let protection = FORCE_PROTECTION[type] + + if (game.who !== lord) { + log(`L${lord}`) + game.who = lord + } + + let die = roll_die() + if (die <= protection) { + logi(`${get_force_name(lord, type)} ${range(protection)}: ${MISS[die]}`) + add_lord_routed_forces(lord, type, -1) + add_lord_forces(lord, type, 1) + } else { + logi(`${get_force_name(lord, type)} ${range(protection)}: ${HIT[die]}`) + add_lord_routed_forces(lord, type, -1) + } + + resume_battle_losses() +} + +states.battle_losses = { + inactive: "Losses", + prompt() { + let done = true + 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_troops(lord)) { + if (get_lord_routed_forces(lord, MERCENARIES) > 0) + gen_action_routed_mercenaries(lord) + if (get_lord_routed_forces(lord, LONGBOWMEN) > 0) + gen_action_routed_longbowmen(lord) + if (get_lord_routed_forces(lord, BURGUNDIANS) > 0) + gen_action_routed_burgundians(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) + done = false + } + } + if (done) { + view.actions.done = 1 + } + }, + routed_mercenaries(lord) { + action_losses(lord, MERCENARIES) + }, + routed_longbowmen(lord) { + action_losses(lord, LONGBOWMEN) + }, + routed_burgundians(lord) { + action_losses(lord, BURGUNDIANS) + }, + routed_men_at_arms(lord) { + action_losses(lord, MEN_AT_ARMS) + }, + routed_militia(lord) { + action_losses(lord, MILITIA) + }, + done() { + goto_death_or_disband() + }, +} + +// === 4.4.3 ENDING THE BATTLE: SPOILS === + +function log_spoils() { + if (game.spoils[PROV] > 0) + logi(game.spoils[PROV] + " Provender") + if (game.spoils[CART] > 0) + logi(game.spoils[CART] + " Cart") +} + +function calculate_spoils() { + let n_prov = 0 + let n_cart = 0 + + if (has_favour_in_locale(game.battle.loser, game.battle.where)) + return + + for (let lord of game.battle.fled) { + if (is_enemy_lord(lord)) { + n_prov += get_lord_assets(lord, PROV) + n_cart += get_lord_assets(lord, CART) + } + } + + for (let lord of game.battle.routed) { + if (is_enemy_lord(lord)) { + n_prov += get_lord_assets(lord, PROV) + n_cart += get_lord_assets(lord, CART) + } + } + + if (is_neutral_locale(game.battle.where)) { + n_prov = Math.ceil(n_prov / 2) + n_cart = Math.ceil(n_cart / 2) + } + + add_spoils(PROV, n_prov) + add_spoils(CART, n_cart) +} + +function find_lone_victor() { + let found = NOBODY + for (let pos of battle_strike_positions) { + let lord = game.battle.array[pos] + if (is_friendly_lord(lord)) { + if (found !== NOBODY) + return NOBODY + found = lord + } + } + return found +} + +function goto_battle_spoils() { + set_active_victor() + // determine Battle Spoils + calculate_spoils() + if (has_any_spoils() && has_friendly_lord(game.battle.where)) { + log_h4("Spoils") + log_spoils() + game.state = "battle_spoils" + game.who = find_lone_victor() + } else { + end_battle_spoils() + } +} + +function end_battle_spoils() { + game.who = NOBODY + game.spoils = 0 + + goto_battle_losses_victor() +} + +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() { + push_undo_without_who() + take_spoils(PROV) + }, + + take_cart() { + push_undo_without_who() + take_spoils(CART) + }, + + end_spoils() { + end_battle_spoils() + }, +} + +// === 4.4.3 ENDING THE BATTLE: DEATH CHECK AND DISBAND === + +function goto_death_or_disband() { + remove_battle_capability_troops() + if (has_defeated_lords()) { + if (game.battle.loser === LANCASTER && lord_has_capability(LORD_RICHARD_III, AOW_YORK_BLOODY_THOU_ART) && get_lord_locale(LORD_RICHARD_III) === game.battle.where) { + game.flags.bloody = 1 + } + game.state = "death_check" + } + else + end_death_or_disband() +} + +function end_death_or_disband() { + set_active_enemy() + if (has_defeated_lords()) { + goto_death_or_disband() + } else { + goto_battle_aftermath() + } +} + +function prompt_held_event_death_check() { + // both attacker and defender events + if (game.active === LANCASTER) { + if (can_play_escape_ship()) + gen_action_card_if_held(EVENT_LANCASTER_ESCAPE_SHIP) + if (can_play_warden_of_the_marches()) + gen_action_card_if_held(EVENT_LANCASTER_WARDEN_OF_THE_MARCHES) + if (can_play_talbot_to_the_rescue()) + gen_action_card_if_held(EVENT_LANCASTER_TALBOT_TO_THE_RESCUE) + } + if (game.active === YORK) { + if (can_play_escape_ship()) + gen_action_card_if_held(EVENT_YORK_ESCAPE_SHIP) + } +} + +states.death_check = { + inactive: "Death or Disband", + prompt() { + view.prompt = `Death or Disband: Select lords to roll for Death or Disband.` + + prompt_held_event_death_check() + + let done = true + for (let lord of game.battle.fled) { + if (is_friendly_lord(lord)) { + gen_action_lord(lord) + done = false + } + } + for (let lord of game.battle.routed) { + if (is_friendly_lord(lord)) { + gen_action_lord(lord) + done = false + } + } + if (game.flags.warden_of_the_marches && game.active === LANCASTER) { + done = true + } + if (done) { + view.actions.done = 1 + } + }, + lord(lord) { + let threshold = 2 + let modifier = 0 + + // CAPABILITY: BLOODY THOU ART, BLOODY WILL BE THE END + if (is_lancaster_lord(lord) && game.flags.bloody === 1) { + logcap(AOW_YORK_BLOODY_THOU_ART) + disband_lord(lord, true) + set_delete(game.battle.fled, lord) + set_delete(game.battle.routed, lord) + } + else { + let roll = roll_die() + if (set_has(game.battle.fled, lord)) + modifier = -2 + + let success = threshold >= roll + modifier + log(`Lord ${lord_name[lord]} ${success ? "Survived" : "Died"}: (${range(2)}) ${success ? HIT[roll] : MISS[roll]} ${modifier < 0 ? "(-2 Fled)" : ""}`) + disband_lord(lord, !success) + set_delete(game.battle.fled, lord) + set_delete(game.battle.routed, lord) + } + }, + done() { + end_death_or_disband() + }, + card: action_held_event, +} + +// === DEATH CHECK EVENT: ESCAPE SHIP === + +function can_play_escape_ship() { + return can_escape_at(game.battle.where) +} + +function can_escape_at(here) { + if (game.active === YORK && has_favoury_marker(here) && is_seaport(here)) + return true + if (game.active === LANCASTER && has_favourl_marker(here) && is_seaport(here)) + return true + if (search_escape_route(here)) + return true + return false +} + +function search_escape_route(start) { + search_seen.fill(0) + search_seen[start] = 1 + let queue = [start] + + while (queue.length > 0) { + let here = queue.shift() + + // Check if the current locale is a seaport + if (is_seaport(here)) { + return true + } + + if (is_friendly_locale(here)) { + for (let next of data.locales[here].adjacent) { + if (!search_seen[next]) { + search_seen[next] = 1 + queue.push(next) + } + } + } + } + return false +} + +function goto_play_escape_ship() { + game.state = "escape_ship" + game.who = NOBODY +} + +states.escape_ship = { + inactive: `Escape ship`, + prompt() { + view.prompt = "Escape Ship: Your lords go to Exile." + view.actions.escape_ship = 1 + }, + escape_ship() { + push_undo() + for (let lord of game.battle.fled) { + log(`${lord_name[lord]} went to exile.`) + exile_lord(lord) + set_delete(game.battle.fled, lord) + } + for (let lord of game.battle.routed) { + log(`${lord_name[lord]} went to exile.`) + exile_lord(lord) + set_delete(game.battle.routed, lord) + } + game.state = "death_check" + }, +} + +// === DEATH CHECK EVENT: TALBOT TO THE RESCUE === + +function can_play_talbot_to_the_rescue() { + return true +} + +function goto_play_talbot_to_the_rescue() { + throw "TODO" +} + +// === DEATH CHECK EVENT: WARDEN OF THE MARCHES === + +function can_play_warden_of_the_marches() { + // TOOD : Maybe a bug with blocked ford/exile ? + let can_play = false + for (let loc = first_locale; loc <= last_locale; loc++) { + if (is_friendly_locale(loc) && is_north(loc) && loc !== game.battle.where) { + can_play = true + } + } + if (!can_play) { + return false + } + // if blocked ford then flee + if (is_north(game.where)) + return true + // if battle + if (is_north(game.battle.where)) + return true + return false +} + +function goto_play_warden_of_the_marches() { + push_undo() + game.state = "warden_of_the_marches" +} + +states.warden_of_the_marches = { + inactive: "Warden of the Marches", + prompt() { + let done = true + view.prompt = "Warden of the Marches: Select a friendly locale in the North to move routed lords there" + for (let loc = first_locale; loc <= last_locale; loc++) { + if (is_friendly_locale(loc) && is_north(loc) && loc !== game.battle.where) { + done = false + gen_action_locale(loc) + } + } + if (done) { + view.actions.done = 1 + } + }, + locale(loc) { + push_undo() + for (let lord = first_lancaster_lord; lord <= last_lancaster_lord; lord++) { + if (get_lord_locale(lord) === game.battle.where) { + set_lord_locale(lord, loc) + if (!lord_has_unrouted_troops(lord)) { + disband_lord(lord, false) + } + else { + set_lord_forces(lord, RETINUE, 1) + } + for (let x = 0; x < FORCE_TYPE_COUNT; ++x) { + set_lord_forces(lord, x, 0) + if (get_lord_routed_forces(lord, x) > 0) { + set_lord_routed_forces(lord, x, 0) + } + } + for_each_vassal_with_lord(lord, v => { + if (set_has(game.battle.routed_vassals, v)) { + array_remove(game.battle.routed_vassals, v) + disband_vassal(v) + } + }) + } + } + logi(`Moved to ${data.locales[loc].name}`) + end_warden_of_the_marches() + }, + done() { + end_warden_of_the_marches() + }, +} + +function end_warden_of_the_marches() { + game.flags.warden_of_the_marches = 1 + game.who = NOBODY + game.state = "death_check" +} + +// === 4.4.4 ENDING THE BATTLE: AFTERMATH === + +function goto_battle_aftermath() { + set_active(game.battle.attacker) + game.where = NOWHERE + + // Routed Vassals get disbanded + for (let lord = first_lord; lord <= last_lord; lord++) { + if (is_lord_on_map(lord)) { + for_each_vassal_with_lord(lord, v => { + if (set_has(game.battle.routed_vassals, v)) { + array_remove(game.battle.routed_vassals, v) + disband_vassal(v) + } + }) + } + } + + // Events + discard_events("hold") + + // Recovery + spend_all_actions() + game.where = NOWHERE + game.battle = 0 + game.flags.bloody = 0 + end_march() +} + +// === 4.7 FEED === + +function can_feed_from_shared(lord) { + let loc = get_lord_locale(lord) + return get_shared_assets(loc, PROV) > 0 +} + +function can_pay_from_shared(lord) { + let loc = get_lord_locale(lord) + return get_shared_assets(loc, COIN) > 0 +} + +function has_friendly_lord_who_must_feed() { + if (is_campaign_phase() && game.flags.supply_depot === 1 && game.active === LANCASTER) { + game.flags.supply_depot = 0 + logi(`No feed ${EVENT_LANCASTER_REBEL_SUPPLY_DEPOT}`) + return false + } + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) + if (is_lord_unfed(lord)) + return true + return false +} + +function set_lord_feed_requirements() { + // Count how much food each lord needs + let n = 0 + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (get_lord_moved(lord)) { + n = Math.ceil(count_lord_all_forces(lord) / 6) + set_lord_unfed(lord, n) + } else + set_lord_unfed(lord, 0) + } +} + +function goto_feed() { + log_br() + set_lord_feed_requirements() + if (has_friendly_lord_who_must_feed()) { + push_state("feed") + } else { + // TODO: clean up transitions for End Command -> Feed and Disembark -> Feed + if (game.state !== "disembark") + goto_remove_markers() + } +} + +states.feed = { + inactive: "Feed", + prompt() { + view.prompt = "Feed: You must Feed Lords who Moved or Fought." + + let done = true + // 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 pillage to feed your troops.` + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (is_lord_unfed(lord)) { + view.actions.pillage = 1 + 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" + }, + pillage() { + push_undo() + set_lord_feed_requirements() + goto_pillage_food() + }, + 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() { + pop_state() + game.where = NOWHERE + // TODO: clean up transitions for End Command -> Feed and Disembark -> Feed + if (game.state !== "disembark") + goto_remove_markers() +} + +// === 4.7 FEED: REMOVE MARKERS === + +function goto_remove_markers() { + clear_lords_moved() + goto_command_activation() +} + +// === 4.8 END CAMPAIGN === + +function goto_end_campaign() { + log_h1("End Campaign") + set_active(P1) + goto_tides_of_war() +} + +// === 4.8.1 END CAMPAIGN: TIDES OF WAR === + +function tides_calc() { + log_h3(`Tides of War`) + let town = 0 + let cities = 0 + let fortress = 0 + let domy = 0 + let doml = 0 + let domnl = 0 + let domny = 0 + let domsl = 0 + let domsy = 0 + let domwl = 0 + let domwy = 0 + let prenl = 0 + let preny = 0 + let presl = 0 + let presy = 0 + let prewl = 0 + let prewy = 0 + let prel = 0 + let prey = 0 + + // DOMINATION CALC + + for (let x of all_north_locales) { + if (has_favourl_marker(x)) { + domnl += 1 + } + if (has_favoury_marker(x)) { + domny += 1 + } + } + + for (let x of all_south_locales) { + if (has_favourl_marker(x)) { + domsl += 1 + } + if (has_favoury_marker(x)) { + domsy += 1 + } + } + + for (let x of all_wales_locales) { + if (has_favourl_marker(x)) { + domwl += 1 + } + if (has_favoury_marker(x)) { + domwy += 1 + } + } + + // SPECIAL LOCALES + + if (has_favourl_marker(LOC_LONDON)) { + log(`London control 2 Influence for Lancaster`) + doml += 2 + } + if (has_favoury_marker(LOC_LONDON)) { + log(`London control 2 Influence for York`) + domy += 2 + } + + if (has_favourl_marker(LOC_CALAIS)) { + log(`Calais control 2 Influence for Lancastrians`) + doml += 2 + } + if (has_favoury_marker(LOC_CALAIS)) { + log(`Calais control 2 Influence for York`) + domy += 2 + } + + if (has_favourl_marker(LOC_HARLECH)) { + log(`Harlech control 1 Influence for Lancaster`) + doml += 1 + } + if (has_favoury_marker(LOC_HARLECH)) { + log(`Harlech control 1 Influence for York`) + domy += 1 + } + + for (let x of all_city_locales) { + if (has_favourl_marker(x)) { + cities -= 1 + } + if (has_favoury_marker(x)) { + cities += 1 + } + } + + for (let x of all_town_locales) { + if (has_favourl_marker(x)) { + town -= 1 + } + if (has_favoury_marker(x)) { + town += 1 + } + } + + for (let x of all_fortress_locales) { + if (has_favourl_marker(x)) { + fortress -= 1 + } + if (has_favoury_marker(x)) { + fortress += 1 + } + } + + // DOMINATION CAPS + + // NORTH + + if (domnl === 6) { + log(`North Domination 2 Influence for Lancaster`) + doml += 2 + } else if (domnl >= 3 && lord_has_capability(LORD_NORTHUMBERLAND_L, AOW_LANCASTER_NORTHMEN)) { + log(`North Domination 2 Influence for Lancaster`) + doml += 2 + } + + if (domny === 6) { + log(`North Domination 2 Influence for York`) + domy += 2 + } + + // SOUTH + + if (domsl === 9) { + log(`South Domination 2 Influence for Lancaster`) + doml += 2 + } + + if (domsy === 9) { + log(`South Domination 2 Influence for York`) + domy += 2 + } else if ( + domsy >= 5 && + (lord_has_capability(LORD_MARCH, AOW_YORK_SOUTHERNERS) || + lord_has_capability(LORD_RUTLAND, AOW_YORK_SOUTHERNERS) || + lord_has_capability(LORD_YORK, AOW_YORK_SOUTHERNERS)) + ) { + log(`South Domination 2 Influence for York`) + domy += 2 + } + + // WALES + + if (domwl === 5) { + log(`Wales Domination 2 Influence for Lancaster`) + doml += 2 + } + + if (domwy === 5) { + log(`Wales Domination 2 Influence for York`) + domy += 2 + } else if ( + domwy >= 3 && + (lord_has_capability(LORD_MARCH, AOW_YORK_WELSHMEN) || lord_has_capability(LORD_YORK, AOW_YORK_WELSHMEN)) + ) { + log(`Wales Domination 2 Influence for York`) + domy += 2 + } + + // LOCALES TUG OF WAR + + if (cities >= 1) { + log(`Most Cities 2 Influence for York`) + domy += 2 + } + if (fortress >= 1) { + log(`Most Fortresses 1 Influence for York`) + domy += 1 + } + if (town >= 1) { + log(`Most Towns 1 Influence for York`) + domy += 1 + } + + if (cities <= -1) { + log(`Most Cities 2 Influence for Lancaster`) + doml += 2 + } + if (fortress <= -1) { + log(`Most Fortresses 1 Influence for Lancaster`) + doml += 1 + } + if (town <= -1) { + log(`Most Towns 1 Influence for Lancaster`) + doml += 1 + } + + // LORD PRESENCE + for (let lord = first_lancaster_lord; lord <= last_lancaster_lord; ++lord) { + if (is_lord_on_map(lord)) { + if (is_lord_in_north(lord)) { + prenl = 1 + } + if (is_lord_in_south(lord)) { + presl = 1 + } + if (is_lord_in_wales(lord)) { + prewl = 1 + } + } + } + for (let lord = first_york_lord; lord <= last_york_lord; ++lord) { + if (is_lord_on_map(lord)) { + if (is_lord_in_north(lord)) { + preny = 1 + } + if (is_lord_in_south(lord)) { + presy = 1 + } + if (is_lord_in_wales(lord)) { + prewy = 1 + } + } + } + prel = prenl + presl + prewl + prey = preny + presy + prewy + + log("Presence in Areas : " + prel + " for Lancaster") + log("Presence in Areas : " + prey + " for Yorkists") + + domy += preny + presy + prewy + doml += prenl + presl + prewl + + // CAPS EFFECT + + if ( + lord_has_capability(LORD_HENRY_VI, AOW_LANCASTER_MARGARET) && + get_lord_locale(LORD_HENRY_VI) !== LOC_LONDON && + is_lord_on_map(LORD_HENRY_VI) + ) { + log(`Capability: Margaret 2 Influence for Lancaster`) + doml += 2 + } + + if ( + lord_has_capability(LORD_EXETER_1, AOW_LANCASTER_COUNCIL_MEMBER) || + lord_has_capability(LORD_EXETER_2, AOW_LANCASTER_COUNCIL_MEMBER) || + lord_has_capability(LORD_SOMERSET_2, AOW_LANCASTER_COUNCIL_MEMBER) || + lord_has_capability(LORD_SOMERSET_1, AOW_LANCASTER_COUNCIL_MEMBER) || + lord_has_capability(LORD_BUCKINGHAM, AOW_LANCASTER_COUNCIL_MEMBER) + ) { + log(`Capability: Council Member 1 Influence for Lancaster`) + doml += 1 + } + + if (lord_has_capability(LORD_EDWARD_IV, AOW_YORK_FIRST_SON)) { + log(`Capability: First Son 1 Influence for York`) + domy += 1 + } + + if (set_has(INFLUENCE_TURNS, current_turn())) { + for (let y = first_york_lord; y <= last_york_lord; y++) { + if (is_lord_on_map(y)) { + domy += data.lords[y].influence + log(`Gain Lords Influence : Yorkists gain ${data.lords[y].influence} for ${data.lords[y].name}`) + } + } + + for (let l = first_lancaster_lord; l <= last_lancaster_lord; l++) { + if (is_lord_on_map(l)) { + doml += data.lords[l].influence + log(`Gain Lords Influence : Lancastrians gain ${data.lords[l].influence} for ${data.lords[l].name}`) + } + } + } + + log(`Total ` + domy + ` Influence for York`) + log(`Total ` + doml + ` Influence for Lancaster`) + + game.influence += doml + game.influence -= domy +} + +function goto_tides_of_war() { + if (lord_has_capability(LORD_BUCKINGHAM, AOW_LANCASTER_STAFFORD_ESTATES)) { + add_lord_assets(LORD_BUCKINGHAM, COIN, 1) + add_lord_assets(LORD_BUCKINGHAM, PROV, 1) + } + tides_calc() + if (eligible_charity()) { + goto_we_done_deeds_of_charity() + } + else + goto_disembark() +} + +// === 4.8.2 END CAMPAIGN: DISEMBARK === + +function has_lords_at_sea() { + for (let x = first_friendly_lord; x <= last_friendly_lord; x++) { + if (is_lord_at_sea(x)) + return true + } + return false +} + +function is_lord_at_sea(lord) { + let here = get_lord_locale(lord) + return here === LOC_NORTH_SEA || here === LOC_IRISH_SEA || here === LOC_ENGLISH_CHANNEL +} + +function goto_disembark() { + if (has_lords_at_sea()) { + game.state = "disembark" + } else { + end_disembark() + } +} + +function end_disembark() { + game.who = NOBODY + set_active_enemy() + if (has_lords_at_sea()) + goto_disembark() + else { + set_active(P1) + goto_victory_check() + } +} + +function do_disembark() { + let roll = roll_die() + let success = roll >= 5 + + log(`Disembark: (>4) ${success ? HIT[roll] : MISS[roll]}`) + + return success +} + +function has_safe_ports(sea) { + for (let loc of find_ports(sea)) + if (!has_enemy_lord(loc)) + return true + return false +} + +states.disembark = { + inactive: "Disembark", + prompt() { + view.prompt = "Disembark your lords at sea." + let done = true + if (game.who === NOBODY) { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; lord++) { + if (is_lord_at_sea(lord)) { + gen_action_lord(lord) + done = false + } + } + } else { + let sea = get_lord_locale(game.who) + for (let loc of find_ports(sea)) + if (!has_enemy_lord(loc)) + gen_action_locale(loc) + } + if (done) { + view.actions.done = 1 + } + }, + lord(lord) { + if (do_disembark()) { + if (has_safe_ports(get_lord_locale(lord))) { + game.who = lord + } else { + no_safe_disembark(lord) + } + } else { + shipwreck(lord) + } + }, + locale(loc) { + successful_disembark(game.who, loc) + }, + done() { + end_disembark() + }, +} + +function successful_disembark(lord, loc) { + set_lord_locale(lord, loc) + set_lord_moved(lord, 1) + levy_burgundians(lord) + game.who = NOBODY + goto_feed() +} + +function shipwreck(lord) { + disband_influence_penalty(lord) + disband_lord(lord, true) +} + +function no_safe_disembark(lord) { + disband_lord(lord) +} + +function disband_influence_penalty(lord) { + let influence = data.lords[lord].influence + + for (let v = first_vassal; v <= last_vassal; v++) { + if (is_vassal_mustered_with(v, lord)) { + influence += 1 + } + } + + if (game.active === LANCASTER) + game.ip -= influence + else + game.ip += influence +} + +function goto_advance_campaign() { + game.turn++ + + log_h1("Levy " + current_turn_name()) + goto_levy_arts_of_war() +} + +// === 4.8.3 END CAMPAIGN: VICTORY CHECK === + +function goto_victory_check() { + if (!(check_scenario_end_victory() || check_campaign_victory() || check_threshold_victory())) { + if (set_has(GROW_TURNS, current_turn())) { + do_grow() + } else if (set_has(WASTE_TURNS, current_turn())) { + do_waste() + } else { + goto_reset() + } + } +} + +// === 4.8.4 END CAMPAIGN: GROW === + +function do_grow() { + log("Grow:") + logi("Changing all Depleted locales to Normal.") + logi("Changing all Exhausted locales to Depleted.") + + for (let x = first_locale; x <= last_locale; x++) { + refresh_locale(x) + } + goto_reset() +} + +// === 4.8.5 END CAMPAIGN: WASTE === + +function do_waste() { + log("Waste:") + logi("Removing half of all lords provinder, carts, and ships.") + logi("Resetting Lords Coin and Troops to initial values.") + for (let x = first_lord; x <= last_lord; x++) { + if (is_lord_on_map(x)) { + do_lord_waste(x) + } + } + + goto_reset() +} + +function do_lord_waste(lord) { + remove_half(lord, PROV) + remove_half(lord, CART) + remove_half(lord, SHIP) + set_lord_assets(lord, COIN, data.lords[lord].assets.coin) + muster_lord_forces(lord) +} + +function remove_half(lord, type) { + set_lord_assets(lord, type, Math.ceil(get_lord_assets(lord, type) / 2)) +} + +// === 4.8.6 END CAMPAIGN: RESET (DISCARD ARTS OF WAR) === + +function goto_reset() { + game.state = "reset" + + // Discard "This Campaign" events from play. + discard_friendly_events("this_campaign") +} + +states.reset = { + inactive: "Reset", + prompt() { + view.prompt = "Reset: You may discard any held Arts of War cards desired." + if (game.active === YORK) { + for (let c = first_york_card; c <= last_york_card; ++c) { + if (set_has(game.hand_y, c)) { + gen_action_card(c) + } + } + } + if (game.active === LANCASTER) { + for (let c = first_lancaster_card; c <= last_lancaster_card; ++c) { + if (set_has(game.hand_l, c)) { + gen_action_card(c) + } + } + } + view.actions.end_discard = 1 + }, + card(c) { + push_undo() + if (set_has(game.hand_y, c)) { + log("Discarded Held card.") + set_delete(game.hand_y, c) + } else if (set_has(game.hand_l, c)) { + log("Discarded Held card.") + set_delete(game.hand_l, c) + } + }, + end_discard() { + end_reset() + }, +} + +function end_reset() { + set_active_enemy() + if (game.active === P2) + goto_reset() + else + goto_advance_campaign() +} + +// === 5.1 CAMPAIGN VICTORY === + +function check_campaign_victory_york(inc_exiles = false) { + for (let lord = first_lancaster_lord; lord <= last_lancaster_lord; ++lord) + if ( + is_lord_on_map(lord) || + (inc_exiles && get_lord_locale(lord) === CALENDAR + current_turn() + 1 && get_lord_in_exile(lord)) + ) + return false + return true +} + +function check_campaign_victory_lancaster(inc_exiles = false) { + for (let lord = first_york_lord; lord <= last_york_lord; ++lord) + if ( + is_lord_on_map(lord) || + (inc_exiles && get_lord_locale(lord) === CALENDAR + current_turn() + 1 && get_lord_in_exile(lord)) + ) + return false + return true +} + +function check_campaign_victory() { + let york_v = check_campaign_victory_york(true) + let lancaster_v = check_campaign_victory_lancaster(true) + + if (york_v && lancaster_v) { + goto_game_over("Draw", "The game ended in a draw.") + return true + } else if (york_v) { + goto_game_over(YORK, `${YORK} won a Campaign Victory!`) + return true + } else if (lancaster_v) { + goto_game_over(LANCASTER, `${LANCASTER} won a Campaign Victory!`) + return true + } + + return false +} + +function check_disband_victory() { + let york_v = check_campaign_victory_york() + let lancaster_v = check_campaign_victory_lancaster() + + if (york_v && lancaster_v) { + goto_game_over("Draw", "The game ended in a draw.") + return true + } else if (york_v) { + goto_game_over(YORK, `${YORK} won a Campaign Victory!`) + return true + } else if (lancaster_v) { + goto_game_over(LANCASTER, `${LANCASTER} won a Campaign Victory!`) + return true + } + + return false +} + +// === 5.2 THRESHOLD VICTORY === + +function check_threshold_victory() { + // This needs to change to account for graduated victory thresholds in some scenarios. + + if (Math.abs(game.ip) > game.victory_check) { + if (game.ip > 0) + goto_game_over(LANCASTER, `${LANCASTER} won with ${game.ip} Influence.`) + else + goto_game_over(YORK, `${YORK} won with ${Math.abs(game.ip)} Influence.`) + + return true + } + + return false +} + +// === 5.3 SCENARIO END VICTORY === + +function check_scenario_end_victory() { + if (current_turn() === scenario_last_turn[game.scenario]) { + // Scenario End Victory + + if (game.ip === 0) + goto_game_over("Draw", "The game ended in a draw.") + else if (game.ip > 0) + goto_game_over(LANCASTER, `${LANCASTER} won with ${game.ip} Influence.`) + else + goto_game_over(YORK, `${YORK} won with ${Math.abs(game.ip)} Influence.`) + + return true + } + return false +} + +// === 6.0 SCENARIOS === + +exports.scenarios = [ + "Ia. Henry VI", + "Ib. Towton", + "Ic. Somerset's Return", + "II. Warwicks' Rebellion", + "III. My Kingdom for a Horse", +// TODO "I-III. Wars of the Roses", +] + +const scenario_last_turn = { + "Ia. Henry VI": 15, + "Ib. Towton": 2, + "Ic. Somerset's Return": 8, + "II. Warwicks' Rebellion": 15, + "III. My Kingdom for a Horse": 15, + "I-III. Wars of the Roses": 15, +} + +function is_card_in_scenario(_c) { + // TODO: Cards setup + return true +} + +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, LONGBOWMEN, info.forces.longbowmen | 0) + set_lord_forces(lord, MILITIA, info.forces.militia | 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.ships | 0) + + muster_lord_forces(lord) +} + +exports.setup = function (seed, scenario, options) { + game = { + seed, + scenario, + hidden: options.hidden ? 1 : 0, + + log: [], + undo: [], + + active: null, + rebel: null, + state: "setup_lords", + stack: [], + victory_check: 0, + influence: 0, + + hand_y: [], + hand_l: [], + plan_y: [], + plan_l: [], + + turn: 0, + events: [], // this levy/this campaign cards + + pieces: { + // per lord data + locale: [], + assets: [], + forces: [], + routed: [], + capabilities: [], // TODO map card -> lord instead of lord+slot -> card + moved: [], + in_exile: 0, + + // per vassal data + vassals: Array(vassal_count).fill(VASSAL_OUT_OF_PLAY), + + // per locale data + depleted: [], + exhausted: [], + favourl: [], + favoury: [], + }, + + flags: { + first_action: 0, + first_march_highway: 0, + free_levy: 0, + free_parley_henry:0, + free_parley_gloucester:0, + burgundians:0, + bloody:0, + london_for_york:0, + surprise_landing:0, + sail_to_port: 0, + march_to_port: 0, + parliament_votes:0, + succession:0, + jack_cade:0, + commons_militia:0, + swap_battle_attacker:0, + supply_depot:0, + loyalty_and_trust:0, + warden_of_the_marches:0, + naval_blockade:0 + }, + + command: NOBODY, + actions: 0, + group: 0, + intercept_group: 0, + who: NOBODY, + where: NOWHERE, + what: NOTHING, + which: NOTHING, + count: 0, + event_data: 0, + + supply: 0, + march: 0, + battle: 0, + spoils: 0, + parley: 0, + } + + log_h1(scenario) + + switch (scenario) { + default: + case "Ia. Henry VI": + setup_Ia() + break + case "Ib. Towton": + setup_Ib() + break + case "Ic. Somerset's Return": + setup_Ic() + break + case "II. Warwicks' Rebellion": + setup_II() + break + case "III. My Kingdom for a Horse": + setup_III() + break + case "I-III. Wars of the Roses": + setup_ItoIII() + break + } + + update_aliases() + + goto_setup_lords() + + return game +} + +function setup_Ia() { + game.turn = 1 << 1 + + game.rebel = YORK + game.active = YORK + game.victory_check = 40 + game.influence = 0 + muster_lord(LORD_YORK, LOC_ELY) + muster_lord(LORD_MARCH, LOC_LUDLOW) + muster_lord(LORD_HENRY_VI, LOC_LONDON) + muster_lord(LORD_SOMERSET_1, LOC_LONDON) + + set_lord_calendar(LORD_NORTHUMBERLAND_L, 2) + set_lord_calendar(LORD_EXETER_1, 3) + set_lord_calendar(LORD_BUCKINGHAM, 5) + set_lord_calendar(LORD_SALISBURY, 2) + set_lord_calendar(LORD_WARWICK_Y, 3) + set_lord_calendar(LORD_RUTLAND, 5) + + add_favourl_marker(LOC_LONDON) + add_favourl_marker(LOC_WELLS) + add_favourl_marker(LOC_SCOTLAND) + add_favourl_marker(LOC_FRANCE) + + add_favoury_marker(LOC_ELY) + add_favoury_marker(LOC_LUDLOW) + add_favoury_marker(LOC_BURGUNDY) + add_favoury_marker(LOC_IRELAND) + + setup_vassals() +} + +function setup_Ib() { + game.turn = 1 << 1 + + game.rebel = YORK + game.active = YORK + game.victory_check = 45 + game.influence = 0 + muster_lord(LORD_NORFOLK, LOC_LONDON) + muster_lord(LORD_WARWICK_Y, LOC_LONDON) + muster_lord(LORD_MARCH, LOC_LUDLOW) + muster_lord(LORD_EXETER_1, LOC_NEWCASTLE) + muster_lord(LORD_SOMERSET_1, LOC_NEWCASTLE) + muster_lord(LORD_NORTHUMBERLAND_L, LOC_CARLISLE) + + add_favourl_marker(LOC_ST_ALBANS) + add_favourl_marker(LOC_SCARBOROUGH) + add_favourl_marker(LOC_NEWCASTLE) + add_favourl_marker(LOC_BAMBURGH) + add_favourl_marker(LOC_HEXHAM) + add_favourl_marker(LOC_APPLEBY) + add_favourl_marker(LOC_CARLISLE) + add_favourl_marker(LOC_SCOTLAND) + add_favourl_marker(LOC_FRANCE) + + add_favoury_marker(LOC_LONDON) + add_favoury_marker(LOC_CALAIS) + add_favoury_marker(LOC_GLOUCESTER) + add_favoury_marker(LOC_HEREFORD) + add_favoury_marker(LOC_OXFORD) + add_favoury_marker(LOC_SALISBURY) + add_favoury_marker(LOC_WINCHESTER) + add_favoury_marker(LOC_GUILDFORD) + add_favoury_marker(LOC_ARUNDEL) + add_favoury_marker(LOC_HASTINGS) + add_favoury_marker(LOC_DOVER) + add_favoury_marker(LOC_ROCHESTER) + add_favoury_marker(LOC_CANTERBURY) + add_favoury_marker(LOC_SOUTHAMPTON) + add_favoury_marker(LOC_BURGUNDY) + add_favoury_marker(LOC_IRELAND) + + setup_vassals([ VASSAL_FAUCONBERG, VASSAL_NORFOLK ]) + muster_vassal(VASSAL_FAUCONBERG, LORD_MARCH) +} + +function setup_Ic() { + game.turn = 5 << 1 + + game.rebel = YORK + game.active = YORK + game.victory_check = 40 + game.influence = 6 + muster_lord(LORD_WARWICK_Y, LOC_LONDON) + muster_lord(LORD_MARCH, LOC_LONDON) + muster_lord(LORD_SOMERSET_1, LOC_BAMBURGH) + + set_lord_calendar(LORD_HENRY_VI, 5) + + add_favourl_marker(LOC_SCARBOROUGH) + add_favourl_marker(LOC_NEWCASTLE) + add_favourl_marker(LOC_BAMBURGH) + add_favourl_marker(LOC_HEXHAM) + add_favourl_marker(LOC_APPLEBY) + add_favourl_marker(LOC_CARLISLE) + add_favourl_marker(LOC_HARLECH) + add_favourl_marker(LOC_PEMBROKE) + add_favourl_marker(LOC_CARDIFF) + add_favourl_marker(LOC_CHESTER) + add_favourl_marker(LOC_LANCASTER) + add_favourl_marker(LOC_SCOTLAND) + add_favourl_marker(LOC_FRANCE) + + add_favoury_marker(LOC_LONDON) + add_favoury_marker(LOC_CALAIS) + add_favoury_marker(LOC_LUDLOW) + add_favoury_marker(LOC_HEREFORD) + add_favoury_marker(LOC_SALISBURY) + add_favoury_marker(LOC_WINCHESTER) + add_favoury_marker(LOC_GUILDFORD) + add_favoury_marker(LOC_ARUNDEL) + add_favoury_marker(LOC_HASTINGS) + add_favoury_marker(LOC_DOVER) + add_favoury_marker(LOC_ROCHESTER) + add_favoury_marker(LOC_CANTERBURY) + add_favoury_marker(LOC_SOUTHAMPTON) + add_favoury_marker(LOC_BURGUNDY) + add_favoury_marker(LOC_IRELAND) + + setup_vassals() +} + +function setup_II() { + game.turn = 1 << 1 + + game.rebel = LANCASTER + game.active = LANCASTER + game.victory_check = 40 + game.influence = 0 + muster_lord(LORD_EDWARD_IV, LOC_LONDON) + muster_lord(LORD_PEMBROKE, LOC_PEMBROKE) + muster_lord(LORD_WARWICK_L, LOC_CALAIS) + muster_lord(LORD_CLARENCE, LOC_YORK) + muster_lord(LORD_JASPER_TUDOR_1, LOC_HARLECH) + + set_lord_calendar(LORD_DEVON, 1) + set_lord_calendar(LORD_GLOUCESTER_1, 9) + set_lord_calendar(LORD_NORTHUMBERLAND_Y1, 9) + set_lord_calendar(LORD_MARGARET, 9) + set_lord_in_exile(LORD_MARGARET) + set_lord_calendar(LORD_SOMERSET_2, 9) + set_lord_in_exile(LORD_SOMERSET_2) + set_lord_calendar(LORD_OXFORD, 9) + set_lord_in_exile(LORD_OXFORD) + set_lord_calendar(LORD_EXETER_2, 9) + set_lord_in_exile(LORD_EXETER_2) + + add_favourl_marker(LOC_CALAIS) + add_favourl_marker(LOC_YORK) + add_favourl_marker(LOC_HARLECH) + add_favourl_marker(LOC_COVENTRY) + add_favourl_marker(LOC_WELLS) + add_favourl_marker(LOC_FRANCE) + + add_favoury_marker(LOC_LONDON) + add_favoury_marker(LOC_ELY) + add_favoury_marker(LOC_LUDLOW) + add_favoury_marker(LOC_CARLISLE) + add_favoury_marker(LOC_PEMBROKE) + add_favoury_marker(LOC_EXETER) + add_favoury_marker(LOC_BURGUNDY) + + setup_vassals([ VASSAL_DEVON, VASSAL_OXFORD ]) + + // TODO: Add Foreign Haven rule + // TODO: Add Skaky Allies rules +} + +function setup_III() { + game.turn = 1 << 1 + + game.rebel = LANCASTER + game.active = LANCASTER + game.victory_check = 40 + game.influence = 0 + muster_lord(LORD_RICHARD_III, LOC_LONDON) + muster_lord(LORD_NORTHUMBERLAND_Y2, LOC_CARLISLE) + muster_lord(LORD_NORFOLK, LOC_ARUNDEL) + muster_lord(LORD_HENRY_TUDOR, LOC_FRANCE) + muster_lord(LORD_JASPER_TUDOR_2, LOC_FRANCE) + muster_lord(LORD_OXFORD, LOC_FRANCE) + + add_favourl_marker(LOC_FRANCE) + add_favourl_marker(LOC_OXFORD) + add_favourl_marker(LOC_HARLECH) + add_favourl_marker(LOC_PEMBROKE) + + add_favoury_marker(LOC_BURGUNDY) + add_favoury_marker(LOC_LONDON) + add_favoury_marker(LOC_CALAIS) + add_favoury_marker(LOC_CARLISLE) + add_favoury_marker(LOC_ARUNDEL) + add_favoury_marker(LOC_YORK) + add_favoury_marker(LOC_GLOUCESTER) + + setup_vassals([ VASSAL_OXFORD, VASSAL_NORFOLK ]) +} + +function setup_ItoIII() { + game.turn = 1 << 1 + + game.rebel = YORK + game.active = YORK + game.victory_check = 45 + game.influence = 0 + muster_lord(LORD_YORK, LOC_ELY) + muster_lord(LORD_MARCH, LOC_LUDLOW) + muster_lord(LORD_HENRY_VI, LOC_LONDON) + muster_lord(LORD_SOMERSET_1, LOC_WELLS) + + set_lord_calendar(LORD_NORTHUMBERLAND_L, 1) + set_lord_calendar(LORD_EXETER_1, 3) + set_lord_calendar(LORD_BUCKINGHAM, 5) + set_lord_calendar(LORD_SALISBURY, 2) + set_lord_calendar(LORD_WARWICK_Y, 3) + set_lord_calendar(LORD_RUTLAND, 5) + + add_favourl_marker(LOC_LONDON) + add_favourl_marker(LOC_WELLS) + add_favourl_marker(LOC_SCOTLAND) + add_favourl_marker(LOC_FRANCE) + + add_favoury_marker(LOC_ELY) + add_favoury_marker(LOC_LUDLOW) + add_favoury_marker(LOC_BURGUNDY) + add_favoury_marker(LOC_IRELAND) + + setup_vassals() +} + +// === 6.0 CAMPAIGN === +/* + +function should_remove_Y28_event_card() { + return game.scenario !== "I-III. Wars of the Roses" +} + +function has_Y28_happened() { + //TODO: Scenario IIY and IIL when Y28 happens. + return false +} + +function add_card_scenario(c) { + // TODO: Add card in scenario +} + +function remove_card_scenario(c) { + //TODO: Remove card in scenario +} + +function setup_II_Y() { + game.turn = 1 << 1 + game.scenario = "IIY. The Kingmaker" + game.rebel = LANCASTER + game.active = LANCASTER + game.victory_check = 45 + game.influence = 0 + + for (let lord = first_lord; lord <= last_lord; lord++) { + if (is_lord_in_play(lord)) { + disband_lord(lord, false) + } + } + for (let loc = first_locale; loc <= last_locale; loc++) { + remove_exhausted_marker(loc) + remove_depleted_marker(loc) + remove_favourl_marker(loc) + remove_favoury_marker(loc) + } + discard_events("this_levy") + discard_events("hold") + discard_events("this_campaign") + + // Setup + // Yorkist setup + // TODO: Add cards Y1-Y13, Y25, Y26, Y27, Y29, Y30 + + if (is_lord_in_play(LORD_RUTLAND) && main_york_heir !== LORD_RUTLAND) { + muster_lord(LORD_RUTLAND, LOC_CANTERBURY) + add_favoury_marker(LOC_CANTERBURY) + } + + set_lord_calendar(LORD_DEVON, 1) + set_lord_calendar(LORD_GLOUCESTER_1, 9) + set_lord_calendar(LORD_NORTHUMBERLAND_Y1, 9) + + if (main_york_heir === LORD_YORK) { + muster_lord(LORD_YORK, LOC_CANTERBURY) + add_favoury_marker(LOC_LONDON) + if (is_lord_in_play(LORD_MARCH)) { + muster_lord(LORD_MARCH, LOC_LUDLOW) + } + // TODO: Add cards Y14, Y18, Y19, Y20 + } + + if (main_york_heir === LORD_MARCH) { + muster_lord(LORD_EDWARD_IV, LOC_LONDON) + // Removed because he can't appear in scenario III + disband_lord(LORD_MARCH, true) + // TODO: Add cards Y23, Y24, Y28, Y31 + } + + if (main_york_heir === LORD_RUTLAND) { + muster_lord(LORD_RUTLAND, LOC_LONDON) + // TODO: Add cards Y20, Y21, Y28, Y35 + } + + // If < 2 heirs, muster Pembroke + if ((main_york_heir === LORD_RUTLAND || main_york_heir === LORD_GLOUCESTER_1) + || (main_york_heir === LORD_EDWARD_IV && !is_lord_in_play(LORD_RUTLAND))) { + muster_lord(LORD_PEMBROKE, LOC_PEMBROKE) + } + + // Lancaster setup + // TODO: Add cards L1-L3, L5-L13, L23, L24, L25, L29, L30, L36 + + if (main_lancaster_heir === LORD_HENRY_VI) { + set_lord_calendar(LORD_HENRY_VI, 9) + set_lord_in_exile(LORD_HENRY_VI) + // TODO: Add L17, L18, L20, L21 + } + if (main_lancaster_heir === LORD_MARGARET) { + set_lord_calendar(LORD_MARGARET, 9) + set_lord_in_exile(LORD_MARGARET) + + // TODO: Add L27, L28, L31 + L26 Special rule + } + if (main_lancaster_heir === LORD_SOMERSET_1 || main_lancaster_heir === LORD_SOMERSET_2) { + // TODO: Add cards L20, L21, L27 + } + + if (is_lord_in_play(LORD_SOMERSET_1)) { + set_lord_calendar(LORD_SOMERSET_1, 9) + set_lord_in_exile(LORD_SOMERSET_1) + } + else if (is_lord_in_play(LORD_SOMERSET_2)) { + set_lord_calendar(LORD_SOMERSET_2, 9) + set_lord_in_exile(LORD_SOMERSET_2) + } + + muster_lord(LORD_WARWICK_L, LOC_CALAIS) + muster_lord(LORD_CLARENCE, LOC_YORK) + muster_lord(LORD_JASPER_TUDOR_1, LOC_HARLECH) + set_lord_calendar(LORD_OXFORD, 9) + set_lord_in_exile(LORD_OXFORD) + set_lord_calendar(LORD_EXETER_2, 9) + set_lord_in_exile(LORD_EXETER_2) + + add_favourl_marker(LOC_CALAIS) + add_favourl_marker(LOC_YORK) + add_favourl_marker(LOC_HARLECH) + add_favourl_marker(LOC_COVENTRY) + add_favourl_marker(LOC_WELLS) + + add_favoury_marker(LOC_LONDON) + add_favoury_marker(LOC_ELY) + add_favoury_marker(LOC_LUDLOW) + add_favoury_marker(LOC_CARLISLE) + add_favoury_marker(LOC_PEMBROKE) + add_favoury_marker(LOC_EXETER) + + // Exile box setup + add_favourl_marker(LOC_FRANCE) + add_favoury_marker(LOC_BURGUNDY) + + setup_vassals([ VASSAL_DEVON, VASSAL_OXFORD ]) + + // TODO: Add Foreign Haven rule + // TODO: Add Skaky Allies rules + // TODO: Natural causes rule + +} + +function setup_II_L() { + game.turn = 1 << 1 + game.scenario = "IIL. Lancastrian Legitimacy Fades" + game.rebel = YORK + game.active = YORK + game.victory_check = 40 + game.influence = 0 + + for (let lord = first_lord; lord <= last_lord; lord++) { + if (is_lord_in_play(lord)) { + disband_lord(lord, false) + } + } + for (let loc = first_locale; loc <= last_locale; loc++) { + remove_exhausted_marker(loc) + remove_depleted_marker(loc) + remove_favourl_marker(loc) + remove_favoury_marker(loc) + } + discard_events("this_levy") + discard_events("hold") + discard_events("this_campaign") + + // Setup + // Lancaster setup + // TODO: Add cards L1-L3, L5-L13, L18, L19, L20, L21, L25, L29, L34 + + if (main_lancaster_heir === LORD_HENRY_VI) { + muster_lord(LORD_HENRY_VI, LOC_LONDON) + // TODO: Add L15, L17 + if (is_lord_in_play(LORD_SOMERSET_1)) { + muster_lord(LORD_SOMERSET_1, LOC_WELLS) + } + if (is_lord_in_play(LORD_SOMERSET_2)) { + muster_lord(LORD_SOMERSET_2, LOC_WELLS) + } + } + + if (main_lancaster_heir === LORD_MARGARET) { + set_lord_calendar(LORD_MARGARET, 1) + // TODO: Add L27, L31 + L26 Special rule + if (is_lord_in_play(LORD_SOMERSET_1)) { + muster_lord(LORD_SOMERSET_1, LOC_WELLS) + } + if (is_lord_in_play(LORD_SOMERSET_2)) { + muster_lord(LORD_SOMERSET_2, LOC_WELLS) + } + } + if (main_lancaster_heir === LORD_SOMERSET_1 || main_lancaster_heir === LORD_SOMERSET_2) { + // TODO: Add cards L16, L27 + muster_lord(LORD_SOMERSET_1, LOC_LONDON) + if (main_lancaster_heir === LORD_SOMERSET_2) { + // Somerset 2 cylinder replaced by Somerset 1 cylinder + disband_lord(LORD_SOMERSET_2, true) + } + } + + // Yorkist setup + // TODO: Add cards Y1-Y13, Y15, Y16, Y17, Y22, Y28, Y29, Y31, Y34 + + if (main_york_heir === LORD_YORK) { + set_lord_calendar(LORD_YORK, 7) + set_lord_in_exile(LORD_YORK) + // TODO: Add cards Y14, Y20 + } + + if (main_york_heir === LORD_MARCH) { + set_lord_calendar(LORD_MARCH, 7) + set_lord_in_exile(LORD_MARCH) + // TODO: Add cards Y20, 21 + } + + if (main_york_heir === LORD_RUTLAND) { + set_lord_calendar(LORD_MARCH, 7) + // TODO: Add cards Y20, Y21 + } + + if (main_york_heir === LORD_GLOUCESTER_1) { + // TODO: Add cards Y25, Y30 + } + + if (is_lord_in_play(LORD_MARCH) && main_york_heir !== LORD_MARCH) { + set_lord_calendar(LORD_MARCH, 7) + set_lord_in_exile(LORD_MARCH) + } + + if (is_lord_in_play(LORD_RUTLAND) && main_york_heir !== LORD_RUTLAND) { + set_lord_calendar(LORD_RUTLAND, 7) + set_lord_in_exile(LORD_RUTLAND) + } + if (is_lord_in_play(LORD_GLOUCESTER_1) && main_york_heir !== LORD_GLOUCESTER_1) { + set_lord_calendar(LORD_GLOUCESTER_1, 7) + set_lord_in_exile(LORD_GLOUCESTER_1) + } + + muster_lord(LORD_WARWICK_Y, LOC_CALAIS) + muster_lord(LORD_SALISBURY, LOC_YORK) + muster_lord(LORD_PEMBROKE, LOC_PEMBROKE) + muster_lord(LORD_JASPER_TUDOR_1, LOC_HARLECH) + set_lord_calendar(LORD_DEVON, 1) + set_lord_calendar(LORD_OXFORD, 2) + set_lord_calendar(LORD_EXETER_2, 2) + set_lord_calendar(LORD_NORTHUMBERLAND_L, 8) + + add_favourl_marker(LOC_LONDON) + add_favourl_marker(LOC_HARLECH) + add_favourl_marker(LOC_OXFORD) + add_favourl_marker(LOC_WELLS) + add_favourl_marker(LOC_EXETER) + add_favourl_marker(LOC_CARLISLE) + + add_favoury_marker(LOC_CALAIS) + add_favoury_marker(LOC_YORK) + add_favoury_marker(LOC_ELY) + add_favoury_marker(LOC_LUDLOW) + add_favoury_marker(LOC_PEMBROKE) + + // Exile box setup + add_favourl_marker(LOC_FRANCE) + add_favoury_marker(LOC_BURGUNDY) + + setup_vassals([ VASSAL_DEVON, VASSAL_OXFORD ]) + + // TODO: Add Foreign Haven rule + // TODO: Add Shaky Allies rules + // TODO: Natural causes rule + +} + +function setup_III_Y() { + game.turn = 1 << 1 + game.scenario = "IIIY. New Rivals" + game.rebel = LANCASTER + game.active = LANCASTER + game.victory_check = 45 + game.influence = 0 + + if (!is_lord_in_play(LORD_YORK)) { + game.influence += 8 + } + if (!is_lord_in_play(LORD_MARCH) && !is_lord_in_play(LORD_EDWARD_IV)) { + game.influence += 8 + } + if (!is_lord_in_play(LORD_RUTLAND)) { + game.influence += 8 + } + if (!is_lord_in_play(LORD_GLOUCESTER_1) && !is_lord_in_play(LORD_GLOUCESTER_2) && !is_lord_in_play(LORD_RICHARD_III)) { + game.influence += 8 + } + if (!is_lord_in_play(LORD_HENRY_VI)) { + game.influence -= 8 + } + if (!is_lord_in_play(LORD_HENRY_VI) && !is_lord_in_play(LORD_MARGARET)) { + game.influence -= 8 + } + if (!is_lord_in_play(LORD_SOMERSET_1)) { + game.influence -= 8 + } + if (!is_lord_in_play(LORD_SOMERSET_1) && !is_lord_in_play(LORD_SOMERSET_2)) { + game.influence -= 8 + } + + for (let lord = first_lord; lord <= last_lord; lord++) { + if (is_lord_in_play(lord)) { + disband_lord(lord, false) + } + } + for (let loc = first_locale; loc <= last_locale; loc++) { + remove_exhausted_marker(loc) + remove_depleted_marker(loc) + remove_favourl_marker(loc) + remove_favoury_marker(loc) + } + discard_events("this_levy") + discard_events("hold") + discard_events("this_campaign") + + // Yorkist Setup + // TODO: Add Y1-Y13, Y36 + + if (has_Y28_happened()) { + if (is_lord_in_play(LORD_RUTLAND) && (is_lord_in_play(LORD_GLOUCESTER_1) || is_lord_in_play(LORD_GLOUCESTER_2) || is_lord_in_play(LORD_RICHARD_III))) { + // If Gloucester (any) and Rutland, Rutland dies + disband_lord(LORD_RUTLAND, true) + } + } + + if (main_york_heir === LORD_RUTLAND && (!is_lord_in_play(LORD_GLOUCESTER_1) && !is_lord_in_play(LORD_GLOUCESTER_2))) { + // If Rutland is lone heir, Rutland dies + disband_lord(LORD_RUTLAND, true) + //Warwick becomes king + muster_lord(LORD_WARWICK_Y, LOC_LONDON) + add_favoury_marker(LOC_LONDON) + muster_lord(LORD_SALISBURY, LOC_YORK) + add_favoury_marker(LOC_YORK) + + // TODO: Add Y16, Y17, Y22 + } + + // If only 1 is alive + if (main_york_heir === LORD_YORK && !is_lord_in_play(LORD_MARCH) && !is_lord_in_play(LORD_RUTLAND) && !is_lord_in_play(LORD_GLOUCESTER_1)) { + muster_lord(LORD_NORTHUMBERLAND_Y2, LOC_CARLISLE) + add_favoury_marker(LOC_CARLISLE) + + // TODO: Add Y37 + } + if ((main_york_heir === LORD_MARCH || main_york_heir === LORD_EDWARD_IV) && !is_lord_in_play(LORD_RUTLAND) && !is_lord_in_play(LORD_GLOUCESTER_1)) { + muster_lord(LORD_NORTHUMBERLAND_Y2, LOC_CARLISLE) + add_favoury_marker(LOC_CARLISLE) + // TODO: Add Y37 + } + if (main_york_heir === LORD_GLOUCESTER_1 || main_york_heir === LORD_RICHARD_III) { + muster_lord(LORD_NORTHUMBERLAND_Y2, LOC_CARLISLE) + add_favoury_marker(LOC_CARLISLE) + // TODO: Add Y37 + } + muster_lord(LORD_NORFOLK, LOC_ARUNDEL) + add_favoury_marker(LOC_ARUNDEL) + + if (main_york_heir === LORD_YORK) { + // TODO: Add Y14, Y21 + if (is_lord_in_play(LORD_MARCH)) { + muster_lord(LORD_MARCH, LOC_LUDLOW) + add_favoury_marker(LOC_LUDLOW) + // Add Y20 + // Only 2 heirs can stay + disband_lord(LORD_RUTLAND, true) + disband_lord(LORD_GLOUCESTER_1, true) + } + if (!is_lord_in_play(LORD_MARCH) && is_lord_in_play(LORD_RUTLAND)) { + muster_lord(LORD_RUTLAND, LOC_CANTERBURY) + add_favoury_marker(LOC_CANTERBURY) + // TODO: Add Y20 + } + if (is_lord_in_play(LORD_GLOUCESTER_1)) { + muster_lord(LORD_GLOUCESTER_1, LOC_GLOUCESTER) + add_favoury_marker(LOC_GLOUCESTER) + // TODO: Y34 + } + } + if (main_york_heir === LORD_MARCH || main_york_heir === LORD_EDWARD_IV) { + muster_lord(LORD_EDWARD_IV, LOC_LONDON) + add_favoury_marker(LOC_LONDON) + + // If Edward IV is on the map, remove March + disband_lord(LORD_MARCH, true) + // TODO: Add Y23, Y24 + if (is_lord_in_play(LORD_RUTLAND)) { + muster_lord(LORD_RUTLAND, LOC_CANTERBURY) + add_favoury_marker(LOC_CANTERBURY) + // TODO: Add Y31 + } + if (is_lord_in_play(LORD_GLOUCESTER_1)) { + muster_lord(LORD_GLOUCESTER_1, LOC_GLOUCESTER) + add_favoury_marker(LOC_GLOUCESTER) + // TODO: Add Y28, Y34 + } + + } + if (main_york_heir === LORD_RUTLAND) { + muster_lord(LORD_RUTLAND, LOC_LONDON) + add_favoury_marker(LOC_LONDON) + // TODO: Add Y20, Y21 + if (is_lord_in_play(LORD_GLOUCESTER_1)) { + muster_lord(LORD_GLOUCESTER_2, LOC_LONDON) + // If Rutland is King, golden gloucester 2 arrives and gloucester 1 is gone + disband_lord(LORD_GLOUCESTER_1, true) + // TODO: Add Y34 + } + } + if (main_york_heir === LORD_GLOUCESTER_1) { + muster_lord(LORD_RICHARD_III, LOC_LONDON) + add_favoury_marker(LOC_LONDON) + // if Richard III is here, both gloucester are gone + disband_lord(LORD_GLOUCESTER_1, true) + disband_lord(LORD_GLOUCESTER_2, true) + // TODO: Add Y32, Y33 + } + + // Lancaster setup + // TODO: Add L1-L13, L34, L35, L36, L37 + + if (main_lancaster_heir === LORD_HENRY_VI || main_lancaster_heir === LORD_MARGARET) { + muster_lord(LORD_MARGARET, LOC_FRANCE) + // TODO: Add L27, L31 + L26 Edward + // Only one heir + disband_lord(LORD_HENRY_VI, true) + disband_lord(LORD_SOMERSET_1, true) + disband_lord(LORD_SOMERSET_2, true) + } + // If Margaret not here and Edward IV not king + if (!is_lord_on_map(LORD_MARGARET) && main_york_heir !== LORD_EDWARD_IV) { + muster_lord(LORD_HENRY_TUDOR, LOC_FRANCE) + // TODO: Add L32, L35 + disband_lord(LORD_SOMERSET_1, true) + disband_lord(LORD_SOMERSET_2, true) + } + if (!is_lord_on_map(LORD_MARGARET) && !is_lord_on_map(LORD_HENRY_TUDOR)) { + muster_lord(LORD_WARWICK_L, LOC_CALAIS) + add_favourl_marker(LOC_CALAIS) + // TODO: Add L23, L30 + } + + if (is_lord_on_map(LORD_MARGARET) || is_lord_on_map(LORD_HENRY_TUDOR)) { + muster_lord(LORD_OXFORD, LOC_FRANCE) + add_favourl_marker(LOC_OXFORD) + muster_lord(LORD_JASPER_TUDOR_2, LOC_FRANCE) + add_favoury_marker(LOC_PEMBROKE) + } + else if (is_lord_on_map(LORD_WARWICK_L)) { + muster_lord(LORD_OXFORD, LOC_CALAIS) + add_favourl_marker(LOC_OXFORD) + muster_lord(LORD_JASPER_TUDOR_2, LOC_CALAIS) + add_favoury_marker(LOC_PEMBROKE) + } + else { + throw Error("Error Lancastrian setup III.Y") + } + + // Exile box setup + add_favourl_marker(LOC_FRANCE) + add_favoury_marker(LOC_BURGUNDY) + + setup_vassals([ VASSAL_OXFORD, VASSAL_NORFOLK ]) +} + +function setup_III_L() { + game.turn = 1 << 1 + game.scenario = "IIIL. Yorkists Last Stand" + game.rebel = YORK + game.active = YORK + game.victory_check = 45 + game.influence = 0 + + if (!is_lord_in_play(LORD_YORK)) { + game.influence += 8 + } + if (!is_lord_in_play(LORD_MARCH) && !is_lord_in_play(LORD_EDWARD_IV)) { + game.influence += 8 + } + if (!is_lord_in_play(LORD_RUTLAND)) { + game.influence += 8 + } + if (!is_lord_in_play(LORD_GLOUCESTER_1) && !is_lord_in_play(LORD_GLOUCESTER_2) && !is_lord_in_play(LORD_RICHARD_III)) { + game.influence += 8 + } + if (!is_lord_in_play(LORD_HENRY_VI)) { + game.influence -= 8 + } + if (!is_lord_in_play(LORD_HENRY_VI) && !is_lord_in_play(LORD_MARGARET)) { + game.influence -= 8 + } + if (!is_lord_in_play(LORD_SOMERSET_1)) { + game.influence -= 8 + } + if (!is_lord_in_play(LORD_SOMERSET_1) && !is_lord_in_play(LORD_SOMERSET_2)) { + game.influence -= 8 + } + + for (let lord = first_lord; lord <= last_lord; lord++) { + if (is_lord_in_play(lord)) { + disband_lord(lord, false) + } + } + for (let loc = first_locale; loc <= last_locale; loc++) { + remove_exhausted_marker(loc) + remove_depleted_marker(loc) + remove_favourl_marker(loc) + remove_favoury_marker(loc) + } + discard_events("this_levy") + discard_events("hold") + discard_events("this_campaign") + + // Lancaster Setup + // TODO: Add L1-L13, L25, L34, L36 + + if (main_lancaster_heir === LORD_HENRY_VI) { + muster_lord(LORD_HENRY_VI, LOC_LONDON) + // TOOD: Add L15, L17 + } + if (main_lancaster_heir === LORD_MARGARET) { + muster_lord(LORD_MARGARET, LOC_LONDON) + // TODO: Add L27, L31 + } + if (main_lancaster_heir === LORD_SOMERSET_1) { + muster_lord(LORD_SOMERSET_1, LOC_LONDON) + add_favourl_marker(LOC_WELLS) + // TODO: Add L18, L20, L27 + } + // Should never happen but as a failsafe + if (main_lancaster_heir === LORD_SOMERSET_2) { + muster_lord(LORD_SOMERSET_1, LOC_LONDON) + add_favourl_marker(LOC_WELLS) + disband_lord(LORD_SOMERSET_2, true) + // TODO: Add L18, L20, L27 + } + muster_lord(LORD_OXFORD, LOC_OXFORD) + muster_lord(LORD_JASPER_TUDOR_2, LOC_PEMBROKE) + add_favourl_marker(LOC_OXFORD) + add_favourl_marker(LOC_PEMBROKE) + add_favourl_marker(LOC_LONDON) + + // York Setup + // TOOD: Add Y1-Y13, Y36 + + if (has_Y28_happened()) { + if (is_lord_in_play(LORD_GLOUCESTER_1) || is_lord_in_play(LORD_GLOUCESTER_2) || is_lord_in_play(LORD_RICHARD_III)) { + // If Gloucester (any), all other yorkist heir dies + disband_lord(LORD_YORK, true) + disband_lord(LORD_RUTLAND, true) + disband_lord(LORD_MARCH, true) + disband_lord(LORD_EDWARD_IV, true) + disband_lord(LORD_GLOUCESTER_1, true) + disband_lord(LORD_RICHARD_III, true) + muster_lord(LORD_GLOUCESTER_2, LOC_BURGUNDY) + // TODO: Add Y35 + } + } + + if (main_york_heir === LORD_YORK) { + muster_lord(LORD_YORK, LOC_BURGUNDY) + add_favoury_marker(LOC_ELY) + // TODO: Add Y14, Y18 + if (is_lord_in_play(LORD_MARCH)) { + // Only next highest heir alive + disband_lord(LORD_RUTLAND, true) + disband_lord(LORD_GLOUCESTER_1, true) + disband_lord(LORD_GLOUCESTER_2, true) + muster_lord(LORD_MARCH, LOC_BURGUNDY) + add_favoury_marker(LOC_LUDLOW) + //TODO: Add Y20 + } + else if (!is_lord_in_play(LORD_MARCH) && is_lord_in_play(LORD_RUTLAND)) { + // Only next highest heir alive + disband_lord(LORD_GLOUCESTER_1, true) + disband_lord(LORD_GLOUCESTER_2, true) + muster_lord(LORD_RUTLAND, LOC_BURGUNDY) + add_favoury_marker(LOC_CANTERBURY) + //TODO: Add Y20 + } + else if (!is_lord_in_play(LORD_MARCH) && !is_lord_in_play(LORD_RUTLAND) && (is_lord_in_play(LORD_GLOUCESTER_1) || is_lord_in_play(LORD_GLOUCESTER_2))) { + // Final Scenario, and no succession rule + disband_lord(LORD_GLOUCESTER_2, true) + muster_lord(LORD_GLOUCESTER_1, LOC_BURGUNDY) + add_favoury_marker(LOC_GLOUCESTER) + // TODO: Add Y4 + } + else { + // If York alone + muster_lord(LORD_SALISBURY, LOC_BURGUNDY) + add_favoury_marker(LOC_YORK) + //TODO: Add Y17, Y22 + } + } + if (main_york_heir === LORD_MARCH || main_york_heir === LORD_RUTLAND) { + // If March or Rutland is highest heir, Warwick takes the lead + disband_lord(LORD_MARCH, true) + disband_lord(LORD_RUTLAND, true) + muster_lord(LORD_WARWICK_Y, LOC_CALAIS) + add_favoury_marker(LOC_CALAIS) + //TODO: Add Y16 + } + + if (main_york_heir === LORD_WARWICK_Y) { + muster_lord(LORD_NORFOLK, LOC_CALAIS) + muster_lord(LORD_SALISBURY, LOC_CALAIS) + add_favoury_marker(LOC_CALAIS) + //TODO: Add Y17, Y22 + } + else ( + muster_lord(LORD_NORFOLK, LOC_BURGUNDY) + ) + + if (main_york_heir === LORD_GLOUCESTER_1) { + disband_lord(LORD_GLOUCESTER_1, true) + muster_lord(LORD_GLOUCESTER_2, LOC_BURGUNDY) + muster_lord(LORD_SALISBURY, LOC_BURGUNDY) + //TODO: Add Y17, Y22 + } + + add_favoury_marker(LOC_ARUNDEL) + + // Exile box setup + add_favourl_marker(LOC_FRANCE) + add_favoury_marker(LOC_BURGUNDY) + + setup_vassals([ VASSAL_OXFORD, VASSAL_NORFOLK ]) +} + +// FULL SCENARIO HEIR +function get_main_york_heir() { + if (is_lord_in_play(LORD_YORK)) + return LORD_YORK + if (!is_lord_in_play(LORD_YORK) && is_lord_in_play(LORD_MARCH)) + return LORD_MARCH + if (!is_lord_in_play(LORD_YORK) && !is_lord_in_play(LORD_MARCH) && is_lord_in_play(LORD_EDWARD_IV)) + return LORD_EDWARD_IV + if (!is_lord_in_play(LORD_YORK) && !is_lord_in_play(LORD_MARCH) && !is_lord_in_play(LORD_EDWARD_IV) && is_lord_in_play(LORD_RUTLAND)) + return LORD_RUTLAND + if (!is_lord_in_play(LORD_YORK) && !is_lord_in_play(LORD_MARCH) && !is_lord_in_play(LORD_EDWARD_IV) && !is_lord_in_play(LORD_RUTLAND) && (is_lord_in_play(LORD_GLOUCESTER_1) || is_lord_in_play(LORD_GLOUCESTER_2) || is_lord_in_play(LORD_RICHARD_III))) + return LORD_GLOUCESTER_1 + if (!is_lord_in_play(LORD_YORK) && !is_lord_in_play(LORD_MARCH) && !is_lord_in_play(LORD_EDWARD_IV) && !is_lord_in_play(LORD_RUTLAND) && !is_lord_in_play(LORD_GLOUCESTER_1) && !is_lord_in_play(LORD_GLOUCESTER_2) && !is_lord_in_play(LORD_RICHARD_III)) + return LORD_WARWICK_Y +} + +function get_main_lancaster_heir() { + if (is_lord_in_play(LORD_HENRY_VI)) + return LORD_HENRY_VI + if (!is_lord_in_play(LORD_HENRY_VI) && is_lord_in_play(LORD_MARGARET)) + return LORD_MARGARET + if (!is_lord_in_play(LORD_HENRY_VI) && !is_lord_in_play(LORD_MARGARET) && is_lord_in_play(LORD_SOMERSET_1)) + return LORD_SOMERSET_1 + if (!is_lord_in_play(LORD_HENRY_VI) && !is_lord_in_play(LORD_MARGARET) && !is_lord_in_play(LORD_SOMERSET_1) && is_lord_in_play(LORD_SOMERSET_2)) + return LORD_SOMERSET_2 + if (!is_lord_in_play(LORD_HENRY_VI) && !is_lord_in_play(LORD_MARGARET) && !is_lord_in_play(LORD_SOMERSET_1) && !is_lord_in_play(LORD_SOMERSET_2) && is_lord_in_play(LORD_HENRY_TUDOR)) + return LORD_HENRY_TUDOR + if (!is_lord_in_play(LORD_HENRY_VI) && !is_lord_in_play(LORD_MARGARET) && !is_lord_in_play(LORD_SOMERSET_1) && !is_lord_in_play(LORD_SOMERSET_2) && !is_lord_in_play(LORD_HENRY_TUDOR) && is_lord_in_play(LORD_WARWICK_L)) + return LORD_WARWICK_L +} + +*/ + +// === CAPABILITY: MUSTER EFFECTS === + +// When a lord levies a capability, its muster vassal applies instantly. +function capability_muster_effects(lord, c) { + if (c === AOW_LANCASTER_MONTAGU) + muster_vassal(VASSAL_MONTAGU, lord) + + if (c === AOW_LANCASTER_MY_FATHERS_BLOOD) + muster_vassal(VASSAL_CLIFFORD, lord) + + if (c === AOW_LANCASTER_ANDREW_TROLLOPE) + muster_vassal(VASSAL_TROLLOPE, lord) + + if (c === AOW_LANCASTER_EDWARD) + muster_vassal(VASSAL_EDWARD, lord) + + if (c === AOW_LANCASTER_THOMAS_STANLEY) { + muster_vassal(VASSAL_THOMAS_STANLEY, lord) + game.flags.free_levy = 1 + } + + if (c === AOW_YORK_HASTINGS) { + add_lord_forces(lord, MEN_AT_ARMS, 2) + muster_vassal(VASSAL_HASTINGS, lord) + } + + if (c === AOW_YORK_FAIR_ARBITER && is_friendly_locale(get_lord_locale(LORD_SALISBURY))) { + game.count += 1 + } + if (c === AOW_YORK_FALLEN_BROTHER && !is_lord_in_play(LORD_CLARENCE)) { + game.count += 1 + } + + if (c === AOW_YORK_BURGUNDIANS) { + if (is_seaport(get_lord_locale(lord) && !is_exile(get_lord_locale(lord)))) { + add_lord_forces(lord, BURGUNDIANS, 2) + logi(AOW_YORK_BURGUNDIANS) + game.flags.burgundians = 1 + } + else { + game.flags.burgundians = 0 + } + } +} + +// === CAPABILITY: LORDSHIP EFFECTS === + +// When a lord levies a capability, its +Lordship effects apply instantly. +function lordship_effects(lord) { + if (is_friendly_locale(get_lord_locale(lord)) && lord_has_capability(lord, AOW_YORK_FAIR_ARBITER)) + game.count += 1 + if (lord_has_capability(lord, AOW_YORK_FALLEN_BROTHER) && !is_lord_in_play(LORD_CLARENCE)) + game.count += 1 + if (is_event_in_play(EVENT_YORK_EDWARD_V) && (lord === LORD_GLOUCESTER_1 || lord === LORD_GLOUCESTER_2)) + game.count += 3 + if (is_lancaster_lord(lord) && is_event_in_play(EVENT_LANCASTER_PARLIAMENT_VOTES)) { + game.flags.parliament_votes = 1 + } + if (is_york_lord(lord) && is_jack_cade_eligible(lord)) { + game.flags.jack_cade = 2 + } + if (is_york_lord(lord) && is_event_in_play(EVENT_YORK_SUCCESSION)) { + game.flags.succession = 1 + } +} + +// === CAPABILITY: SOLDIERS OF FORTUNE === + +states.soldiers_of_fortune = { + inactive: "Levy Troops", + prompt() { + view.prompt = `Pay 1 Coin for Mercenaries ${lord_name[game.who]}.` + let done = true + if (done) { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (is_lord_unfed(lord) && can_pay_from_shared(lord)) { + if (get_lord_assets(lord, COIN) > 0) { + gen_action_coin(lord) + done = false + } + } + } + } + // Done + if (done) { + view.prompt = "Soldiers of fortune: Done." + view.actions.end_sof = 1 + } + }, + coin(lord) { + push_undo() + let here = get_lord_locale(game.who) + let here_type = data.locales[here].type + let number = get_lord_forces(game.who, MERCENARIES) + let merc = 0 + if (!lord_has_capability(game.who, AOW_YORK_WOODWILLES)) + deplete_locale(here) + + switch (here_type) { + case "calais": + add_lord_forces(game.who, MEN_AT_ARMS, 2) + add_lord_forces(game.who, LONGBOWMEN, 1) + break + case "london": + add_lord_forces(game.who, MEN_AT_ARMS, 1) + add_lord_forces(game.who, LONGBOWMEN, 1) + add_lord_forces(game.who, MILITIA, 1) + break + case "harlech": + add_lord_forces(game.who, MEN_AT_ARMS, 1) + add_lord_forces(game.who, LONGBOWMEN, 2) + break + case "city": + add_lord_forces(game.who, LONGBOWMEN, 1) + add_lord_forces(game.who, MILITIA, 1) + break + case "town": + add_lord_forces(game.who, MILITIA, 2) + break + case "fortress": + add_lord_forces(game.who, MEN_AT_ARMS, 1) + add_lord_forces(game.who, MILITIA, 1) + break + } + if (game.flags.free_levy === 1) { + ++game.count + game.flags.free_levy = 0 + } + if (number === 5) + merc = 1 + else + merc = 2 + add_lord_assets(lord, COIN, -1) + add_lord_forces(game.who, MERCENARIES, merc) + set_lord_unfed(game.who, 0) + }, + end_sof() { + end_soldiers_of_fortune() + }, + card: action_held_event, +} + +function end_soldiers_of_fortune() { + goto_the_kings_name("Soldiers of Fortune") +} + +// === CAPABILITY: COMMISSION OF ARRAY === + +states.commission_of_array = { + inactive: "Levy Troops", + prompt() { + view.prompt = `Lord troops adjacent to ${lord_name[game.who]}.` + let done = true + let here = get_lord_locale(game.who) + if (done) { + for (let next of data.locales[here].adjacent) { + if (is_friendly_locale(next) && lord_has_capability(game.who, AOW_LANCASTER_COMMISION_OF_ARRAY) && (!has_exhausted_marker(next) && !is_exile(next))) { + done = false + gen_action_locale(next) + } + } + } + // Done + if (done) { + view.prompt = "Commission of Array: Done." + view.actions.end_coa = 1 + } + }, + locale(loc) { + push_undo() + let loc_type = data.locales[loc].type + deplete_locale(loc) + + switch (loc_type) { + case "calais": + add_lord_forces(game.who, MEN_AT_ARMS, 2) + add_lord_forces(game.who, LONGBOWMEN, 1) + break + case "london": + add_lord_forces(game.who, MEN_AT_ARMS, 1) + add_lord_forces(game.who, LONGBOWMEN, 1) + add_lord_forces(game.who, MILITIA, 1) + break + case "harlech": + add_lord_forces(game.who, MEN_AT_ARMS, 1) + add_lord_forces(game.who, LONGBOWMEN, 2) + break + case "city": + add_lord_forces(game.who, LONGBOWMEN, 1) + add_lord_forces(game.who, MILITIA, 1) + break + case "town": + add_lord_forces(game.who, MILITIA, 2) + break + case "fortress": + add_lord_forces(game.who, MEN_AT_ARMS, 1) + add_lord_forces(game.who, MILITIA, 1) + break + } + if (game.flags.free_levy === 1) { + ++game.count + game.flags.free_levy = 0 + } + end_commission_of_array() + }, + card: action_held_event, +} + +function end_commission_of_array() { + goto_the_kings_name("Commission of Array") +} + +// === CAPABILITY: WE DONE DEEDS OF CHARITY === + +function eligible_charity() { + let lord = find_lord_with_capability_card(AOW_YORK_WE_DONE_DEEDS_OF_CHARITY) + if (lord !== NOBODY && get_shared_assets(lord, PROV) > 0) + return true + return false +} + +function goto_we_done_deeds_of_charity() { + set_active(YORK) + game.state = "we_done_deeds_of_charity" + game.count = 2 +} + +states.we_done_deeds_of_charity = { + inactive: "We done needs of charity", + prompt() { + view.prompt = "We done deeds of charity: Pay up to two Provender for +1 Influence point each." + let lord = find_lord_with_capability_card(AOW_YORK_WE_DONE_DEEDS_OF_CHARITY) + let here = get_lord_locale(lord) + if (game.count > 0) { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (get_lord_locale(lord) === here && (get_lord_assets(lord, PROV) > 0)) { + gen_action_prov(lord) + } + } + } + view.actions.done = 1 + }, + prov(lord) { + push_undo() + increase_york_influence(1) + add_lord_assets(lord, PROV, -1) + game.count-- + }, + done() { + clear_undo() + logi(`${AOW_YORK_WE_DONE_DEEDS_OF_CHARITY}`) + log("York paid " + game.count + " provender to add " + game.count + " Influence Points") + game.count = 0 + + // TODO: who should be active? + goto_disembark() + }, +} + +// === CAPABILITY: MERCHANTS === + +function can_action_merchants() { + let loc = get_lord_locale(game.command) + if (game.actions <= 0) + return false + + if (lord_has_capability(game.command, AOW_LANCASTER_MERCHANTS) && count_deplete(loc) > 0) + return true + else + return false +} + +function goto_merchants() { + game.count = count_deplete(get_lord_locale(game.command)) + game.state = "merchants" + init_influence_check(game.command) +} + +states.merchants = { + inactive: "Merchants", + prompt() { + view.prompt = "Merchants: Succeed an influence check to remove Depleted markers" + prompt_influence_check() + }, + spend1: add_influence_check_modifier_1, + spend3: add_influence_check_modifier_2, + check() { + let results = do_influence_check() + log(`Attempt to C${AOW_LANCASTER_MERCHANTS} with %${game.command} ${results.success ? "Successful" : "Failed"}: (${range(results.rating)}) ${results.success ? HIT[results.roll] : MISS[results.roll]}`) + if (results.success) { + merchants_success() + } else { + end_merchants_attempt() + } + } +} +function merchants_success() { + game.state = "merchants_success" +} + +states.merchants_success = { + inactive: "Merchants Success", + prompt() { + view.prompt = `Remove Depleted/Exhausted markers` + deplete_merchants() + if (game.count === 0) { + view.actions.done = 1 + } + }, + locale(loc) { + push_undo() + remove_depleted_marker(loc) + remove_exhausted_marker(loc) + --game.count + if (game.count === 0) { + end_merchants_attempt() + } + }, + done(){ + end_merchants_attempt() + } +} + +function end_merchants_attempt() { + spend_action(1) + game.count = 0 + push_undo() + end_influence_check() + resume_command() +} + +function deplete_merchants() { + let here = get_lord_locale(game.command) + for (let next of data.locales[here].adjacent) { + if (has_exhausted_marker(next) || has_depleted_marker(next)) + gen_action_locale(next) + } + if (has_exhausted_marker(here) || has_depleted_marker(here)) + gen_action_locale(here) +} + +function count_deplete(loc) { + let n = 0 + for (let next of data.locales[loc].adjacent) { + if (has_exhausted_marker(next) || has_depleted_marker(next)) { + ++n + } + } + if (has_exhausted_marker(loc) || has_depleted_marker(loc)) { + ++n + } + if (n > 1) + n = 2 + return n +} + +// === CAPABILITY: BURGUNDIANS === + +function levy_burgundians(lord) { + if (is_seaport(get_lord_locale(lord)) && !is_exile(get_lord_locale(lord)) && lord_has_capability(lord, AOW_YORK_BURGUNDIANS) && game.flags.burgundians === 0) { + add_lord_forces(lord, BURGUNDIANS, 2) + logi(AOW_YORK_BURGUNDIANS) + game.flags.burgundians = 1 + } +} + +// === CAPABILITY: NAVAL BLOCKADE === + +function parley_through_sea(start, locale) { + // Only entered in levy + let ships = get_shared_assets(start, SHIP) + + if (ships === 0) { + game.flags.naval_blockade = -1 + } + + search_dist.fill(0) + search_seen.fill(0) + search_seen[start] = 1 + + let queue = [ start ] + while (queue.length > 0) { + let here = queue.shift() + let dist = search_dist[here] + let next_dist = dist + 1 + + if (is_friendly_locale(here)) { + for (let next of data.locales[here].adjacent) { + if (!search_seen[next]) { + search_seen[next] = 1 + search_dist[next] = next_dist + queue.push(next) + if (next === locale) { + game.flags.naval_blockade = -1 + } + } + } + } + } + queue = [ start ] + while (queue.length > 0) { + let here = queue.shift() + let dist = search_dist[here] + let next_dist = dist + 1 + + if (is_friendly_locale(here)) { + if (ships > 0 && is_seaport(here)) { + for (let next of find_ports(here)) { + if (!search_seen[next]) { + search_seen[next] = 1 + search_dist[next] = next_dist + queue.push(next) + if (next === locale && game.flags.naval_blockade !== -1) { + game.flags.naval_blockade = 1 + } + } + } + } + } + } +} + +function check_naval_blockade(action, locale) { + let ports = [data.port_1, data.port_2, data.port_3] + game.what = action + + if (!lord_has_capability(LORD_WARWICK_Y, AOW_YORK_NAVAL_BLOCKADE) || !is_seaport(get_lord_locale(LORD_WARWICK_Y)) || is_exile(get_lord_locale(LORD_WARWICK_Y))) { + return false + } + + if (action === "levy parley") { + parley_through_sea(get_lord_locale(game.who), locale) + if (game.flags.naval_blockade !== 1) { + return false + } + } + + if (action === "campaign parley" && data.locales[locale].adjacent.includes(get_lord_locale(game.command))) { + return false + } + + for (let port of ports) { + if (set_has(port, get_lord_locale(LORD_WARWICK_Y)) && set_has(port, locale)) { + return true + } + } +} + +function roll_naval_blockade() { + push_state("naval_blockade") +} + +// Parley, and Tax +states.naval_blockade = { + inactive: "Naval Blockade", + prompt() { + view.prompt = `Naval Blockade : Warwick block this action except on a 1-2` + view.actions.roll = 1 + }, + roll() { + let threshold = 2 + let roll = roll_die() + let success = threshold >= roll + log(`Attempt to counter Naval Blockade ${success ? "Failed" : "Successful"}: (1-2) ${success ? HIT[roll] : MISS[roll]}`) + if (success) { + logi(`Successfully overran C${AOW_YORK_NAVAL_BLOCKADE}`) + if (game.what === "levy parley") { + game.flags.naval_blockade = -1 + } + if (game.what === "campaign parley") { + game.flags.naval_blockade = -1 + } + if (game.what === "levy ship") { + push_the_kings_name() + add_lord_assets(game.who, SHIP, 1) + goto_the_kings_name("Levy Ship") + } + if (game.what === "supply") { + use_port_supply(game.where, get_port_supply_amount(game.where)) + } + if (game.what === "sail") { + do_sail(game.where) + } + } + else { + logi(`Failed C${AOW_YORK_NAVAL_BLOCKADE}`) + if (game.what === "levy parley") { + pop_state() + } + if (game.what === "campaign parley") { + pop_state() + } + } + if (game.what === "levy parley") { + pop_state() + resume_levy_muster_lord() + } + if (game.what === "campaign parley") { + pop_state() + --game.count + resume_command() + } + if (game.what === "levy ship") { + pop_state() + resume_levy_muster_lord() + } + if (game.what === "supply" && !success) { + end_supply() + } + if (game.what === "sail" && !success) { + resume_command() + } + game.what = NOTHING + }, +} + +// === CAPABILITY: AGITATORS === + +function can_action_agitators() { + let here = get_lord_locale(game.command) + if (game.actions <= 0) + return false + if (lord_has_capability(game.command, AOW_YORK_AGITATORS)) { + for (let next of data.locales[here].adjacent) { + if (!has_exhausted_marker(next) && !is_friendly_locale(next)) + return true + } + } + else + return false +} + +function goto_agitators() { + game.count = count_deplete(get_lord_locale(game.command)) + game.state = "agitators" +} + +states.agitators = { + inactive: "Agitators", + prompt() { + view.prompt = "Agitators: Add a depleted marker or flip to exhausted adjacent" + deplete_agitators() + }, + locale(loc) { + push_undo() + if (has_depleted_marker(loc)) { + add_exhausted_marker(loc) + } + else { + add_depleted_marker(loc) + } + end_agitators() + }, +} + +function deplete_agitators() { + let here = get_lord_locale(game.command) + for (let next of data.locales[here].adjacent) { + if (!has_exhausted_marker(next) && !is_friendly_locale(next)) + gen_action_locale(next) + } +} + +function end_agitators() { + spend_action(1) + push_undo() + resume_command() +} + +// === CAPABILITY: HERALDS === + +function can_action_heralds() { + if (game.actions <= 0) + return false + + if (!is_first_action()) + return false + // at a seaport + let here = get_lord_locale(game.command) + if (!is_seaport(here)) + return false + + if (!lord_has_capability(game.command, AOW_LANCASTER_HERALDS)) + return false + + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (is_lord_on_calendar(lord)) + return true + } + return false +} + +function goto_heralds() { + game.state = "heralds" +} + +states.heralds = { + inactive: "Heralds", + prompt() { + view.prompt = "Heralds: Choose a Lord on calendar to shift him to next turn box" + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (is_lord_on_calendar(lord)) + gen_action_lord(lord) + } + }, + lord(other) { + goto_heralds_attempt(other) + }, +} + +function goto_heralds_attempt(lord) { + game.what = lord + game.state = "heralds_attempt" + init_influence_check(game.command) +} + +states.heralds_attempt = { + inactive: "Heralds Attempt", + prompt() { + view.prompt = `Attempt to shift ${lord_name[game.what]} to next Turn Box. ` + prompt_influence_check() + }, + spend1: add_influence_check_modifier_1, + spend3: add_influence_check_modifier_2, + check() { + let results = do_influence_check() + log(`Attempt to shift L${game.what} ${results.success ? "Successful" : "Failed"}: (${range(results.rating)}) ${results.success ? HIT[results.roll] : MISS[results.roll]}`) + + if (results.success) { + game.who = game.what + set_lord_calendar(game.who, current_turn() + 1) + } + + end_heralds_attempt() + }, +} + +function end_heralds_attempt() { + end_influence_check() + spend_all_actions() + resume_command() +} + +// === EVENTS: IMMEDIATE === + +function goto_immediate_event(c) { + switch (c) { + // This Levy / Campaign + case EVENT_LANCASTER_BE_SENT_FOR: + set_add(game.events, c) + return end_immediate_event() + case EVENT_LANCASTER_SEAMANSHIP: + set_add(game.events, c) + return end_immediate_event() + case EVENT_LANCASTER_FORCED_MARCHES: + set_add(game.events, c) + return end_immediate_event() + case EVENT_LANCASTER_RISING_WAGES: + set_add(game.events, c) + return end_immediate_event() + case EVENT_LANCASTER_NEW_ACT_OF_PARLIAMENT: + set_add(game.events, c) + return end_immediate_event() + case EVENT_LANCASTER_MY_CROWN_IS_IN_MY_HEART: + set_add(game.events, c) + return end_immediate_event() + case EVENT_LANCASTER_PARLIAMENT_VOTES: + set_add(game.events, c) + return end_immediate_event() + case EVENT_LANCASTER_FRENCH_FLEET: + set_add(game.events, c) + return end_immediate_event() + case EVENT_LANCASTER_BUCKINGHAMS_PLOT: + set_add(game.events, c) + return end_immediate_event() + case EVENT_LANCASTER_MARGARET_BEAUFORT: + set_add(game.events, c) + return end_immediate_event() + case EVENT_LANCASTER_THE_EARL_OF_RICHMOND: + set_add(game.events, c) + return end_immediate_event() + + case EVENT_YORK_JACK_CADE: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_SEAMANSHIP: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_YORKISTS_BLOCK_PARLIAMENT: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_EXILE_PACT: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_RICHARD_OF_YORK: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_THE_COMMONS: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_SUCCESSION: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_LOYALTY_AND_TRUST: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_OWAIN_GLYNDWR: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_GLOUCESTER_AS_HEIR: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_DORSET: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_THE_KINGS_NAME: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_EDWARD_V: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_AN_HONEST_TALE_SPEEDS_BEST: + set_add(game.events, c) + return end_immediate_event() + case EVENT_YORK_PRIVY_COUNCIL: + set_add(game.events, c) + return end_immediate_event() + + // Immediate effect + // Discard - Immediate Events + case EVENT_LANCASTER_SCOTS: + return goto_lancaster_event_scots() + case EVENT_LANCASTER_HENRY_PRESSURES_PARLIAMENT: + return goto_lancaster_event_henry_pressures_parliament() + case EVENT_LANCASTER_HENRYS_PROCLAMATION: + return goto_lancaster_event_henrys_proclamation() + case EVENT_LANCASTER_FRENCH_TROOPS: + return goto_lancaster_event_french_troops() + case EVENT_LANCASTER_WARWICKS_PROPAGANDA: + return goto_warwicks_propaganda() + case EVENT_LANCASTER_WARWICKS_PROPAGANDA2: + return goto_warwicks_propaganda() + case EVENT_LANCASTER_WELSH_REBELLION: + return goto_lancaster_event_welsh_rebellion() + case EVENT_LANCASTER_HENRY_RELEASED: + return goto_lancaster_event_henry_released() + case EVENT_LANCASTER_LUNIVERSELLE_ARAGNE: + return goto_lancaster_event_luniverselle_aragne() + case EVENT_LANCASTER_TO_WILFUL_DISOBEDIANCE: + return goto_lancaster_event_to_wilful_disobediance() + case EVENT_LANCASTER_FRENCH_WAR_LOANS: + return goto_lancaster_event_french_war_loans() + case EVENT_LANCASTER_ROBINS_REBELLION: + return goto_lancaster_event_robins_rebellion() + case EVENT_LANCASTER_TUDOR_BANNERS: + return goto_lancaster_event_tudor_banners() + case EVENT_YORK_TAX_COLLECTORS: + return goto_york_event_tax_collectors() + case EVENT_YORK_LONDON_FOR_YORK: + return goto_york_event_london_for_york() + case EVENT_YORK_SHEWOLF_OF_FRANCE: + return goto_york_event_shewolf_of_france() + case EVENT_YORK_SIR_RICHARD_LEIGH: + return goto_york_event_sir_richard_leigh() + case EVENT_YORK_CHARLES_THE_BOLD: + return goto_york_event_charles_the_bold() + case EVENT_YORK_DUBIOUS_CLARENCE: + return goto_dubious_clarence() + case EVENT_YORK_YORKIST_NORTH: + return goto_york_event_yorkist_north() + case EVENT_YORK_EARL_RIVERS: + return goto_york_event_earl_rivers() + default: + log("NOT IMPLEMENTED") + return end_immediate_event() + } +} + +function end_immediate_event() { + resume_levy_arts_of_war() +} + +// === EVENT: LANCASTER SCOTS === + +function goto_lancaster_event_scots() { + game.state = "scots" + game.count = [] + game.who = NOBODY +} + +function end_lancaster_event_scots() { + game.count = 0 + game.who = NOBODY + end_immediate_event() +} + +states.scots = { + inactive: "Scots", + prompt() { + view.prompt = "Scots: You may add 1 Men-at-Arms and 1 Militia to each Lord." + for (let lord = first_lancaster_lord; lord <= last_lancaster_lord; lord++) { + if (is_lord_on_map(lord) && map_get(game.count, lord, 0) < 3) { + gen_action_lord(lord) + } + } + + if (game.who !== NOBODY) { + let troops = map_get(game.count, game.who, 0) + if ((troops & 1) === 0) + view.actions.add_militia = 1 + if ((troops & 2) === 0) + view.actions.add_men_at_arms = 1 + } + view.actions.done = 1 + }, + done() { + end_lancaster_event_scots() + }, + add_militia() { + add_lord_forces(game.who, MILITIA, 1) + let troops = map_get(game.count, game.who, 0) + map_set(game.count, game.who, troops + 1) + if (troops !== 0) + game.who = NOBODY + }, + add_men_at_arms() { + add_lord_forces(game.who, MEN_AT_ARMS, 1) + let troops = map_get(game.count, game.who, 0) + map_set(game.count, game.who, troops + 2) + if (troops !== 0) + game.who = NOBODY + }, + lord(lord) { + push_undo() + game.who = lord + }, +} + +// === EVENT: LANCASTER HENRY PRESSURES PARLIAMENT === + +function goto_lancaster_event_henry_pressures_parliament() { + let count = 0 + for (let vassal = first_vassal; vassal <= last_vassal; vassal++) { + if (is_vassal_mustered_with_york_lord(vassal)) { + count++ + } + } + + if (count > 0) { + logi(`Removed ${count} York influence.`) + reduce_york_influence(count) + } + + end_immediate_event() +} + +// === EVENT: LANCASTER HENRY'S PROCLAMATION === + +function goto_lancaster_event_henrys_proclamation() { + for (let vassal = first_vassal; vassal <= last_vassal; vassal++) { + if (is_vassal_mustered_with_york_lord(vassal)) { + set_vassal_lord_and_service(vassal, get_vassal_lord(vassal), current_turn()) + logi(`Vassal ${data.vassals[vassal].name} moved to current turn`) + + } + } + end_immediate_event() +} + +// === EVENT: LANCASTER FRENCH TROOPS === + +function goto_lancaster_event_french_troops() { + let can_play = false + for (let lord = first_friendly_lord; lord <= last_friendly_lord; lord++) { + if (is_lord_on_map(lord) && data.seaports.includes(get_lord_locale(lord))) { + can_play = true + } + } + if (can_play) { + game.state = "french_troops" + game.who = NOBODY + game.count = 0 + } else { + end_immediate_event() + } +} + +function end_lancaster_event_french_troops() { + game.who = NOBODY + game.count = 0 + end_immediate_event() +} + +states.french_troops = { + inactive: "French Troops", + prompt() { + + view.prompt = `Add 2 Men at Arms and 2 Militia to a Lord at a port.` + if (game.who === NOBODY) { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; lord++) { + if (is_lord_on_map(lord) && is_seaport(get_lord_locale(lord))) { + gen_action_lord(lord) + } + } + } else { + view.prompt = `Add ${2-pack2_get(game.count, 0)} Men at Arms and ${2-pack2_get(game.count, 1)} Militia to ${lord_name[game.who]}.` + if (pack2_get(game.count, 0) < 2) + view.actions.add_men_at_arms = 1 + if (pack2_get(game.count, 1) < 2) + view.actions.add_militia = 1 + } + + view.actions.done = 1 + }, + add_men_at_arms() { + push_undo() + add_lord_forces(game.who, MEN_AT_ARMS, 1) + let c = pack2_get(game.count, 0) + game.count = pack2_set(game.count, 0, c+1) + }, + add_militia() { + push_undo() + add_lord_forces(game.who, MILITIA, 1) + let c = pack2_get(game.count, 1) + game.count = pack2_set(game.count, 1, c+1) + }, + lord(lord) { + push_undo() + game.who = lord + }, + done() { + end_lancaster_event_french_troops() + } +} + +// === EVENT: WARWICKS PROPAGANDA === + +function add_propaganda_target(loc) { + set_add(game.event_data, loc) +} + +function remove_propaganda_target(loc) { + set_delete(game.event_data, loc) +} + +function is_propaganda_target(loc) { + return set_has(game.event_data, loc) +} + +function goto_warwicks_propaganda() { + let can_play = false + for (let loc = first_locale; loc <= last_locale; ++loc) { + if (has_favoury_marker(loc)) { + can_play = true + } + } + + if (can_play) { + game.state = "warwicks_propaganda" + game.event_data = [] + game.where = NOWHERE + game.count = 0 + } else { + end_immediate_event() + } +} + +states.warwicks_propaganda = { + inactive: "Warwick's Propaganda", + prompt() { + view.prompt = `Select up to ${3-game.count} Yorkists Locales.` + for (let loc = first_locale; loc <= last_locale; loc++) { + if (game.count < 3 && has_favoury_marker(loc) && !is_exile(loc) && !is_propaganda_target(loc)) { + gen_action_locale(loc) + } + } + view.actions.done = 1 + }, + locale(loc) { + push_undo() + add_propaganda_target(loc) + game.count++ + }, + done() { + goto_yorkist_choice() + } +} + +function goto_yorkist_choice() { + game.where = NOBODY + set_active_enemy() + game.state = "warwicks_propaganda_yorkist_choice" +} + +states.warwicks_propaganda_yorkist_choice = { + inactive: "Yorkists to choose to Pay or Remove favour", + prompt() { + view.prompt = `For each Stronghold, Pay 2 influence or Remove favour.` + let done = true + if (game.where === NOBODY) { + for (let loc = first_locale; loc <= last_locale; loc++) { + if (is_propaganda_target(loc)) { + gen_action_locale(loc) + done = false + } + } + if (done) { + view.actions.done = 1 + } + } else { + view.actions.remove_favour = 1 + view.actions.pay = 1 + } + }, + locale(loc) { + game.where = loc + }, + remove_favour() { + push_undo() + remove_favoury_marker(game.where) + remove_propaganda_target(game.where) + logi(`Removed favour in ${game.where}`) + game.where = NOBODY + }, + pay() { + push_undo() + reduce_influence(2) + logi(`Paid 2 to keep ${game.where}`) + remove_propaganda_target(game.where) + game.where = NOWHERE + }, + done() { + end_warwicks_propaganda() + }, +} + +function end_warwicks_propaganda() { + game.event_data = 0 + game.where = NOWHERE + game.count = 0 + set_active_enemy() + end_immediate_event() +} + +// === EVENT: WELSH REBELLION === + +function goto_lancaster_event_welsh_rebellion() { + let can_remove_troops = false + let can_remove_favour = false + for (let lord = first_york_lord; lord <= last_york_lord; ++lord) { + if (is_lord_on_map(lord) && is_lord_in_wales(lord)) { + set_lord_moved(lord, 1) + can_remove_troops = true + } + } + for (let loc = first_locale; loc <= last_locale; loc++) { + if (is_wales(loc) && has_favoury_marker(loc)) + can_remove_favour = true + } + + if (can_remove_troops) { + game.state = "welsh_rebellion_remove_troops" + game.who = NOBODY + game.count = 0 + } + else if (can_remove_favour) { + game.state = "welsh_rebellion_remove_favour" + game.who = NOBODY + game.count = 0 + } + else { + end_immediate_event() + } +} + +states.welsh_rebellion_remove_troops = { + inactive: "Welsh Rebellion \u2014 Remove troops", + prompt() { + view.prompt = `Remove 2 Troops from each enemy Lord in Wales.` + let done = true + if (game.who === NOBODY) { + for (let lord = first_enemy_lord; lord <= last_enemy_lord; lord++) { + if (is_lord_on_map(lord) && is_lord_in_wales(lord) && get_lord_moved(lord)) { + gen_action_lord(lord) + done = false + } + } + if (done) { + view.actions.done = 1 + } + } + else { + view.prompt = `Remove ${game.count} Troops from ${data.lords[game.who].name}.` + if (get_lord_forces(game.who, BURGUNDIANS) > 0) + gen_action_burgundians(game.who) + if (get_lord_forces(game.who, MERCENARIES) > 0) + gen_action_mercenaries(game.who) + if (get_lord_forces(game.who, LONGBOWMEN) > 0) + gen_action_longbowmen(game.who) + if (get_lord_forces(game.who, MEN_AT_ARMS) > 0) + gen_action_men_at_arms(game.who) + if (get_lord_forces(game.who, MILITIA) > 0) + gen_action_militia(game.who) + } + }, + lord(lord) { + push_undo() + set_lord_moved(lord, 0) + game.who = lord + game.count = 2 + }, + burgundians(lord) { + add_lord_forces(lord, BURGUNDIANS, -1) + if (--game.count === 0 || !lord_has_unrouted_units(lord)) + game.who = NOBODY + }, + mercenaries(lord) { + add_lord_forces(lord, MERCENARIES, -1) + if (--game.count === 0 || !lord_has_unrouted_units(lord)) + game.who = NOBODY + }, + longbowmen(lord) { + add_lord_forces(lord, LONGBOWMEN, -1) + if (--game.count === 0 || !lord_has_unrouted_units(lord)) + game.who = NOBODY + }, + men_at_arms(lord) { + add_lord_forces(lord, MEN_AT_ARMS, -1) + if (--game.count === 0 || !lord_has_unrouted_units(lord)) + game.who = NOBODY + }, + militia(lord) { + add_lord_forces(lord, MILITIA, -1) + if (--game.count === 0 || !lord_has_unrouted_units(lord)) + game.who = NOBODY + }, + done() { + end_welsh_rebellion_remove_troops() + }, +} + +function end_welsh_rebellion_remove_troops() { + for (let lord = first_york_lord; lord <= last_york_lord; ++lord) { + if (is_lord_on_map(lord) && is_lord_in_wales(lord) && !lord_has_unrouted_units(lord)) + disband_lord(lord, false) + } + game.count = 0 + game.who = NOBODY + end_immediate_event() +} + +states.welsh_rebellion_remove_favour = { + inactive: "Welsh Rebellion \u2014 Remove Favour", + prompt() { + view.prompt = `Select up to ${2-game.count} Locales in Wales.` + for (let loc = first_locale; loc <= last_locale; loc++) { + if (game.count < 2 && is_wales(loc) && has_favoury_marker(loc)) { + gen_action_locale(loc) + } + } + view.actions.done = 1 + }, + locale(loc) { + push_undo() + remove_favoury_marker(loc) + logi(`Removed favour at ${data.locales[loc].name}`) + game.count++ + }, + done() { + end_immediate_event() + }, +} + +// === EVENT: HENRY RELEASED === + +function goto_lancaster_event_henry_released() { + if (has_favourl_marker(LOC_LONDON)) { + logi(`Henry Released : 5 Influence for Lancaster`) + increase_lancaster_influence(5) + } + end_immediate_event() +} + +// === EVENT: L'UNIVERSELLE ARAGNE === + +function goto_lancaster_event_luniverselle_aragne() { + let can_play = false + for (let vassal = first_vassal; vassal <= last_vassal; vassal++) { + if (is_vassal_mustered_with_york_lord(vassal)) { + can_play = true + } + } + if (can_play) { + game.state = "aragne_1" + game.event_data = [] + } else { + logi("No Effect") + end_immediate_event() + } +} + +states.aragne_1 = { + inactive: "L'universelle Aragne", + prompt() { + view.prompt = "Select up to 2 Vassals" + if (game.event_data.length < 2) { + for (let v = first_vassal; v <= last_vassal; v++) { + if (!set_has(game.event_data, v) && is_vassal_mustered_with_york_lord(v)) { + gen_action_vassal(v) + } + } + } + view.actions.done = 1 + }, + vassal(v) { + push_undo() + set_add(game.event_data, v) + logi(`Vassal ${data.vassals[v].name} selected`) + }, + done() { + push_undo() + goto_yorkist_aragne() + }, +} + +function goto_yorkist_aragne() { + set_active_enemy() + game.state = "aragne_2" +} + +states.aragne_2 = { + inactive: "Influence checks", + prompt() { + view.prompt = `For Each vassal, influence check : failure disbands it` + let done = true + for (let v of game.event_data) { + gen_action_vassal(v) + done = false + } + if (done) + view.actions.done = 1 + }, + vassal(other) { + push_undo() + game.who = other + init_influence_check(get_vassal_lord(other)) + game.check.push({ + cost: 0, + modifier: data.vassals[other].influence * (game.active === LANCASTER ? -1 : 1), + source: "vassal", + }) + game.state = "aragne_3" + }, + done() { + end_universelle_aragne() + }, +} + +states.aragne_3 = { + inactive: `Influence check`, + prompt() { + view.prompt = `Influence check : Failure disbands ${data.vassals[game.who].name}` + prompt_influence_check() + }, + spend1: add_influence_check_modifier_1, + spend3: add_influence_check_modifier_2, + check() { + let results = do_influence_check() + logi(`Attempt to save ${data.vassals[game.who].name} ${results.success ? "Successful" : "Failed"}: (${range(results.rating)}) ${results.success ? HIT[results.roll] : MISS[results.roll]}`) + + if (!results.success) { + disband_vassal(game.who) + } + + end_influence_check() + + set_delete(game.event_data, game.who) + game.who = NOBODY + game.state = "aragne_2" + }, +} + +function end_universelle_aragne() { + game.who = NOBODY + game.event_data = 0 + end_immediate_event() +} + +// === EVENT: TO WILFUL DISOBEDIANCE === + +function goto_lancaster_event_to_wilful_disobediance() { + let can_play = false + for (let loc = first_locale; loc <= last_locale; loc++){ + if (has_favoury_marker(loc) && !has_enemy_lord(loc) && !has_adjacent_enemy(loc) && (has_friendly_lord(loc) || has_adjacent_friendly(loc))) { + can_play = true + } + } + if (can_play) { + game.state = "wilful_disobediance" + game.who = NOBODY + game.count = 0 + } else { + end_immediate_event() + logi(`No Effect`) + } + +} +states.wilful_disobediance = { + inactive: "to wilful disobediance", + prompt() { + view.prompt = `Select up to ${2-game.count} Yorkists Locales.` + for (let loc = first_locale; loc <= last_locale; loc++) { + if ( + game.count < 2 && + has_favoury_marker(loc) && + !has_enemy_lord(loc) && + !has_adjacent_enemy(loc) && + (has_friendly_lord(loc) || has_adjacent_friendly(loc)) + ) { + gen_action_locale(loc) + } + } + view.actions.done = 1 + }, + locale(loc) { + push_undo() + remove_favoury_marker(loc) + game.count++ + logi(`Favour removed at ${loc}`) + }, + done() { + logi(`No Effect`) + end_immediate_event() + } +} + +// === EVENT: FRENCH WAR LOANS === + +function goto_lancaster_event_french_war_loans() { + for (let lord = first_lancaster_lord; lord <= last_lancaster_lord; ++lord) { + if (is_lord_on_map(lord) && !is_lord_on_calendar(lord)) { + add_lord_assets(lord, PROV, 1) + add_lord_assets(lord, COIN, 1) + logi(`1 Coin and 1 Provender added to ${data.lords[lord].name}`) + } + } + end_immediate_event() +} + +// === EVENT: ROBINS REBELLION === + +function goto_lancaster_event_robins_rebellion() { + let can_play = false + for (let loc = first_locale; loc <= last_locale; loc++) { + if (is_north(loc) && !has_favourl_marker(loc)) { + can_play = true + } + } + if (can_play) { + game.state = "robins_rebellion" + game.who = NOBODY + game.count = 0 + } else { + logi(`No Effect`) + end_immediate_event() + } +} + +states.robins_rebellion = { + inactive: "Robin's Rebellion", + prompt() { + view.prompt = `Select up to ${3-game.count} Locales in North.` + for (let loc = first_locale; loc <= last_locale; loc++) { + if (game.count < 3 && is_north(loc) && !has_favourl_marker(loc)) { + gen_action_locale(loc) + } + } + view.actions.done = 1 + }, + locale(loc) { + push_undo() + shift_favour_toward(loc) + logi(`Placed/Removed favour at ${data.locales[loc].name}`) + game.count++ + }, + done() { + end_immediate_event() + } +} + +// === EVENT: TUDOR BANNERS === + +function tudor_banner_eligible() { + if (is_lord_on_map(LORD_HENRY_TUDOR) && !is_lord_on_calendar(LORD_HENRY_TUDOR)) { + for (let next of data.locales[get_lord_locale(LORD_HENRY_TUDOR)].adjacent) { + if (can_parley_at(next)) + return true + } + } + return false +} + +function goto_lancaster_event_tudor_banners() { + if (tudor_banner_eligible()) { + game.state = "tudor_banners" + game.who = NOBODY + } else { + logi(`No Effect`) + end_immediate_event() + } +} + +states.tudor_banners = { + inactive: "Tudor banners", + prompt() { + view.prompt = `Select locales adjacent to Henry to make them Lancastrian` + let done = true + for (let next of data.locales[get_lord_locale(LORD_HENRY_TUDOR)].adjacent) { + if (!has_enemy_lord(next) && !has_favourl_marker(next)) { + gen_action_locale(next) + done = false + } + } + if (done) + view.actions.done = 1 + }, + locale(loc) { + push_undo() + remove_favoury_marker(loc) + add_favourl_marker(loc) + logi(`Placed Lancastrian favour at ${data.locales[loc].name}`) + }, + done() { + end_immediate_event() + } +} + +// === EVENT: TAX COLLECTORS === + +function goto_york_event_tax_collectors() { + game.state = "tax_collectors" +} + +function can_tax_collectors(lord) { + let here = get_lord_locale(lord) + if (can_tax_at(here, lord)) + return true + return !!search_tax(false, here, lord) +} + +states.tax_collectors = { + inactive: "Tax Collectors", + prompt() { + view.prompt = "Tax Collectors : You may tax for Double coin with each lord" + for (let lord = first_york_lord; lord <= last_york_lord; ++lord) { + if (!get_lord_moved(lord) && can_tax_collectors(lord)) + gen_action_lord(lord) + } + view.actions.done = 1 + }, + lord(lord) { + push_undo() + set_lord_moved(lord, 0) + game.where = NOWHERE + game.who = lord + game.state = "tax_collectors_lord" + init_influence_check(lord) + }, + done() { + end_tax_collectors() + }, +} + +states.tax_collectors_lord = { + inactive: "Tax Collectors", + prompt() { + view.prompt = "Tax: Select the location to tax for double." + if (game.where === NOWHERE) { + for (let loc of search_tax([], get_lord_locale(game.who), game.who)) + gen_action_locale(loc) + } else { + view.prompt = `Tax: Attempting to tax ${data.locales[game.where].name}. ` + prompt_influence_check() + } + }, + locale(loc) { + game.where = loc + if (loc === data.lords[game.who].seat) { + // Auto succeed without influence check at Lords seat. + deplete_locale(game.where) + + log(`Taxed %${game.where}.`) + add_lord_assets(game.who, COIN, get_tax_amount(game.where, game.who) * 2) + end_tax_collectors_lord() + } + }, + spend1: add_influence_check_modifier_1, + spend3: add_influence_check_modifier_2, + check() { + let results = do_influence_check() + logi(`Tax : ${results.success ? "Successful" : "Failed"}: (${range(results.rating)}) ${results.success ? HIT[results.roll] : MISS[results.roll]}`) + + if (results.success) { + deplete_locale(game.where) + log(`Taxed %${game.where}.`) + add_lord_assets(game.who, COIN, get_tax_amount(game.where, game.who) * 2) + } else { + log(`Tax of %${game.where} failed.`) + + } + end_tax_collectors_lord() + }, +} + +function end_tax_collectors_lord() { + game.who = NOBODY + game.state = "tax_collectors" +} + +function end_tax_collectors() { + game.where = NOWHERE + game.who = NOBODY + game.count = 0 + end_immediate_event() +} + +// === EVENT: LONDON FOR YORK === + +function goto_york_event_london_for_york() { + let can_play = false + if (has_favoury_marker(LOC_LONDON)) { + can_play = true + } + if (can_play) { + game.who = NOBODY + game.state = "london_for_york" + } else { + logi(`No Effect`) + end_immediate_event() + } +} + +states.london_for_york = { + inactive: "London For York", + prompt() { + view.prompt = `Select London to add a second favour marker` + gen_action_locale(LOC_LONDON) + }, + locale(loc) { + push_undo() + game.flags.london_for_york = 1 + logi(`Second marker placed at ${data.locales[loc].name}`) + logi(`Immune to Lancastrian parley unless aided by event`) + end_immediate_event() + }, +} + +// === EVENT: SHE-WOLF OF FRANCE === + +function goto_york_event_shewolf_of_france() { + let can_play = false + for (let v = first_vassal; v <= last_vassal; v++) { + if (is_vassal_mustered_with_friendly_lord(v)) { + can_play = true + } + } + if (can_play) { + game.state = "she_wolf" + game.event_data = [] + game.who = NOBODY + } else { + logi(`No Effect`) + end_immediate_event() + } +} + +states.she_wolf = { + inactive: "She-Wolf of France", + prompt() { + view.prompt = "Shift each Yorkist Vassal's Calendar marker 1 box right." + let done = true + for (let v = first_vassal; v <= last_vassal; v++) { + if (!set_has(game.event_data, v) && is_vassal_mustered_with_friendly_lord(v)) { + gen_action_vassal(v) + done = false + } + } + if (done) + view.actions.done = 1 + }, + vassal(v) { + push_undo() + if (current_turn() < 16) + set_vassal_lord_and_service(v, get_vassal_lord(v), get_vassal_service(v) + 1) + set_add(game.event_data, v) + logi(`Vassal ${data.vassals[v].name} shifted one calendar box`) + }, + done() { + game.event_data = 0 + end_immediate_event() + }, +} + +// === EVENT: RICHARD LEIGH === + +function goto_york_event_sir_richard_leigh() { + let can_play = false + if (!has_favoury_marker(LOC_LONDON)) { + can_play = true + } + if (can_play) { + game.state = "richard_leigh" + game.who = LOC_LONDON + game.count = 0 + } else { + logi(`No Effect`) + end_immediate_event() + } +} + +states.richard_leigh = { + inactive: "Richard Leigh", + prompt() { + view.prompt = `Select London, shift it once in your favour` + if (game.who === LOC_LONDON && !has_favoury_marker(LOC_LONDON)) { + gen_action_locale(LOC_LONDON) + } else { + view.actions.done = 1 + } + }, + locale(loc) { + push_undo() + shift_favour_toward(loc) + logi(`London shifted once in your favour`) + game.who = NOBODY + }, + done() { + end_immediate_event() + } +} + +// === EVENT: CHARLES THE BOLD === + +function goto_york_event_charles_the_bold() { + for (let lord = first_york_lord; lord <= last_york_lord; ++lord) { + if (is_lord_on_map(lord) && !is_lord_on_calendar(lord)) { + add_lord_assets(lord, PROV, 1) + add_lord_assets(lord, COIN, 1) + logi(`1 Coin and 1 Provender added to ${data.lords[lord].name}`) + } + } + end_immediate_event() +} + +// === EVENT: DUBIOUS CLARENCE === + +function goto_dubious_clarence() { + let can_play = false + if ((is_lord_on_map(LORD_EDWARD_IV) && !is_lord_on_calendar(LORD_EDWARD_IV)) + && is_lord_on_map(LORD_CLARENCE) && !is_lord_on_calendar(LORD_CLARENCE)) + can_play = true + + if (can_play) { + game.state = "dubious_clarence" + game.who = LORD_EDWARD_IV + init_influence_check(LORD_EDWARD_IV) + } else { + logi(`No Effect`) + end_immediate_event() + } +} + +states.dubious_clarence = { + inactive: "Dubious Clarence", + prompt() { + view.prompt = `You may Influence check with Edward to disband Clarence ` + prompt_influence_check() + }, + spend1: add_influence_check_modifier_1, + spend3: add_influence_check_modifier_2, + check() { + let results = do_influence_check() + log(`Attempt to disband Clarence ${results.success ? "Successful" : "Failed"}: (${range(results.rating)}) ${results.success ? HIT[results.roll] : MISS[results.roll]}`) + + if (results.success) { + disband_lord(LORD_CLARENCE, false) + end_immediate_event() + } else { + end_immediate_event() + } + }, +} + +// === EVENT: YORKIST NORTH === + +function goto_york_event_yorkist_north() { + let influence_gained = 0 + for (let lord = first_york_lord; lord <= last_york_lord; ++lord) { + if (is_lord_on_map(lord) && !is_lord_on_calendar(lord) && is_lord_in_north(lord)) + influence_gained++ + } + for (let loc = first_locale; loc <= last_locale; loc++) { + if (loc !== NOWHERE && loc < CALENDAR && has_favoury_marker(loc) && is_north(loc)) { + influence_gained++ + } + } + logi(`Yorkist North : ${influence_gained} Influence for Yorkists`) + increase_york_influence(influence_gained) + end_immediate_event() +} + +// === EVENT: EARL RIVERS === + +function goto_york_event_earl_rivers() { + game.state = "earl_rivers" + game.count = [] + game.who = NOBODY +} + +function end_york_event_earl_rivers() { + game.count = 0 + game.who = NOBODY + end_immediate_event() +} + +states.earl_rivers = { + inactive: "Earl Rivers", + prompt() { + view.prompt = "Earl Rivers: Add up to 2 Militia to each lord" + view.actions.done = 1 + for (let lord = first_york_lord; lord <= last_york_lord; lord++) { + if (is_lord_on_map(lord) && map_get(game.count, lord, 0) < 3) { + gen_action_lord(lord) + } + } + + if (game.who !== NOBODY) { + let troops = map_get(game.count, game.who, 0) + if ((troops & 1) === 0) + view.actions.add_militia = 1 + } + if (game.who !== NOBODY) { + let troops = map_get(game.count, game.who, 0) + if ((troops & 1) === 0) + view.actions.add_militia2 = 1 + } + }, + done() { + end_york_event_earl_rivers() + }, + add_militia() { + push_undo() + add_lord_forces(game.who, MILITIA, 1) + let troops = map_get(game.count, game.who, 0) + map_set(game.count, game.who, troops + 1) + if (troops > 1) + game.who = NOBODY + }, + add_militia2() { + push_undo() + add_lord_forces(game.who, MILITIA, 2) + let troops = map_get(game.count, game.who, 0) + map_set(game.count, game.who, troops + 1) + if (troops > 1) + game.who = NOBODY + }, + lord(lord) { + push_undo() + game.who = lord + } +} + +// === EVENT (AS LEVY EFFECT): THE KINGS NAME === + +function eligible_kings_name() { + if ( + (!is_lord_on_calendar(LORD_GLOUCESTER_1) && is_lord_on_map(LORD_GLOUCESTER_1)) || + (!is_lord_on_calendar(LORD_GLOUCESTER_2) && is_lord_on_map(LORD_GLOUCESTER_2)) + ) { + if (is_event_in_play(EVENT_YORK_THE_KINGS_NAME) && game.active === LANCASTER) + return true + } + return false +} + +function push_the_kings_name() { + if (eligible_kings_name()) + save_state_for_the_kings_name() +} + +function goto_the_kings_name(action) { + if (eligible_kings_name()) { + // TODO: pause for confirmation before changing control? + game.what = action + set_active_enemy() + game.state = "the_kings_name" + } else { + resume_levy_muster_lord() + } +} + +states.the_kings_name = { + inactive: "The King's Name", + prompt() { + view.prompt = `The King's Name: You pay may 1 Influence to cancel last ${game.what} action.` + view.actions.pass = 1 + view.actions.pay = 1 + }, + pay() { + restore_state_for_the_kings_name() + log(`${game.what} action cancelled.`) + logevent(`${EVENT_YORK_THE_KINGS_NAME}`) + reduce_york_influence(1) + resume_levy_muster_lord() + }, + pass() { + delete_state_for_the_kings_name() + set_active_enemy() + resume_levy_muster_lord() + } +} + +// === EVENT (AS LEVY EFFECT): RISING WAGES === + +function goto_rising_wages() { + game.state = "rising_wages" +} + +states.rising_wages = { + inactive: "Rising Wages", + prompt() { + let here = get_lord_locale(game.who) + view.prompt = "Rising Wages: Pay 1 extra coin to levy troops" + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + let loc = get_lord_locale(lord) + if (here === loc && (get_lord_assets(lord, COIN) > 0)) { + gen_action_coin(lord) + } + } + }, + coin(lord) { + push_undo() + add_lord_assets(lord, COIN, -1) + logi(`${EVENT_LANCASTER_RISING_WAGES}`) + log("York paid 1 Coin to Levy troops") + + do_levy_troops() + }, +} + +// === EVENT (AS LEVY EFFECT): THE COMMONS === + +// each Levy Troops action ends with coming here + +function goto_the_commons() { + game.state = "the_commons" + game.flags.commons_militia = 2 +} + +states.the_commons = { + inactive: "The Commons", + prompt() { + view.prompt = `Add up to ${game.flags.commons_militia} Militias.` + if (game.flags.commons_militia > 0) + view.actions.add_militia = 1 + view.actions.done = 1 + }, + add_militia() { + push_undo() + add_lord_forces(game.who, MILITIA, 1) + if (--game.flags.commons_militia === 0) + end_the_commons() + }, + done() { + push_undo() + end_the_commons() + } +} + +function end_the_commons() { + game.flags.commons_militia = 0 + goto_the_kings_name("Levy Troops") +} + +// === EVENT (AS LEVY EFFECT): JACK CADE === + +function is_york_dominating_north() { + let dom = 0 + for (let loc of all_north_locales) { + if (has_favoury_marker(loc)) { + dom++ + } + } + if (dom > 5) + return true + return false +} + +function is_york_dominating_south() { + let dom = 0 + for (let loc of all_south_locales) { + if (has_favoury_marker(loc)) { + dom++ + } + } + if (dom > 9) + return true + if (dom > 4 + && (lord_has_capability(LORD_MARCH, AOW_YORK_SOUTHERNERS) + || lord_has_capability(LORD_RUTLAND, AOW_YORK_SOUTHERNERS) + || lord_has_capability(LORD_YORK, AOW_YORK_SOUTHERNERS))) + return true + return false +} + +function is_york_dominating_wales() { + let dom = 0 + for (let loc of all_wales_locales) { + if (has_favoury_marker(loc)) { + dom++ + } + } + if (dom > 5) + return true + if (dom > 2 + && (lord_has_capability(LORD_MARCH, AOW_YORK_WELSHMEN) + || lord_has_capability(LORD_YORK, AOW_YORK_WELSHMEN))) + return true + return false +} + +function is_jack_cade_eligible(lord) { + if (!is_event_in_play(EVENT_YORK_JACK_CADE)) + return false + if (is_lord_in_or_adjacent_to_south(lord) && is_york_dominating_south()) + return true + if (is_lord_in_or_adjacent_to_north(lord) && is_york_dominating_north()) + return true + if (is_lord_in_or_adjacent_to_wales(lord) && is_york_dominating_wales()) + return true + return false +} + +// === EVENT (AS ACTION): EXILE PACT === + +function can_action_exile_pact() { + return game.actions > 0 && is_event_in_play(EVENT_YORK_EXILE_PACT) +} + +function goto_exile_pact() { + push_undo() + game.state = "exile_pact" +} + +states.exile_pact = { + inactive: "Exile Pact", + prompt() { + view.prompt = "Exile Pact: Place your cylinder in a Friendly Exile box." + for (let loc of data.exile_boxes) { + if (has_favour_in_locale(game.active, loc)) + gen_action_locale(loc) + } + }, + locale(loc) { + push_undo() + set_lord_locale(game.command, loc) + end_exile_pact() + } +} + +function end_exile_pact() { + spend_action(1) + push_undo() + resume_command() +} + +// === EVENTS: HELD === + +function play_held_event(c) { + log(`Played E${c}.`) + if (c >= first_york_card && c <= last_york_card) + set_delete(game.hand_y, c) + else + set_delete(game.hand_l, c) + + /* Hold events with This Levy/Campaign */ + if ( + c === EVENT_YORK_YORKIST_PARADE || + c === EVENT_YORK_PARLIAMENTS_TRUCE || + c === EVENT_LANCASTER_PARLIAMENTS_TRUCE + ) { + set_add(game.events, c) + } +} + +function prompt_held_event() { + for (let c of current_hand()) + if (can_play_held_event(c)) + gen_action_card(c) +} + +function can_play_held_event(c) { + switch (c) { + case EVENT_LANCASTER_ASPIELLES: + return can_play_l_aspielles() + // TODO: move into states.command ? + case EVENT_LANCASTER_REBEL_SUPPLY_DEPOT: + return can_play_rebel_supply_depot() + // TODO: move into states.command ? + case EVENT_LANCASTER_SURPRISE_LANDING: + return can_play_surprise_landing() + case EVENT_LANCASTER_PARLIAMENTS_TRUCE: + return can_play_parliaments_truce() + case EVENT_YORK_PARLIAMENTS_TRUCE: + return can_play_parliaments_truce() + case EVENT_YORK_ASPIELLES: + return can_play_y_aspielles() + case EVENT_YORK_YORKIST_PARADE: + return can_play_yorkist_parade() + case EVENT_YORK_SUN_IN_SPLENDOUR: + return can_play_sun_in_splendour() + } + 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) { + // Play upon Death Check + // TODO: move into states.death_check + case EVENT_YORK_ESCAPE_SHIP: + case EVENT_LANCASTER_ESCAPE_SHIP: + goto_play_escape_ship() + break + case EVENT_LANCASTER_TALBOT_TO_THE_RESCUE: + goto_play_talbot_to_the_rescue() + break + case EVENT_LANCASTER_WARDEN_OF_THE_MARCHES: + goto_play_warden_of_the_marches() + break + + // Play in Levy + // TODO: move into states.levy + case EVENT_YORK_SUN_IN_SPLENDOUR: + goto_play_sun_in_splendour() + break + + // Play in Levy (for passive effect) + case EVENT_YORK_YORKIST_PARADE: + // no effect + break + + // Play any time + case EVENT_YORK_ASPIELLES: + case EVENT_LANCASTER_ASPIELLES: + goto_play_aspielles() + break + + // Play after march/sail to seaport + // TODO: move into states.command ? + case EVENT_LANCASTER_REBEL_SUPPLY_DEPOT: + goto_play_rebel_supply_depot() + break + + // Play after sail to seaport + // TODO: move into states.command ? + case EVENT_LANCASTER_SURPRISE_LANDING: + goto_play_surprise_landing() + break + } +} + +// === HELD EVENT (LEVY): YORKIST PARADE === + +function can_play_yorkist_parade() { + if (is_levy_phase()) { + if (is_favour_friendly(LOC_LONDON) && (get_lord_locale(LORD_WARWICK_Y) === LOC_LONDON || get_lord_locale(LORD_YORK) === LOC_LONDON)) + return true + } + return false +} + +// === HELD EVENT (LEVY): SUN IN SPLENDOUR === + +function can_play_sun_in_splendour() { + if (is_levy_phase()) + return is_lord_on_calendar(LORD_EDWARD_IV) + return false +} + +function goto_play_sun_in_splendour() { + push_state("sun_in_splendour") +} + +states.sun_in_splendour = { + inactive: "Sun in Splendour", + prompt() { + view.prompt = "Sun in Splendour: Muster Edward IV in any friendly locale with no enemy lord" + // ... TODO: or a scenario-designated Yorkist Exile box + for (let loc = first_locale; loc <= last_locale; loc++) + if (is_friendly_locale(loc)) + gen_action_locale(loc) + }, + locale(loc) { + push_undo() + muster_lord(LORD_EDWARD_IV, loc) + // TODO: remove_lord_from_exile(LORD_EDWARD_IV) ? + // TODO: muster to exile box ? + logi(`Mustered Edward IV at ${data.locales[loc].name}`) + + pop_state() + game.what = NOTHING + }, +} + +// === HELD EVENT: ASPIELLES === + +function can_play_l_aspielles() { + return game.hand_y.length > 0 || game.hidden +} + +function can_play_y_aspielles() { + return game.hand_l.length > 0 || game.hidden +} + +function goto_play_aspielles() { + push_state("aspielles") + game.who = NOBODY + if (game.active === YORK) + log("Lancaster hand shown to the York player") + if (game.active === LANCASTER) + log("York hand shown to the Lancaster player") +} + +states.aspielles = { + inactive: "Aspielles", + prompt() { + view.prompt = "Aspielles: You may see enemy held cards" + if (game.hidden) { + view.prompt += " and an enemy lord to see his mat" + if (game.who === NOBODY) { + for (let lord = first_enemy_lord; lord <= last_enemy_lord; ++lord) + gen_action_lord(lord) + } else { + view.reveal |= (1 << game.who) + view.actions.done = 1 + } + } else { + view.actions.done = 1 + } + if (game.active === YORK) + view.hand = game.hand_l + if (game.active === LANCASTER) + view.hand = game.hand_y + }, + lord(lord) { + log(`${lord_name[lord]} Spied`) + game.who = lord + }, + done() { + pop_state() + game.what = NOTHING + }, +} + +// === HELD EVENT: REBEL SUPPLY DEPOT === + +function can_play_rebel_supply_depot() { + if (game.flags.sail_to_port || game.flags.march_to_port) + return true + return false +} + +function goto_play_rebel_supply_depot() { + game.flags.supply_depot = 1 + add_spoils(PROV, 4) + push_state("rebel_supply_depot") +} + +states.rebel_supply_depot = { + inactive: "Rebel Supply depot", + prompt() { + if (has_any_spoils()) { + view.prompt = "Rebel Supply Depot: Divide " + list_spoils() + "." + let here = get_lord_locale(game.command) + 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 = "Rebel Supply Depot: All done." + view.actions.end_spoils = 1 + } + }, + lord: action_select_lord, + take_prov() { + push_undo_without_who() + take_spoils(PROV) + }, + end_spoils() { + push_undo_without_who() + end_rebel_supply_depot() + }, +} + +function end_rebel_supply_depot() { + pop_state() + game.what = NOTHING + game.spoils = 0 +} + +// === HELD EVENT: SURPRISE LANDING === + +function can_play_surprise_landing() { + let here = get_lord_locale(game.command) + if (game.flags.sail_to_port) { + if (here !== LOC_CALAIS && here !== LOC_PEMBROKE && here !== LOC_HARLECH && here !== LOC_LANCASTER) + return true + } + return false +} + +function goto_play_surprise_landing() { + game.state = "surprise_landing" + game.flags.surprise_landing = 2 + game.who = NOBODY +} + +states.surprise_landing = { + inactive: "Surprise Landing", + prompt() { + view.prompt = "Surprise Landing : You may march once (no path)." + prompt_held_event() + + view.group = game.group + let here = get_lord_locale(game.command) + // 4.3.2 Marshals MAY take other lords + if ( + is_marshal(game.command) || + (lord_has_capability(game.command, AOW_YORK_CAPTAIN) && !other_marshal_or_lieutenant(here)) + ) { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) + if (lord !== game.command) + if (get_lord_locale(lord) === here) + gen_action_lord(lord) + } + + // Lieutenant may not take marshall + if (is_lieutenant(game.command)) { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) + if (lord !== game.command) + if (get_lord_locale(lord) === here && !is_marshal(lord)) { + gen_action_lord(lord) + } + } + + prompt_march() + }, + lord(lord) { + set_toggle(game.group, lord) + }, + locale: goto_march, + card: action_held_event, +} + +// === LOGGING === + +function range(x) { + switch (x) { + case 0: return "0" + case 1: return "1" + case 2: return "1-2" + case 3: return "1-3" + case 4: return "1-4" + case 5: return "1-5" + case 6: return "Automatic success" + } +} + +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 log_h1(msg) { + log_br() + log(".h1 " + msg) + log_br() +} + +function log_h2(msg) { + log_br() + if (game.active === YORK) + log(".h2t " + msg) + else + log(".h2r " + msg) + log_br() +} + +function log_h3(msg) { + log_br() + if (game.active === YORK) + 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) +} + +// === VIEW & ACTION === + +exports.view = function (state, current) { + load_state(state) + + view = { + prompt: null, + actions: null, + log: game.log, + reveal: 0, + + end: scenario_last_turn[game.scenario], + turn: game.turn, + victory_check: game.victory_check, + influence: game.influence, + + events: game.events, + pieces: game.pieces, + battle: game.battle, + + held_y: game.hand_y.length, + held_l: game.hand_l.length, + + command: game.command, + hand: null, + plan: null, + } + + if (!game.hidden) + view.reveal = -1 + + if (current === YORK) { + view.hand = game.hand_y + view.plan = game.plan_y + if (game.hidden) + view.reveal |= YORK_LORD_MASK + } + if (current === LANCASTER) { + view.hand = game.hand_l + view.plan = game.plan_l + if (game.hidden) + view.reveal |= LANCASTER_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 +} + +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(lord) { + if (lord !== game.who) + gen_action_lord(lord) +} + +function action_select_lord(lord) { + if (game.who === lord) + game.who = NOBODY + else + game.who = lord +} + +function gen_action_locale(locale) { + gen_action("locale", locale) +} + +function gen_action_lord(lord) { + gen_action("lord", lord) +} + +function gen_action_array(pos) { + gen_action("array", pos) +} + +function gen_action_vassal(vassal) { + gen_action("vassal", vassal) +} + +function gen_action_card(card_or_list) { + if (Array.isArray(card_or_list)) + for (let c of card_or_list) + gen_action("card", c) + else + gen_action("card", card_or_list) +} + +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_mercenaries(lord) { + gen_action("mercenaries", lord) +} + +function gen_action_burgundians(lord) { + gen_action("burgundians", lord) +} + +function gen_action_longbowmen(lord) { + gen_action("longbowmen", lord) +} + +function gen_action_retinue(lord) { + gen_action("retinue", 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_routed_mercenaries(lord) { + gen_action("routed_mercenaries", lord) +} + +function gen_action_routed_longbowmen(lord) { + gen_action("routed_longbowmen", lord) +} + +function gen_action_routed_burgundians(lord) { + gen_action("routed_burgundians", 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) +} + +// === 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 + }, +} + +// === UTILITY FUNCTIONS === + +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 save_state_for_the_kings_name() { + let copy = {} + for (let k in game) { + let v = game[k] + if (k === "undo") + v = undefined + else if (k === "log") + v = undefined + else if (typeof v === "object" && v !== null) + v = object_copy(v) + copy[k] = v + } + game.event_data = copy +} + +function restore_state_for_the_kings_name() { + let save_log = game.log + let save_influence = game.influence + game = game.event_data + game.undo = [] + game.log = save_log + game.influence = save_influence + game.event_data = 0 +} + +function delete_state_for_the_kings_name() { + game.event_data = 0 +} + +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 roll_die() { + clear_undo() + return random(6) + 1 +} + +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 + } +} + +// 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) +} + +// 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_get_pack4(map, lord, k) { + return pack4_get(map_get(map, lord, 0), k) +} + +function map_set_pack4(map, lord, k, v) { + let val = pack4_set(map_get(map, lord, 0), k, v) + if (val === 0) + map_delete(map, lord) + else + map_set(map, lord, val) +} + +function map2_get(map, x, y, v) { + return map_get(map, (x << 1) + y, v) +} + +function map2_set(map, x, y, v) { + return map_set(map, (x << 1) + y, v) +} + +function map2_delete(map, x, y) { + return map_delete(map, (x << 1) + y) +} + +function map_has_value(map, value) { + for (let i = 1; i < map.length; i += 2) + if (map[i] === value) + return true + return false +} + +function map_clear(map) { + map.length = 0 +} + +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 + } + } +} + +// === FUZZING ASSERTS === + +const mutually_exclusive_lords = [ + [LORD_EXETER_1, LORD_EXETER_2], + [LORD_SOMERSET_1, LORD_SOMERSET_2], + [LORD_JASPER_TUDOR_1, LORD_JASPER_TUDOR_2], + [LORD_MARCH, LORD_EDWARD_IV], + [LORD_GLOUCESTER_1, LORD_GLOUCESTER_2, LORD_RICHARD_III], + [LORD_NORTHUMBERLAND_Y1, LORD_NORTHUMBERLAND_Y2, LORD_NORTHUMBERLAND_L], + [LORD_WARWICK_Y, LORD_WARWICK_L], + [LORD_YORK, LORD_EDWARD_IV, LORD_RICHARD_III], + [LORD_HENRY_VI, LORD_MARGARET], + [LORD_MARGARET, LORD_HENRY_TUDOR], + [LORD_HENRY_VI, LORD_HENRY_TUDOR], +] + +function assert_mutually_exclusive_lords() { + for (const lords of mutually_exclusive_lords) { + if (lords.filter(is_lord_in_play).length > 1) { + const lord_names = lords.map(l => lord_name[l]) + throw Error(`ASSERT: Mutually exclusive Lords [${lord_names}] can't be all in play.`) + } + } +} + +function assert_all_lords_have_troops_or_retinue() { + for (let lord = first_york_lord; lord <= last_lancaster_lord; ++lord) { + if (is_lord_on_map(lord) && !count_lord_all_forces(lord) && !get_lord_forces(lord, RETINUE)) + throw Error(`ASSERT: Lord "${lord_name[lord]}" without troops or retinue.`) + } +} + +function assert_all_lords_on_land() { + for (let lord = first_york_lord; lord <= last_lancaster_lord; ++lord) { + if (is_lord_at_sea(lord)) + throw Error(`ASSERT: Lord "${lord_name[lord]}" at sea during Levy phase.`) + } +} + +function assert_all_lords_without_routed_troops() { + for (let lord = first_york_lord; lord <= last_lancaster_lord; ++lord) { + if (lord_has_routed_troops(lord)) + throw Error(`ASSERT: Lord "${lord_name[lord]}" has routed troops during Levy phase.`) + } +} + +exports.assert_state = function (state) { + load_state(state) + + // assert_mutually_exclusive_lords() + if (game.state === "feed") + assert_all_lords_have_troops_or_retinue() + + if (is_levy_phase()) { + assert_all_lords_on_land() + assert_all_lords_without_routed_troops() + } +} + +let log_sanity = [] +exports.fuzz_log = function (fuzz_info) { + console.log(`${fuzz_info.state.state} - ${fuzz_info.actions} - - ${fuzz_info.args} [${fuzz_info.chosen_action}, ${fuzz_info.chosen_arg}]`) + + log_sanity.push(fuzz_info.state.state) + if (log_sanity.length > 200) { + log_sanity = log_sanity.slice(1) + + if (false) + if (log_sanity.every(l => l === fuzz_info.state.state)) { + console.log(`STATE`, fuzz_info.state) + console.log(`VIEW`, fuzz_info.view) + throw new Error("Too many times in the same state.") + } + } +} + +if (false) + exports.fuzz_crash = function (state, view) { + for (let x = 0; x < log_sanity.length; x++) { + console.log(log_sanity[x]) + } + } -- cgit v1.2.3