diff options
-rw-r--r-- | about.html | 17 | ||||
-rw-r--r-- | cover.1x.jpg | bin | 0 -> 29817 bytes | |||
-rw-r--r-- | cover.2x.jpg | bin | 0 -> 97957 bytes | |||
-rw-r--r-- | cover.jpg | bin | 10299 -> 1240937 bytes | |||
-rw-r--r-- | data.js | 429 | ||||
-rw-r--r-- | info/blocks.html | 60 | ||||
-rw-r--r-- | info/notes.html | 11 | ||||
-rw-r--r-- | play.html | 398 | ||||
-rw-r--r-- | rules.js | 1570 | ||||
-rw-r--r-- | thumbnail.jpg | bin | 0 -> 17191 bytes | |||
-rw-r--r-- | ui.js | 718 |
11 files changed, 3170 insertions, 33 deletions
@@ -1,8 +1,23 @@ <p> -Crusader Rex is a wargame of the 3rd Crusade. One player plays the Christian Franks, the other, the Muslim Saracens. The objective of the game is to control important Victory Cities such as Jerusalem, Acre, Damascus, and Antioch. +Crusader Rex is a wargame of the 3rd Crusade. One player plays the Christian +Franks, the other, the Muslim Saracens. The objective of the game is to control +important Victory Cities such as Jerusalem, Acre, Damascus, and Antioch. + +<br clear="left"> <p> Designer: Jerry Taylor and Tom Dalgliesh. <p> Publisher: <a href="https://columbiagames.com/cgi-bin/query/cfg/zoom.cfg?product_id=3151">Columbia Games</a>. + +<ul> +<li><a href="/crusader-rex/info/rules.html">Rulebook</a> +<li><a href="/crusader-rex/info/cards.html">Cards</a> +<li><a href="/crusader-rex/info/blocks.html">Blocks</a> +</ul> + +<p class="warning"> +This game is in Beta testing. Please report bugs, errors, and glitches on +<a href="https://github.com/ccxvii/rally-the-troops/issues">Github</a> or the +<a href="https://discord.gg/CBrTh8k84A">Discord</a> server. diff --git a/cover.1x.jpg b/cover.1x.jpg Binary files differnew file mode 100644 index 0000000..dbb23ef --- /dev/null +++ b/cover.1x.jpg diff --git a/cover.2x.jpg b/cover.2x.jpg Binary files differBinary files differnew file mode 100644 index 0000000..3c059b8 --- /dev/null +++ b/cover.2x.jpg @@ -0,0 +1,429 @@ +"use strict"; + +const VICTORY_TOWNS = [ + "Aleppo", "Damascus", "Egypt", + "Antioch", "Tripoli", "Acre", "Jerusalem" +]; + +const CARDS = { + 1: { name: "Assassins", event: "assassins", image: "card_assassins" }, + 2: { name: "Guide", event: "guide", image: "card_guide" }, + 3: { name: "Intrigue", event: "intrigue", image: "card_intrigue" }, + 4: { name: "Jihad", event: "jihad", image: "card_jihad" }, + 5: { name: "Manna", event: "manna", image: "card_manna" }, + 6: { name: "Winter Campaign", event: "winter_campaign", image: "card_winter_campaign" }, + 7: { name: "3", moves: 3, image: "card_3" }, + 8: { name: "3", moves: 3, image: "card_3" }, + 9: { name: "3", moves: 3, image: "card_3" }, + 10: { name: "3", moves: 3, image: "card_3" }, + 11: { name: "3", moves: 3, image: "card_3" }, + 12: { name: "3", moves: 3, image: "card_3" }, + 13: { name: "2", moves: 2, image: "card_2" }, + 14: { name: "2", moves: 2, image: "card_2" }, + 15: { name: "2", moves: 2, image: "card_2" }, + 16: { name: "2", moves: 2, image: "card_2" }, + 17: { name: "2", moves: 2, image: "card_2" }, + 18: { name: "2", moves: 2, image: "card_2" }, + 19: { name: "2", moves: 2, image: "card_2" }, + 20: { name: "2", moves: 2, image: "card_2" }, + 21: { name: "2", moves: 2, image: "card_2" }, + 22: { name: "1", moves: 1, image: "card_1" }, + 23: { name: "1", moves: 1, image: "card_1" }, + 24: { name: "1", moves: 1, image: "card_1" }, + 25: { name: "1", moves: 1, image: "card_1" }, + 26: { name: "1", moves: 1, image: "card_1" }, + 27: { name: "1", moves: 1, image: "card_1" }, +}; + +const BLOCKS = {}; +const ROADS = {}; + +// From edit.html output +const TOWNS = { + "Acre":{"x":452,"y":1566}, + "Ajlun":{"x":987,"y":1542}, + "Albara":{"x":810,"y":388}, + "Aleppo":{"x":1051,"y":108}, + "Amman":{"x":1088,"y":1838}, + "Anjar":{"x":753,"y":1129}, + "Antioch":{"x":471,"y":189}, + "Artah":{"x":865,"y":149}, + "Ascalon":{"x":367,"y":2081}, + "Ashtera":{"x":1038,"y":1419}, + "Baalbek":{"x":842,"y":1008}, + "Baisan":{"x":707,"y":1685}, + "Banyas":{"x":764,"y":1362}, + "Beaufort":{"x":605,"y":1354}, + "Beersheba":{"x":444,"y":2283}, + "Beirut":{"x":527,"y":1137}, + "Botron":{"x":540,"y":991}, + "Caesarea":{"x":402,"y":1754}, + "Damascus":{"x":1059,"y":1185}, + "Damiya":{"x":847,"y":1808}, + "Dimona":{"x":630,"y":2294}, + "Egypt":{"x":202,"y":2318}, + "Gaza":{"x":300,"y":2183}, + "Hama":{"x":1035,"y":477}, + "Harim":{"x":699,"y":124}, + "Hebron":{"x":680,"y":2109}, + "Homs":{"x":1053,"y":683}, + "Jaffa":{"x":399,"y":1923}, + "Jericho":{"x":836,"y":1931}, + "Jerusalem":{"x":680,"y":1980}, + "Kassab":{"x":426,"y":339}, + "Kerak":{"x":1008,"y":2076}, + "Krak":{"x":774,"y":726}, + "Lachish":{"x":495,"y":2148}, + "Lacum":{"x":919,"y":885}, + "Latakia":{"x":401,"y":445}, + "Legio":{"x":587,"y":1658}, + "Margat":{"x":540,"y":567}, + "Masyaf":{"x":758,"y":604}, + "Monterrand":{"x":920,"y":603}, + "Nablus":{"x":643,"y":1787}, + "Qaddas":{"x":1145,"y":916}, + "Ramallah":{"x":514,"y":1952}, + "Saone":{"x":653,"y":428}, + "Shughur":{"x":656,"y":296}, + "Sidon":{"x":493,"y":1276}, + "St. Simeon":{"x":364,"y":211}, + "Tartus":{"x":605,"y":718}, + "Tiberias":{"x":699,"y":1560}, + "Tripoli":{"x":621,"y":882}, + "Tyre":{"x":465,"y":1397}, + "Zerdana":{"x":1021,"y":300}, + "Zoar":{"x":955,"y":2278}, + /* + "Germania1":{"x":139,"y":273}, + "Germania2":{"x":139,"y":359}, + "Germania3":{"x":138,"y":447}, + "France1":{"x":140,"y":573}, + "France2":{"x":140,"y":660}, + "France3":{"x":139,"y":747}, + "England1":{"x":139,"y":873}, + "England2":{"x":140,"y":961}, + "England3":{"x":138,"y":1047}, + "F. Pool":{"x":120,"y":2150}, + "S. Pool":{"x":120,"y":2150}, + */ + "Germania":{"x":139,"y":273}, + "France":{"x":140,"y":573}, + "England":{"x":139,"y":873}, + "F. Pool":{"x":120,"y":2150}, + "S. Pool":{"x":120,"y":2150}, +}; + +const PORTS = []; + +(function () { + let r = 1, c = 1; + + let nomads = { Arabs: 1, Turks: 1, Kurds: 1 } + + function army(rc, owner, name, home, move, steps, combat, order) { + let id = name; + if (order == 'Military Orders' || order == 'Pilgrims' || order == 'Turcopoles') + id = home + " " + name; + if (order == 'Nomads') + id += " " + nomads[name]++; + if (name == 'Reynald' || name == 'Raymond') + id += " " + home; + if (id in BLOCKS) + throw Error("Name clash: " + id + " order:"+order + " " + JSON.stringify(nomads)); + BLOCKS[id] = { + owner: owner, + name: name, + type: order.toLowerCase().replace(/ /g, "_"), + home: home, + move: move, + steps: steps, + combat: combat, + image: rc, + } + } + + function frank(rc, name, home, move, steps, combat, order) { + army(rc, "Frank", name, home, move, steps, combat, order); + } + function saracen(rc, name, home, move, steps, combat, order) { + army(rc, "Saracen", name, home, move, steps, combat, order); + } + + frank(13, "Barbarossa", "Germania", 2, 4, "B3", "Crusaders"); + frank(23, "Frederik", "Germania", 2, 3, "B2", "Crusaders"); + frank(33, "Leopold", "Germania", 2, 3, "B3", "Crusaders"); + + frank(11, "Richard", "England", 3, 4, "B4", "Crusaders"); + frank(21, "Robert", "England", 2, 3, "B3", "Crusaders"); + frank(31, "Crossbows", "Aquitaine", 2, 3, "A2", "Crusaders"); + + frank(12, "Philippe", "France", 2, 4, "B3", "Crusaders"); + frank(22, "Hugues", "Bourgogne", 2, 4, "B2", "Crusaders"); + frank(32, "Fileps", "Flanders", 2, 3, "B3", "Crusaders"); + + frank(42, "Pilgrims", "Genoa", 2, 4, "C2", "Pilgrims"); + frank(43, "Pilgrims", "Sicily", 2, 3, "C2", "Pilgrims"); + frank(52, "Pilgrims", "Brittany", 2, 4, "C2", "Pilgrims"); + + frank(14, "Templars", "Jerusalem", 3, 3, "B3", "Military Orders"); + frank(15, "Templars", "Antioch", 3, 3, "B3", "Military Orders"); + frank(16, "Templars", "Gaza", 3, 3, "B3", "Military Orders"); + frank(17, "Templars", "Tartus", 3, 3, "B3", "Military Orders"); + frank(24, "Hospitallers", "Jerusalem", 3, 4, "B3", "Military Orders"); + frank(25, "Hospitallers", "Acre", 3, 3, "B3", "Military Orders"); + frank(26, "Hospitallers", "Krak", 3, 2, "B3", "Military Orders"); + + frank(27, "Reynald", "Sidon", 2, 3, "B2", "Outremers"); + frank(34, "Conrad", "Tyre", 2, 4, "B3", "Outremers"); + frank(35, "Balian", "Nablus", 2, 3, "B2", "Outremers"); + frank(36, "Walter", "Caesarea", 2, 3, "B2", "Outremers"); + frank(37, "Raymond", "Tiberias", 2, 3, "B2", "Outremers"); + frank(44, "King Guy", "Jerusalem", 2, 4, "B2", "Outremers"); + frank(45, "Reynald", "Kerak", 3, 2, "B3", "Outremers"); + frank(46, "Bohemond", "Antioch", 2, 4, "B2", "Outremers"); + frank(47, "Raymond", "Tripoli", 2, 4, "B2", "Outremers"); + frank(53, "Josselin", "Saone", 2, 3, "B2", "Outremers"); + + frank(41, "Turcopole", "Antioch", 3, 3, "A2", "Turcopoles"); + frank(51, "Turcopole", "Beirut", 3, 3, "A2", "Turcopoles"); + + army(54, "Assassins", "Assassins", "Masyaf", 0, 3, "A3", "Assassins"); + + saracen(55, "Qara-Qush", "Egypt", 3, 3, "B3", "Emirs"); + saracen(56, "Zangi", "Aleppo", 3, 3, "B2", "Emirs"); + saracen(57, "Sanjar", "Aleppo", 3, 3, "B2", "Emirs"); + + saracen(61, "Yazkuj", "Ashtera", 3, 2, "B2", "Emirs"); + saracen(62, "Sulaiman", "Artah", 3, 2, "B2", "Emirs"); + saracen(63, "Keukburi", "Damascus", 3, 3, "B3", "Emirs"); + saracen(64, "Shirkuh", "Homs", 3, 3, "B2", "Emirs"); + saracen(65, "Jurdik", "Zerdana", 3, 3, "B2", "Emirs"); + saracen(66, "Bahram", "Baalbek", 3, 3, "B2", "Emirs"); + saracen(67, "Tuman", "Homs", 3, 3, "B3", "Emirs"); + + saracen(71, "Taqi al Din", "Hama", 3, 4, "A2", "Emirs"); + saracen(72, "Al Mashtub", "Damascus", 3, 4, "B3", "Emirs"); + saracen(73, "Al Adil", "Egypt", 3, 4, "A2", "Emirs"); + saracen(74, "Saladin", "Damascus", 3, 4, "A3", "Emirs"); + saracen(75, "Al Aziz", "Egypt", 3, 3, "B2", "Emirs"); + saracen(76, "Al Afdal", "Damascus", 3, 3, "B3", "Emirs"); + saracen(77, "Al Zahir", "Aleppo", 3, 3, "A2", "Emirs"); + + saracen(81, "Yuzpah", "Egypt", 3, 4, "B2", "Emirs"); + saracen(82, "Qaimaz", "Banyas", 3, 3, "B2", "Emirs"); + + saracen(83, "Kurds", "Damascus", 3, 4, "C1", "Nomads"); + saracen(84, "Kurds", "Damascus", 3, 4, "C1", "Nomads"); + saracen(85, "Kurds", "Damascus", 3, 3, "C2", "Nomads"); + saracen(86, "Kurds", "Damascus", 3, 3, "C2", "Nomads"); + + saracen(91, "Turks", "Aleppo", 3, 3, "A2", "Nomads"); + saracen(92, "Turks", "Aleppo", 3, 3, "A2", "Nomads"); + saracen(93, "Turks", "Aleppo", 3, 4, "A1", "Nomads"); + saracen(94, "Turks", "Aleppo", 3, 4, "A1", "Nomads"); + + saracen(95, "Arabs", "Egypt", 3, 3, "B2", "Nomads"); + saracen(96, "Arabs", "Egypt", 3, 3, "B2", "Nomads"); + saracen(97, "Arabs", "Egypt", 3, 4, "B1", "Nomads"); + saracen(87, "Arabs", "Egypt", 3, 4, "B1", "Nomads"); + + function town(axis, major, minor, wrap, region, name, rating, type) { + TOWNS[name].region = region; + TOWNS[name].rating = rating; + if (type == 'port' || type == 'fortified-port') + TOWNS[name].port = true; + if (type == 'fortified-port') + TOWNS[name].fortified_port = true; + if (TOWNS[name].port) + PORTS.push(name); + TOWNS[name].exits = []; + TOWNS[name].layout_axis = axis; + TOWNS[name].layout_major = (1 - major) / 2; + TOWNS[name].layout_minor = (1 - minor) / 2; + TOWNS[name].wrap = wrap ? wrap : Math.max(2, rating); + } + +/* + town('X', 0, 0, 0, "Staging", "England1", 0, "staging"); + town('X', 0, 0, 0, "Staging", "England2", 0, "staging"); + town('X', 0, 0, 0, "Staging", "England3", 0, "staging"); + town('X', 0, 0, 0, "Staging", "France1", 0, "staging"); + town('X', 0, 0, 0, "Staging", "France2", 0, "staging"); + town('X', 0, 0, 0, "Staging", "France3", 0, "staging"); + town('X', 0, 0, 0, "Staging", "Germania1", 0, "staging"); + town('X', 0, 0, 0, "Staging", "Germania2", 0, "staging"); + town('X', 0, 0, 0, "Staging", "Germania3", 0, "staging"); +*/ + town('X', 0, 0, 0, "Staging", "England", 0, "staging", "S", 3); + town('X', 0, 0, 0, "Staging", "France", 0, "staging", "S", 3); + town('X', 0, 0, 0, "Staging", "Germania", 0, "staging", "S", 3); + + town('X', 0, 0, 0, "Pool", "F. Pool", 0, "pool", "N", 12); + town('X', 0, 0, 0, "Pool", "S. Pool", 0, "pool", "N", 12); + + town('X', 1, 0, 0, "Syria", "Aleppo", 3, "town", "E"); + town('Y', 0, 0, 0, "Syria", "Artah", 1, "town", "V"); + town('X', 1, 0, 0, "Syria", "Zerdana", 1, "town", "E"); + town('X', 1, 0, 0, "Syria", "Hama", 1, "town", "E"); + town('X', 1, 0, 0, "Syria", "Homs", 2, "town", "E"); + town('X', 0, 0, 0, "Syria", "Lacum", 0, "town", "H"); + town('X', 0, 0, 0, "Syria", "Qaddas", 0, "town", "H"); + town('X', 0, 0, 0, "Syria", "Baalbek", 1, "town", "H"); + town('X', 0, 0, 0, "Syria", "Anjar", 0, "town", "H"); + town('X', 0, 0, 0, "Syria", "Damascus", 4, "town", "H"); + town('X', 1, 0, 0, "Syria", "Banyas", 1, "town", "E"); + town('X', 1, 0, 0, "Syria", "Ashtera", 1, "town", "E"); + town('X', 1, 0, 0, "Syria", "Ajlun", 0, "town", "E"); + + town('X', 0, 0, 0, "Antioch", "St. Simeon", 0, "port", "W"); + town('Y', 0, 0, 0, "Antioch", "Antioch", 3, "town", "V"); + town('Y', 1, 0, 0, "Antioch", "Harim", 0, "town", "S"); + town('X', 0, 0, 0, "Antioch", "Kassab", 0, "town", "H"); + town('X', 0, 0, 0, "Antioch", "Shughur", 0, "town", "H"); + town('X', -1, 0, 0, "Antioch", "Latakia", 1, "port", "W"); + town('X', 0, 0, 0, "Antioch", "Saone", 1, "town", "H"); + town('Y', 0, 0, 0, "Antioch", "Albara", 0, "town", "V"); + town('X', -1, 0, 0, "Antioch", "Margat", 1, "port", "W"); + + town('X', 0, 0, 0, "Masyaf", "Masyaf", 1, "town", "H"); + + town('Y', 0, 0, 0, "Tripoli", "Monterrand", 0, "town", "V"); + town('X', -1, 0, 0, "Tripoli", "Tartus", 1, "port", "W"); + town('X', 1, 0, 0, "Tripoli", "Krak", 1, "town", "E"); + town('X', -1, 0, 0, "Tripoli", "Tripoli", 2, "fortified-port", "W"); + town('X', -1, 0, 0, "Tripoli", "Botron", 0, "town", "W"); + + town('X', -1, 0, 0, "Jerusalem", "Beirut", 2, "port", "W"); + town('X', -1, 0, 0, "Jerusalem", "Sidon", 1, "port", "W"); + town('X', -1, 0, 0, "Jerusalem", "Tyre", 2, "fortified-port", "W"); + town('Y', 0, 0, 0, "Jerusalem", "Beaufort", 1, "town", "V"); + town('X', -1, 0, 0, "Jerusalem", "Acre", 3, "port", "W"); + town('X', 1, 0, 0, "Jerusalem", "Tiberias", 2, "town", "E"); + town('Y', 1, 0, 0, "Jerusalem", "Legio", 0, "town", "N"); + town('X', 1, 0, 0, "Jerusalem", "Baisan", 1, "town", "E"); + town('X', -1, 0, 0, "Jerusalem", "Caesarea", 1, "port", "W"); + town('X', 0, 0, 0, "Jerusalem", "Nablus", 1, "town", "H"); + town('X', 0, 0, 0, "Jerusalem", "Damiya", 0, "town", "H"); + town('X', 0, 0, 0, "Jerusalem", "Amman", 1, "town", "H"); + town('X', -1, 0, 0, "Jerusalem", "Jaffa", 1, "port", "W"); + town('Y', 0, 0, 0, "Jerusalem", "Ramallah", 0, "town", "V"); + town('X', 0, 0, 0, "Jerusalem", "Jerusalem", 3, "town", "H"); + town('Y', 0, 0, 0, "Jerusalem", "Jericho", 0, "town", "V"); + town('X', -1, 0, 0, "Jerusalem", "Ascalon", 2, "port", "W"); + town('Y', 0, 0, 0, "Jerusalem", "Lachish", 0, "town", "V"); + town('X', 0, 0, 0, "Jerusalem", "Hebron", 1, "town", "H"); + town('X', 1, 0, 0, "Jerusalem", "Kerak", 1, "town", "E"); + town('X', 0, 0, 0, "Jerusalem", "Gaza", 1, "town", "H"); + town('Y', 0, 0, 0, "Jerusalem", "Beersheba", 0, "town", "V"); + town('X', 0, 0, 0, "Jerusalem", "Dimona", 0, "town", "H"); + town('X', 1, 0, 0, "Jerusalem", "Zoar", 0, "town", "E"); + + town('X', 0, 0, 0, "Egypt", "Egypt", 4, "port", "H"); + + function road(A,B,type) { + let id = (A < B) ? (A + "/" + B) : (B + "/" + A); + ROADS[id] = type; + TOWNS[A].exits.push(B); + TOWNS[B].exits.push(A); + } + + function major(A,B) { road(A,B,"major"); } + function minor(A,B) { road(A,B,"minor"); } + + major("Antioch", "Harim"); + major("Harim", "Artah"); + major("Artah", "Aleppo"); + major("Aleppo", "Zerdana"); + major("Zerdana", "Hama"); + major("Hama", "Albara"); + major("Hama", "Monterrand"); + major("Hama", "Homs"); + major("Albara", "Shughur"); + major("Shughur", "Harim"); + major("Monterrand", "Krak"); + major("Krak", "Homs"); + major("Krak", "Tripoli"); + major("Tripoli", "Tartus"); + major("Tripoli", "Botron"); + major("Tartus", "Margat"); + major("Margat", "Latakia"); + major("Botron", "Beirut"); + major("Beirut", "Sidon"); + major("Sidon", "Tyre"); + major("Tyre", "Beaufort"); + major("Beaufort", "Banyas"); + major("Banyas", "Damascus"); + major("Damascus", "Qaddas"); + major("Qaddas", "Homs"); + major("Homs", "Lacum"); + major("Lacum", "Baalbek"); + major("Baalbek", "Anjar"); + major("Anjar", "Beaufort"); + major("Damascus", "Ashtera"); + major("Ashtera", "Ajlun"); + major("Ajlun", "Amman"); + major("Amman", "Kerak"); + major("Kerak", "Zoar"); + major("Zoar", "Hebron"); + major("Hebron", "Jerusalem"); + major("Jerusalem", "Ramallah"); + major("Ramallah", "Jaffa"); + major("Jaffa", "Ascalon"); + major("Ascalon", "Gaza"); + major("Gaza", "Egypt"); + major("Ajlun", "Tiberias"); + major("Tiberias", "Acre"); + major("Acre", "Legio"); + major("Legio", "Baisan"); + major("Baisan", "Tiberias"); + major("Baisan", "Nablus"); + major("Nablus", "Legio"); + major("Nablus", "Jerusalem"); + major("Acre", "Caesarea"); + major("Caesarea", "Jaffa"); + + minor("St. Simeon", "Antioch"); + minor("Antioch", "Kassab"); + minor("Kassab", "Latakia"); + minor("Latakia", "Saone"); + minor("Saone", "Shughur"); + minor("Saone", "Albara"); + minor("Albara", "Zerdana"); + minor("Zerdana", "Artah"); + + minor("Monterrand", "Homs"); + + minor("Tartus", "Krak"); + minor("Krak", "Lacum"); + minor("Lacum", "Qaddas"); + minor("Tripoli", "Baalbek"); + minor("Beirut", "Anjar"); + minor("Anjar", "Damascus"); + minor("Sidon", "Beaufort"); + minor("Tiberias", "Banyas"); + minor("Banyas", "Ashtera"); + minor("Tyre", "Acre"); + minor("Caesarea", "Nablus"); + minor("Nablus", "Damiya"); + minor("Damiya", "Baisan"); + minor("Damiya", "Amman"); + minor("Amman", "Jericho"); + minor("Jericho", "Damiya"); + minor("Jericho", "Kerak"); + minor("Jericho", "Jerusalem"); + + minor("Ramallah", "Ascalon"); + minor("Ascalon", "Lachish"); + minor("Lachish", "Gaza"); + minor("Gaza", "Beersheba"); + minor("Beersheba", "Egypt"); + minor("Beersheba", "Dimona"); + minor("Dimona", "Zoar"); + minor("Dimona", "Hebron"); + minor("Hebron", "Lachish"); + + // TODO: seats and alternate seats +})(); + +if (typeof module != 'undefined') + module.exports = { CARDS, BLOCKS, TOWNS, PORTS, ROADS } diff --git a/info/blocks.html b/info/blocks.html index e3b75e4..178a8f8 100644 --- a/info/blocks.html +++ b/info/blocks.html @@ -30,84 +30,88 @@ div.block:hover { transform: scale(2); } <h1> Crusader Rex - Blocks </h1> - -<h2>Frank</h2> <div class="list"> <div class="frank block block_11"></div> +<div class="frank block block_21"></div> +<div class="frank block block_31"></div> <div class="frank block block_12"></div> +<div class="frank block block_22"></div> +<div class="frank block block_32"></div> <div class="frank block block_13"></div> +<div class="frank block block_23"></div> +<div class="frank block block_33"></div> +</div> +<div class="list"> <div class="frank block block_14"></div> <div class="frank block block_15"></div> <div class="frank block block_16"></div> <div class="frank block block_17"></div> -</div> -<div class="list"> -<div class="frank block block_21"></div> -<div class="frank block block_22"></div> -<div class="frank block block_23"></div> <div class="frank block block_24"></div> <div class="frank block block_25"></div> <div class="frank block block_26"></div> -<div class="frank block block_27"></div> </div> <div class="list"> -<div class="frank block block_31"></div> -<div class="frank block block_32"></div> -<div class="frank block block_33"></div> +<div class="frank block block_27"></div> <div class="frank block block_34"></div> <div class="frank block block_35"></div> <div class="frank block block_36"></div> <div class="frank block block_37"></div> +<div class="frank block block_44"></div> +<div class="frank block block_45"></div> +<div class="frank block block_46"></div> +<div class="frank block block_47"></div> +<div class="frank block block_53"></div> </div> <div class="list"> <div class="frank block block_41"></div> +<div class="frank block block_51"></div> <div class="frank block block_42"></div> <div class="frank block block_43"></div> -<div class="frank block block_44"></div> -<div class="frank block block_45"></div> -<div class="frank block block_46"></div> -<div class="frank block block_47"></div> +<div class="frank block block_52"></div> +</div> +<div class="list"> +<div class="assassins block block_54"></div> </div> - -<h2>Saracen</h2> <div class="list"> +<div class="saracen block block_55"></div> +<div class="saracen block block_56"></div> +<div class="saracen block block_57"></div> <div class="saracen block block_61"></div> <div class="saracen block block_62"></div> <div class="saracen block block_63"></div> <div class="saracen block block_64"></div> +</div> +<div class="list"> <div class="saracen block block_65"></div> <div class="saracen block block_66"></div> <div class="saracen block block_67"></div> -</div> -<div class="list"> <div class="saracen block block_71"></div> <div class="saracen block block_72"></div> -<div class="saracen block block_73"></div> +<div class="saracen block block_81"></div> +<div class="saracen block block_82"></div> +</div> +<div class="list"> <div class="saracen block block_74"></div> +<div class="saracen block block_73"></div> <div class="saracen block block_75"></div> <div class="saracen block block_76"></div> <div class="saracen block block_77"></div> </div> <div class="list"> -<div class="saracen block block_81"></div> -<div class="saracen block block_82"></div> <div class="saracen block block_83"></div> <div class="saracen block block_84"></div> <div class="saracen block block_85"></div> <div class="saracen block block_86"></div> -<div class="saracen block block_87"></div> </div> <div class="list"> <div class="saracen block block_91"></div> <div class="saracen block block_92"></div> <div class="saracen block block_93"></div> <div class="saracen block block_94"></div> +</div> +<div class="list"> <div class="saracen block block_95"></div> <div class="saracen block block_96"></div> <div class="saracen block block_97"></div> -</div> - -<h2>Assassins</h2> -<div class="list"> -<div class="assassins block block_54"></div> +<div class="saracen block block_87"></div> </div> diff --git a/info/notes.html b/info/notes.html index 4a4e5f6..c1361cf 100644 --- a/info/notes.html +++ b/info/notes.html @@ -14,11 +14,14 @@ Crusader Rex: Implementation Notes</h1> <hr> <p> -How do I designate the attacking main force? +How do I designate the Main Attack? <p> -The first block that moves into a contested area defines the main force. Any other blocks -who enter the battle from the same road also count as being part of the main force. -The attacking main force move is marked with an asterisk in the game log. +The first block that moves into an enemy area defines the main attack road. +Any other blocks who enter the battle from the same road also count as being part of the main attack. +<p> +Likewise, the first block that moves to reinforce a battle started by player one defines the main reinforcement. +<p> +The main attack, main reinforcement, and secondary reinforcement moves are noted in the game log with *, †, and ‡ respectively. <hr> diff --git a/play.html b/play.html new file mode 100644 index 0000000..dfc3c5a --- /dev/null +++ b/play.html @@ -0,0 +1,398 @@ +<!DOCTYPE html> +<html> +<head> +<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1"> +<meta charset="UTF-8"> +<title>CRUSADER REX</title> +<link rel="icon" href="Cross_of_the_Knights_Templar.png"> +<link rel="stylesheet" href="/fonts/fonts.css"> +<link rel="stylesheet" href="/common/grid.css"> +<link rel="stylesheet" href="/common/battle_abc.css"> +<script defer src="/socket.io/socket.io.min.js"></script> +<script defer src="/common/client.js"></script> +<script defer src="data.js"></script> +<script defer src="ui.js"></script> +<style> + +.grid_center { background-color: slategray; } +.grid_role { background-color: gainsboro; } +.grid_log { background-color: ghostwhite; } +.grid_top { background-color: gainsboro; } +.grid_top.your_turn { background-color: orange; } +.grid_top.Frank.your_turn { background-color: khaki; } +.grid_top.Saracen.your_turn { background-color: lightgreen; } +.grid_top.disconnected { background-color: red; } +.role_info { background-color: silver; } +.one .role_name { background-color: khaki; } +.two .role_name { background-color: lightgreen; } + +#turn { + padding: 8px 0px 8px 8px; + border-bottom: 1px solid black; + white-space: pre-wrap; + font-style: italic; + font-size: 12px; + line-height: 18px; + font-family: "Source Serif SmText"; +} + +.card { + width: 225px; + height: 350px; + border-radius: 15px; +} + +.card.enabled:hover { + transform: scale(1.1); +} + +.small_card { + width: 90px; + height: 140px; + border-radius: 6px; +} + +.role_info { + display: flex; + justify-content: center; +} + +/* MAP */ + +.mapwrap { + box-shadow: 0px 1px 10px rgba(0,0,0,0.5); + width: 1275px; + height: 2475px; +} + +.map { + position: relative; + background-repeat: no-repeat; +} + +.map.tall { + width: 1275px; + height: 2475px; + background-image: url("map-v.jpg"); +} + +.map.wide { + width: 2475px; + height: 1275px; + background-image: url("map-h.jpg"); +} + +.map.crop.tall { + width: 1215px; + height: 2378px; + background-position: -30px -30px; +} + +.map.crop.wide { + width: 2378px; + height: 1215px; + background-position: -30px -30px; +} + +.map.hide_blocks div.blocks { + visibility: hidden; +} + +.town { + position: absolute; + opacity: 0.0; + z-index: 1; + border: 5px solid white; + width: 60px; + height: 60px; + border-radius: 50%; +} +.town.highlight { + cursor: pointer; + opacity: 0.8; + z-index: 9; +} + +/* BLOCKS */ + +body.shift .block.known:hover { + transform: scale(2) !important; + transition: 100ms; + z-index: 100; +} + +.battle .block { position: relative; } +.map .block { position: absolute; z-index: 2; } +.map .block.highlight { z-index: 3; } +.map .block.selected { z-index: 4; } +.map .block:hover { z-index: 5; } + +.block.highlight { cursor: pointer; box-shadow: 0px 0px 4px 1px white; } + +.block { + background-size: cover; + background-repeat: no-repeat; + border-radius: 6px; + width: 60px; + height: 60px; + box-shadow: 0px 0px 4px 0px black; +} + +.block.Frank { border: 3px solid orange; background-color: orange; } +.block.Saracen { border: 3px solid green; background-color: green; } +.block.Assassins { border: 3px solid purple; background-color: purple; } + +.block.Frank.highlight { border-color: white; } +.block.Saracen.highlight { border-color: white; } +.block.Assassins.highlight { border-color: white; } + +.block.Frank.selected { border-color: yellow; } +.block.Saracen.selected { border-color: yellow; } +.block.Assassins.selected { border-color: yellow; } + +.block.selected { box-shadow: 0 0 4px 1px yellow; } +.block.moved { filter: brightness(85%) grayscale(50%); } +.block.highlight.moved { filter: brightness(95%) grayscale(60%); } + +.block.r1 { transform: rotate(-90deg); } +.block.r2 { transform: rotate(-180deg); } +.block.r3 { transform: rotate(-270deg); } + +.block { + transition-property: top, left, transform; + transition-duration: 700ms, 700ms, 200ms; + transition-timing-function: ease; +} + +/* BATTLE BOARD */ + +.battle { background-color: darkgray; } +.battle .battle_message { background-color: gainsboro; } +.battle .battle_header { background-color: #224467; color: white; font-weight: bold; } +.battle .battle_separator { background-color: #224467; } + +.battle_line.enemy .battle_menu_list { min-height: 0; } +.battle_reserves > td > div { height: 75px; padding: 5px; } +.battle_a_cell > div { min-width: 270px; padding: 5px 5px; } +.battle_b_cell > div { min-width: 270px; padding: 5px 5px; } +.battle_c_cell > div { min-width: 270px; padding: 5px 5px; } + +/* CARD AND BLOCK IMAGES */ + +.card_back{background-image:url('cards/card_back.jpg')} +.card_assassins{background-image:url('cards/card_assassins.jpg')} +.card_guide{background-image:url('cards/card_guide.jpg')} +.card_intrigue{background-image:url('cards/card_intrigue.jpg')} +.card_jihad{background-image:url('cards/card_jihad.jpg')} +.card_manna{background-image:url('cards/card_manna.jpg')} +.card_winter_campaign{background-image:url('cards/card_winter_campaign.jpg')} +.card_1{background-image:url('cards/card_1.jpg')} +.card_2{background-image:url('cards/card_2.jpg')} +.card_3{background-image:url('cards/card_3.jpg')} + +.known.block_11{background-image:url(blocks/block_11.png)} +.known.block_12{background-image:url(blocks/block_12.png)} +.known.block_13{background-image:url(blocks/block_13.png)} +.known.block_14{background-image:url(blocks/block_14.png)} +.known.block_15{background-image:url(blocks/block_15.png)} +.known.block_16{background-image:url(blocks/block_16.png)} +.known.block_17{background-image:url(blocks/block_17.png)} +.known.block_21{background-image:url(blocks/block_21.png)} +.known.block_22{background-image:url(blocks/block_22.png)} +.known.block_23{background-image:url(blocks/block_23.png)} +.known.block_24{background-image:url(blocks/block_24.png)} +.known.block_25{background-image:url(blocks/block_25.png)} +.known.block_26{background-image:url(blocks/block_26.png)} +.known.block_27{background-image:url(blocks/block_27.png)} +.known.block_31{background-image:url(blocks/block_31.png)} +.known.block_32{background-image:url(blocks/block_32.png)} +.known.block_33{background-image:url(blocks/block_33.png)} +.known.block_34{background-image:url(blocks/block_34.png)} +.known.block_35{background-image:url(blocks/block_35.png)} +.known.block_36{background-image:url(blocks/block_36.png)} +.known.block_37{background-image:url(blocks/block_37.png)} +.known.block_41{background-image:url(blocks/block_41.png)} +.known.block_42{background-image:url(blocks/block_42.png)} +.known.block_43{background-image:url(blocks/block_43.png)} +.known.block_44{background-image:url(blocks/block_44.png)} +.known.block_45{background-image:url(blocks/block_45.png)} +.known.block_46{background-image:url(blocks/block_46.png)} +.known.block_47{background-image:url(blocks/block_47.png)} +.known.block_51{background-image:url(blocks/block_51.png)} +.known.block_52{background-image:url(blocks/block_52.png)} +.known.block_53{background-image:url(blocks/block_53.png)} +.known.block_54{background-image:url(blocks/block_54.png)} +.known.block_55{background-image:url(blocks/block_55.png)} +.known.block_56{background-image:url(blocks/block_56.png)} +.known.block_57{background-image:url(blocks/block_57.png)} +.known.block_61{background-image:url(blocks/block_61.png)} +.known.block_62{background-image:url(blocks/block_62.png)} +.known.block_63{background-image:url(blocks/block_63.png)} +.known.block_64{background-image:url(blocks/block_64.png)} +.known.block_65{background-image:url(blocks/block_65.png)} +.known.block_66{background-image:url(blocks/block_66.png)} +.known.block_67{background-image:url(blocks/block_67.png)} +.known.block_71{background-image:url(blocks/block_71.png)} +.known.block_72{background-image:url(blocks/block_72.png)} +.known.block_73{background-image:url(blocks/block_73.png)} +.known.block_74{background-image:url(blocks/block_74.png)} +.known.block_75{background-image:url(blocks/block_75.png)} +.known.block_76{background-image:url(blocks/block_76.png)} +.known.block_77{background-image:url(blocks/block_77.png)} +.known.block_81{background-image:url(blocks/block_81.png)} +.known.block_82{background-image:url(blocks/block_82.png)} +.known.block_83{background-image:url(blocks/block_83.png)} +.known.block_84{background-image:url(blocks/block_84.png)} +.known.block_85{background-image:url(blocks/block_85.png)} +.known.block_86{background-image:url(blocks/block_86.png)} +.known.block_87{background-image:url(blocks/block_87.png)} +.known.block_91{background-image:url(blocks/block_91.png)} +.known.block_92{background-image:url(blocks/block_92.png)} +.known.block_93{background-image:url(blocks/block_93.png)} +.known.block_94{background-image:url(blocks/block_94.png)} +.known.block_95{background-image:url(blocks/block_95.png)} +.known.block_96{background-image:url(blocks/block_96.png)} +.known.block_97{background-image:url(blocks/block_97.png)} + +</style> +</head> +<body> + +<div class="status" id="status"></div> + +<div class="chat_window"> +<div class="chat_header">Chat</div> +<div class="chat_text"></div> +<form class="chat_form" action=""><input id="chat_input" autocomplete="off"></form> +</div> + +<table class="battle"> +<tr> +<th class="battle_header" colspan=4></th> +<tr class="battle_reserves enemy"> +<td colspan=4><div id="ER"></div></td> +<tr class="battle_line enemy"> +<td class="battle_a_cell"><div id="EA"></div></td> +<td class="battle_b_cell"><div id="EB"></div></td> +<td class="battle_c_cell"><div id="EC"></div></td> +<tr class="battle_separator"> +<td colspan=4> +<tr class="battle_line friendly"> +<td class="battle_a_cell"><div id="FA"></div></td> +<td class="battle_b_cell"><div id="FB"></div></td> +<td class="battle_c_cell"><div id="FC"></div></td> +<tr class="battle_reserves friendly"> +<td colspan=4><div id="FR"></div></td> +<tr> +<th class="battle_message" colspan=4></th> +</table> + +<div class="grid_window"> + + <div class="grid_top"> + + <div class="menu"> + <div class="menu_title"><img src="/images/cog.svg"></div> + <div class="menu_popup"> + <div class="menu_item" onclick="toggle_fullscreen()">Fullscreen</div> + <div class="menu_item" onclick="wide_map()">Wide Map</div> + <div class="menu_item" onclick="tall_map()">Tall Map</div> + <div class="menu_separator"></div> + <div class="menu_item" onclick="window.open('info/notes.html', '_blank')">Notes</div> + <div class="menu_item" onclick="window.open('info/rules.html', '_blank')">Rules</div> + <div class="menu_item" onclick="window.open('info/cards.html', '_blank')">Cards</div> + <div class="menu_item" onclick="window.open('info/blocks.html', '_blank')">Blocks</div> + <div class="menu_separator"></div> + <div class="menu_item" onclick="send_save()">🐞 Save</div> + <div class="menu_item" onclick="send_restore()">🐞 Restore</div> + <div class="menu_separator"></div> + <div class="menu_item" onclick="send_restart('Historical')">⚠ Restart</div> + </div> + </div> + + <div class="image_button" onclick="toggle_blocks()"><img src="/images/earth-africa-europe.svg"></div> + <div class="image_button" onclick="toggle_zoom()"><img src="/images/magnifying-glass.svg"></div> + <div class="image_button" onclick="toggle_log()"><img src="/images/scroll-quill.svg"></div> + <div class="image_button chat_button" onclick="toggle_chat()"><img src="/images/chat-bubble.svg"></div> + + <div id="prompt" class="prompt">$PROMPT</div> + + <button id="eliminate_button" class="hide" onclick="on_button_eliminate()">Eliminate</button> + <button id="sea_move_button" class="hide" onclick="on_button_sea_move()">Sea Move</button> + <button id="group_move_button" class="hide" onclick="on_button_group_move()">Group Move</button> + <button id="muster_button" class="hide" onclick="on_button_muster()">Muster</button> + <button id="end_muster_button" class="hide" onclick="on_button_end_muster()">End muster</button> + <button id="end_retreat_button" class="hide" onclick="on_button_end_retreat()">End retreat</button> + <button id="end_regroup_button" class="hide" onclick="on_button_end_regroup()">End regroup</button> + <button id="end_move_phase_button" class="hide" onclick="on_button_end_move_phase()">End move phase</button> + <button id="pass_button" class="hide" onclick="on_button_pass()">Pass</button> + <button id="undo_button" class="hide" onclick="on_button_undo()">Undo</button> + + </div> + + <div class="grid_role"> + + <div class="role one"> + <div class="role_vp" id="frank_vp"></div> + <div class="role_name">Frank (<span class="role_user">$USER</span>)</div> + <div class="role_info"><div class="small_card card_back" id="frank_card"></div></div> + </div> + + <div class="role two"> + <div class="role_vp" id="saracen_vp"></div> + <div class="role_name">Saracen (<span class="role_user">$USER</span>)</div> + <div class="role_info"><div class="small_card card_back" id="saracen_card"></div></div> + </div> + + <div id="turn">$TURN</div> + + </div> + + <div class="grid_log"> + <div class="log" id="log"></div> + </div> + + <div class="grid_center"> + +<div class="mapwrap" id="mapwrap"> +<div class="map" id="map"> +<div id="blocks" class="blocks"></div> +<div id="offmap" class="offmap" style="visibility:hidden"></div> +<div id="towns" class="towns"></div> +</div> +</div> + +<div class="hand"> +<div id="card+1" class="card card_assassins"></div> +<div id="card+2" class="card card_guide"></div> +<div id="card+3" class="card card_intrigue"></div> +<div id="card+4" class="card card_jihad"></div> +<div id="card+5" class="card card_manna"></div> +<div id="card+6" class="card card_winter_campaign"></div> +<div id="card+7" class="card card_3"></div> +<div id="card+8" class="card card_3"></div> +<div id="card+9" class="card card_3"></div> +<div id="card+10" class="card card_3"></div> +<div id="card+11" class="card card_3"></div> +<div id="card+12" class="card card_3"></div> +<div id="card+13" class="card card_2"></div> +<div id="card+14" class="card card_2"></div> +<div id="card+15" class="card card_2"></div> +<div id="card+16" class="card card_2"></div> +<div id="card+17" class="card card_2"></div> +<div id="card+18" class="card card_2"></div> +<div id="card+19" class="card card_2"></div> +<div id="card+20" class="card card_2"></div> +<div id="card+21" class="card card_2"></div> +<div id="card+22" class="card card_1"></div> +<div id="card+23" class="card card_1"></div> +<div id="card+24" class="card card_1"></div> +<div id="card+25" class="card card_1"></div> +<div id="card+26" class="card card_1"></div> +<div id="card+27" class="card card_1"></div> +</div> + + </div> + +</div> +</body> diff --git a/rules.js b/rules.js new file mode 100644 index 0000000..3682c88 --- /dev/null +++ b/rules.js @@ -0,0 +1,1570 @@ +"use strict"; + +// TODO: frank seat adjustment at setup +// TODO: saladin seat adjustment at setup + +exports.scenarios = [ + "Rules 2.0", +]; + +const { CARDS, BLOCKS, TOWNS, PORTS, ROADS } = require('./data'); + +const FRANK = "Frank"; +const SARACEN = "Saracen"; +const ASSASSINS = "Assassins"; +const ENEMY = { Frank: "Saracen", Saracen: "Frank" }; +const OBSERVER = "Observer"; +const BOTH = "Both"; +const F_POOL = "F. Pool"; +const S_POOL = "S. Pool"; + +const HIT_TEXT = "\u2605"; +const MISS_TEXT = "\u2606"; + +const ATTACK_MARK = "*"; +const RESERVE_MARK_1 = "\u2020"; +const RESERVE_MARK_2 = "\u2021"; +const NO_MARK = ""; + +let states = {}; + +let game = null; + +function log(...args) { + let s = Array.from(args).join(""); + game.log.push(s); +} + +function logp(...args) { + let s = game.active + " " + Array.from(args).join(""); + game.log.push(s); +} + +function log_move_start(from) { + game.move_buf = [ from ]; +} + +function log_move_continue(to, mark) { + if (mark) + game.move_buf.push(to + mark); + else + game.move_buf.push(to); +} + +function log_move_end() { + if (game.move_buf && game.move_buf.length > 1) + game.turn_log.push(game.move_buf); + delete game.move_buf; +} + +function print_turn_log_no_count(text) { + function print_move(last) { + return "\n" + last.join(" \u2192 "); + } + if (game.turn_log.length > 0) { + game.turn_log.sort(); + for (let entry of game.turn_log) + text += print_move(entry); + } else { + text += "\nnothing."; + } + log(text); + delete game.turn_log; +} + +function print_turn_log(text) { + function print_move(last) { + return "\n" + n + " " + last.join(" \u2192 "); + } + game.turn_log.sort(); + let last = game.turn_log[0]; + let n = 0; + for (let entry of game.turn_log) { + if (entry.toString() != last.toString()) { + text += print_move(last); + n = 0; + } + ++n; + last = entry; + } + if (n > 0) + text += print_move(last); + else + text += "\nnothing."; + log(text); + delete game.turn_log; +} + +function is_active_player(current) { + return (current == game.active) || (game.active == BOTH && current != OBSERVER); +} + +function is_inactive_player(current) { + return current == OBSERVER || (game.active != current && game.active != BOTH); +} + +function remove_from_array(array, item) { + let i = array.indexOf(item); + if (i >= 0) + array.splice(i, 1); +} + +function clear_undo() { + game.undo = []; +} + +function push_undo() { + game.undo.push(JSON.stringify(game, (k,v) => { + if (k === 'undo') return undefined; + if (k === 'log') return v.length; + return v; + })); +} + +function pop_undo() { + let undo = game.undo; + let log = game.log; + Object.assign(game, JSON.parse(undo.pop())); + game.undo = undo; + log.length = game.log; + game.log = log; +} + +function gen_action_undo(view) { + if (!view.actions) + view.actions = {} + if (game.undo && game.undo.length > 0) + view.actions.undo = 1; + else + view.actions.undo = 0; +} + +function gen_action(view, action, argument) { + if (!view.actions) + view.actions = {} + if (argument != undefined) { + if (!(action in view.actions)) { + view.actions[action] = [ argument ]; + } else { + if (!view.actions[action].includes(argument)) + view.actions[action].push(argument); + } + } else { + view.actions[action] = true; + } +} + +function roll_d6() { + return Math.floor(Math.random() * 6) + 1; +} + +function shuffle_deck() { + let deck = []; + for (let c = 1; c <= 27; ++c) + deck.push(c); + return deck; +} + +function deal_cards(deck, n) { + let hand = []; + for (let i = 0; i < n; ++i) { + let k = Math.floor(Math.random() * deck.length); + hand.push(deck[k]); + deck.splice(k, 1); + } + return hand; +} + +function block_name(who) { + return BLOCKS[who].name; +} + +function block_type(who) { + return BLOCKS[who].type; +} + +function block_home(who) { + let home = BLOCKS[who].home; + if (home == "Aquitaine") return "England"; + if (home == "Bourgogne") return "France"; + if (home == "Flanders") return "France"; + return home; +} + +function block_pool(who) { + if (BLOCKS[who].owner == FRANK) + return F_POOL; + return S_POOL; +} + +function block_owner(who) { + return BLOCKS[who].owner; +} + +function block_initiative(who) { + return BLOCKS[who].combat[0]; +} + +function block_printed_fire_power(who) { + return BLOCKS[who].combat[1] | 0; +} + +function block_fire_power(who, where) { + let combat = block_printed_fire_power(who); + return combat; +} + +function block_move(who) { + return BLOCKS[who].move; +} + +function block_max_steps(who) { + return BLOCKS[who].steps; +} + +function is_block_on_map(who) { + return game.location[who] && game.location[who] != F_POOL && game.location[who] != S_POOL; +} + +function can_activate(who) { + return block_owner(who) == game.active && is_block_on_map(who) && !game.moved[who]; +} + +function road_id(a, b) { + return (a < b) ? a + "/" + b : b + "/" + a; +} + +function road_was_last_used_by_enemy(from, to) { + return game.last_used[road_id(from, to)] == ENEMY[game.active]; +} + +function road_type(a, b) { + return ROADS[road_id(a,b)]; +} + +function road_limit(a, b) { + return game.road_limit[road_id(a,b)] || 0; +} + +function reset_road_limits() { + game.road_limit = {}; +} + +function count_friendly(where) { + let p = game.active; + let count = 0; + for (let b in BLOCKS) + if (game.location[b] == where && block_owner(b) == p) + ++count; + return count; +} + +function count_enemy(where) { + let p = ENEMY[game.active]; + let count = 0; + for (let b in BLOCKS) + if (game.location[b] == where && block_owner(b) == p) + ++count; + return count; +} + +function count_enemy_excluding_reserves(where) { + let p = ENEMY[game.active]; + let count = 0; + for (let b in BLOCKS) + if (game.location[b] == where && block_owner(b) == p) + if (!game.reserves.includes(b)) + ++count; + return count; +} + +function is_friendly_town(where) { return count_friendly(where) > 0 && count_enemy(where) == 0; } +function is_enemy_town(where) { return count_friendly(where) == 0 && count_enemy(where) > 0; } +function is_vacant_town(where) { return count_friendly(where) == 0 && count_enemy(where) == 0; } +function is_contested_town(where) { return count_friendly(where) > 0 && count_enemy(where) > 0; } +function is_friendly_or_vacant_town(where) { return is_friendly_town(where) || is_vacant_town(where); } + +function is_fortified_port(where) { + return TOWNS[where].fortified_port; +} + +function is_port(where) { + return TOWNS[where].port; +} + +function is_friendly_port(where) { + // TODO: Tripoli/Tyre are friendly to besieged defender! + return TOWNS[where].port && is_friendly_town(where); +} + +function have_contested_towns() { + for (let where in TOWNS) + if (is_contested_town(where)) + return true; + return false; +} + +function count_pinning(where) { + return count_enemy_excluding_reserves(where); +} + +function count_pinned(where) { + let count = 0; + for (let b in BLOCKS) + if (game.location[b] == where && block_owner(b) == game.active) + if (!game.reserves.includes(b)) + ++count; + return count; +} + +function is_pinned(who, from) { + if (game.active == game.p2) { + if (count_pinned(from) <= count_pinning(from)) + return true; + } + return false; +} + +function can_block_use_road(who, from, to) { + switch (road_type(from, to)) { + case 'major': return road_limit(from, to) < 4; + case 'minor': return road_limit(from, to) < 2; + } + return false; +} + +function can_block_land_move_to(who, from, to) { + if (can_block_use_road(who, from, to)) { + if (count_pinning(from) > 0) + if (road_was_last_used_by_enemy(from, to)) + return false; + return true; + } + return false; +} + +function can_block_land_move(who) { + if (can_activate(who)) { + let from = game.location[who]; + if (from) { + if (is_pinned(who, from)) + return false; + for (let to of TOWNS[from].exits) + if (can_block_land_move_to(who, from, to)) + return true; + } + } + return false; +} + +function can_block_sea_move_to(who, from, to) { + if (is_port(to)) { + // English Crusaders may attack by sea if they are the Main Attacker + if (who == "Richard" || who == "Robert" || who == "Crossbows") { + if (game.attacker[to] != FRANK) + return false; + if (game.main_road[to] != "England") + return false; + return true; + } + return is_friendly_port(to); + } + return false; +} + +function can_block_sea_move(who) { + if (can_activate(who)) { + let from = game.location[who]; + if (is_friendly_port(from)) { + for (let to of PORTS) + if (to != from && can_block_sea_move_to(who, from, to)) + return true; + } + } + return false; +} + +function can_block_continue(who, from, to) { + if (is_contested_town(to)) + return false; + if (game.distance >= block_move(who)) + return false; + return true; +} + +function can_block_retreat_to(who, to) { + if (is_friendly_town(to) || is_vacant_town(to)) { + let from = game.location[who]; + if (can_block_use_road(who, from, to)) { + if (road_was_last_used_by_enemy(from, to)) + return false; + return true; + } + } + return false; +} + +function can_block_regroup_to(who, to) { + if (is_friendly_town(to) || is_vacant_town(to)) { + let from = game.location[who]; + if (can_block_use_road(who, from, to)) + return true; + } + return false; +} + +function can_block_regroup(who) { + if (block_owner(who) == game.active) { + let from = game.location[who]; + for (let to of TOWNS[from].exits) + if (can_block_regroup_to(who, to)) + return true; + } + return false; +} + +function can_block_muster_via(who, from, next, muster) { + if (can_block_land_move_to(who, from, next) && is_friendly_or_vacant_town(next)) { + if (next == muster) + return true; + if (road_type(from, next) != 'minor') { + if (TOWNS[next].exits.includes(muster)) + if (can_block_land_move_to(who, next, muster)) + return true; + } + } +} + +function can_block_muster(who, muster) { + let from = game.location[who]; + if (from == muster) + return false; + if (can_activate(who)) { + if (is_pinned(who, from)) + return false; + for (let next of TOWNS[from].exits) + if (can_block_muster_via(who, from, next, muster)) + return true; + } + return false; +} + +function can_muster_to(muster) { + for (let b in BLOCKS) + if (can_block_muster(b, muster)) + return true; + return false; +} + +function is_battle_reserve(who) { + return game.reserves.includes(who); +} + +function is_attacker(who) { + if (game.location[who] == game.where && block_owner(who) == game.attacker[game.where]) + return !game.reserves.includes(who); + return false; +} + +function is_defender(who) { + if (game.location[who] == game.where && block_owner(who) != game.attacker[game.where]) + return !game.reserves.includes(who); + return false; +} + +function disband(who) { + log(block_name(who) + " disbands."); + game.location[who] = block_pool(who); + game.steps[who] = block_max_steps(who); +} + +function eliminate_block(who) { + log(block_name(who) + " is eliminated."); + game.location[who] = null; + game.steps[who] = block_max_steps(who); +} + +function reduce_block(who) { + if (game.steps[who] == 1) { + eliminate_block(who); + } else { + --game.steps[who]; + } +} + +function filter_battle_blocks(ci, is_candidate) { + let output = null; + for (let b in BLOCKS) { + if (is_candidate(b) && !game.moved[b]) { + if (block_initiative(b) == ci) { + if (!output) + output = []; + output.push(b); + } + } + } + return output; +} + +function count_attackers() { + let count = 0; + for (let b in BLOCKS) + if (is_attacker(b)) + ++count; + return count; +} + +function count_defenders() { + let count = 0; + for (let b in BLOCKS) + if (is_defender(b)) + ++count; + return count; +} + +// GAME TURN + +function start_year() { + log(""); + log("Start Year " + game.year + "."); + + let deck = shuffle_deck(); + game.f_hand = deal_cards(deck, 6); + game.s_hand = deal_cards(deck, 6); + + start_game_turn(); +} + +function start_game_turn() { + // Reset movement and attack tracking state + reset_road_limits(); + game.last_used = {}; + game.attacker = {}; + game.reserves = []; + game.moved = {}; + + goto_card_phase(); +} + +function end_game_turn() { + if (game.f_hand.length > 0) + start_game_turn() + else + goto_winter_turn(); +} + +// CARD PHASE + +function goto_card_phase() { + game.f_card = 0; + game.s_card = 0; + game.show_cards = false; + game.state = 'play_card'; + game.active = BOTH; +} + +states.play_card = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for players to play a card."; + if (current == FRANK) { + if (game.f_card) { + view.prompt = "Waiting for Saracen to play a card."; + gen_action(view, 'undo'); + } else { + view.prompt = "Play a card."; + for (let c of game.f_hand) + gen_action(view, 'play', c); + } + } + if (current == SARACEN) { + if (game.s_card) { + view.prompt = "Waiting for Frank to play a card."; + gen_action(view, 'undo'); + } else { + view.prompt = "Play a card."; + for (let c of game.s_hand) + gen_action(view, 'play', c); + } + } + }, + play: function (card, current) { + if (current == FRANK) { + remove_from_array(game.f_hand, card); + game.f_card = card; + } + if (current == SARACEN) { + remove_from_array(game.s_hand, card); + game.s_card = card; + } + if (game.s_card > 0 && game.f_card > 0) + reveal_cards(); + }, + undo: function (_, current) { + if (current == FRANK) { + game.f_hand.push(game.f_card); + game.f_card = 0; + } + if (current == SARACEN) { + game.s_hand.push(game.s_card); + game.s_card = 0; + } + } +} + +function reveal_cards() { + log("Frank plays " + CARDS[game.f_card].name + "."); + log("Saracen plays " + CARDS[game.s_card].name + "."); + game.show_cards = true; + + let fc = CARDS[game.f_card]; + let sc = CARDS[game.s_card]; + + if (fc.event && sc.event) { + log("Game Turn is cancelled."); + end_game_turn(); + return; + } + + let fp = fc.moves; + let sp = sc.moves; + if (fp == sp) { + if (roll_d6() > 3) + ++fp; + else + ++sp; + } + + if (fp > sp) { + game.p1 = FRANK; + game.p2 = SARACEN; + } else { + game.p1 = SARACEN; + game.p2 = FRANK; + } + + game.active = game.p1; + start_player_turn(); +} + +function start_player_turn() { + log(""); + log("Start " + game.active + " turn."); + reset_road_limits(); + let card = CARDS[game.active == FRANK ? game.f_card : game.s_card]; + if (card.event) + goto_event_card(card.event); + else + goto_move_phase(card.moves); +} + +function end_player_turn() { + game.moves = 0; + game.activated = null; + game.main_road = null; + + if (game.active == game.p2) { + goto_battle_phase(); + } else { + game.active = game.p2; + start_player_turn(); + } +} + +// EVENTS + +function goto_event_card(event) { + end_player_turn(); +} + +// ACTION PHASE + +function move_block(who, from, to) { + game.location[who] = to; + game.road_limit[road_id(from, to)] = road_limit(from, to) + 1; + game.distance ++; + if (is_contested_town(to)) { + game.last_used[road_id(from, to)] = game.active; + if (!game.attacker[to]) { + game.attacker[to] = game.active; + game.main_road[to] = from; + return ATTACK_MARK; + } else { + // Attacker main attack or reinforcements + if (game.attacker[to] == game.active) { + if (game.main_road[to] != from) { + game.reserves.push(who); + return RESERVE_MARK_1; + } + return ATTACK_MARK; + } + + // Defender reinforcements + if (!game.main_road[to]) + game.main_road[to] = from; + + if (game.main_road[to] == from) { + game.reserves.push(who); + return RESERVE_MARK_1; + } else { + game.reserves.push(who); + return RESERVE_MARK_2; + } + } + } + return false; +} + +function goto_move_phase(moves) { + game.state = 'group_move'; + game.moves = moves; + game.activated = []; + game.main_road = {}; + game.turn_log = []; +} + +function end_move_phase() { + game.moves = 0; + clear_undo(); + print_turn_log(game.active + " moves:"); + end_player_turn(); +} + +states.group_move = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Move Phase: Waiting for " + game.active + "."; + view.prompt = "Group Move: Choose a block to group move. " + game.moves + "AP left."; + gen_action_undo(view); + gen_action(view, 'end_move_phase'); + if (game.moves > 0) { + gen_action(view, 'sea_move'); + gen_action(view, 'muster'); + for (let b in BLOCKS) + if (can_block_land_move(b)) + gen_action(view, 'block', b); + } else { + for (let b in BLOCKS) { + let from = game.location[b]; + if (game.activated.includes(from)) + if (can_block_land_move(b)) + gen_action(view, 'block', b); + } + } + }, + block: function (who) { + push_undo(); + game.who = who; + game.origin = game.location[who]; + game.distance = 0; + game.state = 'group_move_to'; + }, + sea_move: function () { + game.state = 'sea_move'; + }, + muster: function () { + game.state = 'muster'; + }, + end_move_phase: end_move_phase, + undo: pop_undo +} + +states.group_move_to = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to move."; + view.prompt = "Group Move: Move " + block_name(game.who) + "."; + gen_action_undo(view); + gen_action(view, 'block', game.who); + let from = game.location[game.who]; + if (game.distance > 0) + gen_action(view, 'town', from); + for (let to of TOWNS[from].exits) { + if (can_block_land_move_to(game.who, from, to)) + gen_action(view, 'town', to); + } + }, + town: function (to) { + let from = game.location[game.who]; + if (to == from) { + end_move(); + return; + } + if (game.distance == 0) + log_move_start(from); + let mark = move_block(game.who, from, to); + if (mark) + log_move_continue(to + mark); + else + log_move_continue(to); + if (!can_block_continue(game.who, from, to)) + end_move(); + }, + block: function () { + if (game.distance == 0) + pop_undo(); + else + end_move(); + }, + undo: pop_undo +} + +states.sea_move = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Move Phase: Waiting for " + game.active + "."; + view.prompt = "Sea Move: Choose a block to sea move. " + game.moves + "AP left."; + gen_action_undo(view); + gen_action(view, 'end_move_phase'); + gen_action(view, 'group_move'); + if (game.moves > 0) { + gen_action(view, 'muster'); + for (let b in BLOCKS) { + let from = game.location[b]; + if (can_block_sea_move(b)) { + gen_action(view, 'block', b); + } + } + } + }, + group_move: function () { + game.state = 'group_move'; + }, + muster: function () { + game.state = 'muster'; + }, + block: function (who) { + push_undo(); + game.who = who; + game.state = 'sea_move_to'; + }, + end_move_phase: end_move_phase, + undo: pop_undo +} + +states.sea_move_to = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to move."; + view.prompt = "Sea Move " + block_name(game.who) + "."; + gen_action_undo(view); + gen_action(view, 'block', game.who); + let from = game.location[game.who]; + for (let to of PORTS) + if (to != from && can_block_sea_move_to(game.who, from, to)) + gen_action(view, 'town', to); + }, + town: function (to) { + --game.moves; + let from = game.location[game.who]; + game.location[game.who] = to; + game.moved[game.who] = true; + log_move_start(from); + log_move_continue("Sea"); + + // English Crusaders attack! + if (is_contested_town(to)) { + game.attacker[to] = FRANK; + game.main_road[to] = "England"; + log_move_continue(to + ATTACK_MARK); + } else { + log_move_continue(to); + } + + game.state = 'sea_move'; + game.who = null; + }, + block: pop_undo, + undo: pop_undo +} + +states.muster = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to move."; + view.prompt = "Muster: Choose one friendly or vacant muster area."; + gen_action_undo(view); + gen_action(view, 'group_move'); + gen_action(view, 'sea_move'); + gen_action_undo(view); + gen_action(view, 'end_action_phase'); + for (let where in TOWNS) { + if (is_friendly_or_vacant_town(where)) + if (can_muster_to(where)) + gen_action(view, 'town', where); + } + }, + area: function (where) { + push_undo(); + game.where = where; + game.state = 'muster_who'; + }, + end_action_phase: function () { + clear_undo(); + print_turn_log(game.active + " musters:"); + end_player_turn(); + }, + undo: pop_undo, +} + +states.muster_who = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to move."; + view.prompt = "Muster: Move blocks to the designated muster area."; + gen_action_undo(view); + gen_action(view, 'end_action_phase'); + for (let b in BLOCKS) + if (can_block_muster(b, game.where)) + gen_action(view, 'block', b); + }, + block: function (who) { + push_undo(); + game.who = who; + game.state = 'muster_move_1'; + }, + end_action_phase: function () { + game.where = null; + clear_undo(); + print_turn_log(game.active + " musters:"); + end_player_turn(); + }, + undo: pop_undo, +} + +function end_move() { + if (game.distance > 0) { + let to = game.location[game.who]; + if (!game.activated.includes(game.origin)) { + logp("activates " + game.origin + "."); + game.activated.push(game.origin); + game.moves --; + } + game.moved[game.who] = true; + } + log_move_end(); + game.who = null; + game.distance = 0; + game.origin = null; + game.state = 'group_move'; +} + +// BATTLE PHASE + +function goto_battle_phase() { + if (have_contested_towns()) { + game.active = game.p1; + game.state = 'battle_phase'; + } else { + goto_draw_phase(); + } +} + +states.battle_phase = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to choose a battle."; + view.prompt = "Choose the next battle to fight!"; + for (let where in TOWNS) + if (is_contested_town(where)) + gen_action(view, 'town', where); + }, + town: function (where) { + start_battle(where); + }, +} + +function start_battle(where) { + game.flash = ""; + log(""); + log("Battle in " + where + "."); + game.where = where; + game.battle_round = 0; + game.state = 'battle_round'; + start_battle_round(); +} + +function resume_battle() { + if (game.victory) + return goto_game_over(); + game.who = null; + game.state = 'battle_round'; + pump_battle_round(); +} + +function end_battle() { + game.flash = ""; + game.battle_round = 0; + reset_road_limits(); + game.moved = {}; + + game.active = game.attacker[game.where]; + let victor = game.active; + if (is_contested_town(game.where)) + victor = ENEMY[game.active]; + else if (is_enemy_town(game.where)) + victor = ENEMY[game.active]; + log(victor + " wins the battle in " + game.where + "!"); + + goto_retreat(); +} + +function bring_on_reserves(round) { + // TODO: defender reserves in round 3... + for (let b in BLOCKS) { + if (game.location[b] == game.where) { + remove_from_array(game.reserves, b); + } + } +} + +function start_battle_round() { + if (++game.battle_round <= 3) { + log("~ Battle Round " + game.battle_round + " ~"); + + reset_road_limits(); + game.moved = {}; + + if (game.battle_round == 2) { + if (count_defenders() == 0) { + log("Defending main force was eliminated."); + log("The attacker is now the defender."); + game.attacker[game.where] = ENEMY[game.attacker[game.where]]; + } else if (count_attackers() == 0) { + log("Attacking main force was eliminated."); + } + bring_on_reserves(2); + } + + if (game.battle_round == 3) { + bring_on_reserves(3); + } + + pump_battle_round(); + } else { + end_battle(); + } +} + +function pump_battle_round() { + if (is_friendly_town(game.where) || is_enemy_town(game.where)) { + end_battle(); + } else if (count_attackers() == 0 || count_defenders() == 0) { + start_battle_round(); + } else { + function battle_step(active, initiative, candidate) { + game.battle_list = filter_battle_blocks(initiative, candidate); + if (game.battle_list) { + game.active = active; + return true; + } + return false; + } + + let attacker = game.attacker[game.where]; + let defender = ENEMY[attacker]; + + if (battle_step(defender, 'A', is_defender)) return; + if (battle_step(attacker, 'A', is_attacker)) return; + if (battle_step(defender, 'B', is_defender)) return; + if (battle_step(attacker, 'B', is_attacker)) return; + if (battle_step(defender, 'C', is_defender)) return; + if (battle_step(attacker, 'C', is_attacker)) return; + + start_battle_round(); + } +} + +function retreat_with_block(b) { + game.who = b; + game.state = 'retreat_in_battle'; +} + +function roll_attack(verb, b) { + game.hits = 0; + let fire = block_fire_power(b, game.where); + let printed_fire = block_printed_fire_power(b); + let rolls = []; + let results = []; + let steps = game.steps[b]; + for (let i = 0; i < steps; ++i) { + let die = roll_d6(); + rolls.push(die); + if (die <= fire) { + results.push(HIT_TEXT); + ++game.hits; + } else { + results.push(MISS_TEXT); + } + } + game.flash += block_name(b) + " " + BLOCKS[b].combat; + if (fire > printed_fire) + game.flash += "+" + (fire - printed_fire); + game.flash += " " + verb + "\n" + rolls.join(" ") + " = " + results.join(" "); +} + +function fire_with_block(b) { + game.moved[b] = true; + game.flash = ""; + roll_attack("fires", b); + log(game.flash); + if (game.hits > 0) { + game.active = ENEMY[game.active]; + goto_battle_hits(); + } else { + resume_battle(); + } +} + +states.battle_round = { + show_battle: true, + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to choose a combat action."; + view.prompt = "Battle: Charge, Fire, Harry, or Retreat with an army."; + for (let b of game.battle_list) { + gen_action(view, 'block', b); // take default action + gen_action(view, 'battle_fire', b); + gen_action(view, 'battle_retreat', b); + // Turcopoles and Nomads can Harry (fire and retreat) + if (block_type(b) == 'turcopoles' || block_type(b) == 'nomads') + gen_action(view, 'battle_harry', b); + // All Frank B blocks are knights who can Charge + if (block_owner(b) == FRANK && block_initiative(b) == 'B') + gen_action(view, 'battle_charge', b); + } + }, + battle_charge: function (who) { + charge_with_block(who); + }, + battle_fire: function (who) { + fire_with_block(who); + }, + battle_harry: function (who) { + harry_with_block(who); + }, + battle_retreat: function (who) { + retreat_with_block(who); + }, + block: function (who) { + fire_with_block(who); + }, +} + +function goto_battle_hits() { + game.battle_list = list_victims(game.active); + if (game.battle_list.length == 0) + resume_battle(); + else + game.state = 'battle_hits'; +} + +function apply_hit(who) { + game.flash = block_name(who) + " takes a hit."; + log(game.flash); + reduce_block(who, 'combat'); + game.hits--; + if (game.hits == 1) + game.flash += " 1 hit left."; + else if (game.hits > 1) + game.flash += " " + game.hits + " hits left."; + if (game.hits == 0) + resume_battle(); + else + goto_battle_hits(); +} + +function list_victims(p) { + let is_candidate = (p == game.attacker[game.where]) ? is_attacker : is_defender; + let max = 0; + for (let b in BLOCKS) + if (is_candidate(b) && game.steps[b] > max) + max = game.steps[b]; + let list = []; + for (let b in BLOCKS) + if (is_candidate(b) && game.steps[b] == max) + list.push(b); + return list; +} + +states.battle_hits = { + show_battle: true, + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to assign hits."; + view.prompt = "Assign " + game.hits + (game.hits != 1 ? " hits" : " hit") + " to your armies."; + for (let b of game.battle_list) { + gen_action(view, 'battle_hit', b); + gen_action(view, 'block', b); + } + }, + battle_hit: function (who) { + apply_hit(who); + }, + block: function (who) { + apply_hit(who); + }, +} + +function goto_retreat() { + game.active = game.attacker[game.where]; + if (is_contested_town(game.where)) { + game.state = 'retreat'; + game.turn_log = []; + clear_undo(); + } else { + clear_undo(); + goto_regroup(); + } +} + +states.retreat = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to retreat."; + view.prompt = "Retreat: Choose an army to move."; + gen_action_undo(view); + let can_retreat = false; + for (let b in BLOCKS) { + if (game.location[b] == game.where && can_block_retreat(b)) { + gen_action(view, 'block', b); + can_retreat = true; + } + } + if (!is_contested_town(game.where) || !can_retreat) + gen_action(view, 'end_retreat'); + }, + end_retreat: function () { + for (let b in BLOCKS) + if (game.location[b] == game.where && block_owner(b) == game.active) + eliminate_block(b); + print_turn_log(game.active + " retreats:"); + clear_undo(); + goto_regroup(); + }, + block: function (who) { + push_undo(); + game.who = who; + game.state = 'retreat_to'; + }, + undo: pop_undo +} + +states.retreat_to = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to retreat."; + view.prompt = "Retreat: Move the army to a friendly or neutral area."; + gen_action_undo(view); + gen_action(view, 'block', game.who); + let can_retreat = false; + for (let to of AREAS[game.where].exits) { + if (can_block_retreat_to(game.who, to)) { + gen_action(view, 'area', to); + can_retreat = true; + } + } + if (!can_retreat) + gen_action(view, 'eliminate'); + }, + area: function (to) { + let from = game.where; + game.turn_log.push([from, to]); + move_block(game.who, game.where, to); + game.who = null; + game.state = 'retreat'; + }, + eliminate: function () { + eliminate_block(game.who); + game.who = null; + game.state = 'retreat'; + }, + block: pop_undo, + undo: pop_undo +} + +states.retreat_in_battle = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to retreat."; + gen_action(view, 'undo'); + gen_action(view, 'block', game.who); + view.prompt = "Retreat: Move the army to a friendly or vacant area."; + for (let to of TOWNS[game.where].exits) + if (can_block_retreat_to(game.who, to)) + gen_action(view, 'town', to); + }, + town: function (to) { + logp("retreats to " + to + "."); + game.location[game.who] = to; + resume_battle(); + }, + eliminate: function () { + eliminate_block(game.who); + resume_battle(); + }, + block: function (to) { + resume_battle(); + }, + undo: function () { + resume_battle(); + } +} + +function goto_regroup() { + game.active = game.attacker[game.where]; + if (is_enemy_town(game.where)) + game.active = ENEMY[game.active]; + log(game.active + " wins the battle in " + game.where + "."); + game.state = 'regroup'; + game.turn_log = []; +} + +states.regroup = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to regroup."; + view.prompt = "Regroup: Choose an army to move."; + gen_action_undo(view); + gen_action(view, 'end_regroup'); + for (let b in BLOCKS) { + if (game.location[b] == game.where) { + if (can_block_regroup(b)) + gen_action(view, 'block', b); + } + } + }, + block: function (who) { + push_undo(); + game.who = who; + game.state = 'regroup_to'; + }, + end_regroup: function () { + print_turn_log(game.active + " regroups:"); + game.where = null; + clear_undo(); + goto_battle_phase(); + }, + undo: pop_undo +} + +states.regroup_to = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to regroup."; + view.prompt = "Regroup: Move the army to a friendly or vacant area."; + gen_action_undo(view); + gen_action(view, 'block', game.who); + for (let to of TOWNS[game.where].exits) + if (can_block_regroup_to(game.who, to)) + gen_action(view, 'town', to); + }, + town: function (to) { + game.turn_log.push([from, to]); + move_block(game.who, game.where, to); + game.who = null; + game.state = 'regroup'; + }, + block: pop_undo, + undo: pop_undo +} + +// DRAW PHASE + +function goto_draw_phase() { + end_game_turn(); +} + +// GAME OVER + +function goto_game_over() { + game.active = "None"; + game.state = 'game_over'; +} + +states.game_over = { + prompt: function (view, current) { + view.prompt = game.victory; + } +} + +// SETUP + +function deploy(who, where) { + game.location[who] = where; + game.steps[who] = block_max_steps(who); +} + +function reset_blocks() { + for (let b in BLOCKS) { + game.location[b] = null; + game.steps[b] = block_max_steps(b); + } +} + +function setup_game() { + reset_blocks(); + game.year = 1187; + for (let b in BLOCKS) { + if (block_owner(b) == FRANK) { + switch (block_type(b)) { + case 'pilgrims': + case 'crusaders': + deploy(b, block_pool(b)); + break; + default: + deploy(b, block_home(b)); + break; + } + } + if (block_owner(b) == SARACEN) { + if (block_type(b) == 'emirs') + deploy(b, block_home(b)); + if (block_type(b) == 'nomads') + deploy(b, block_pool(b)); + } + if (block_owner(b) == ASSASSINS) { + deploy(b, block_home(b)); + } + } +} + +// VIEW + +function make_battle_view() { + let battle = { + FA: [], FB: [], FC: [], FR: [], + SA: [], SB: [], SC: [], SR: [], + flash: game.flash + }; + + battle.title = game.attacker[game.where] + " attacks " + game.where; + battle.title += " \u2014 round " + game.battle_round + " of 3"; + + function fill_cell(cell, owner, fn) { + for (let b in BLOCKS) + if (game.location[b] == game.where & block_owner(b) == owner && fn(b)) + cell.push([b, game.steps[b], game.moved[b]?1:0]) + } + + fill_cell(battle.FR, FRANK, b => is_battle_reserve(b)); + fill_cell(battle.FA, FRANK, b => !is_battle_reserve(b) && block_initiative(b) == 'A'); + fill_cell(battle.FB, FRANK, b => !is_battle_reserve(b) && block_initiative(b) == 'B'); + fill_cell(battle.FC, FRANK, b => !is_battle_reserve(b) && block_initiative(b) == 'C'); + + fill_cell(battle.SR, SARACEN, b => is_battle_reserve(b)); + fill_cell(battle.SA, SARACEN, b => !is_battle_reserve(b) && block_initiative(b) == 'A'); + fill_cell(battle.SB, SARACEN, b => !is_battle_reserve(b) && block_initiative(b) == 'B'); + fill_cell(battle.SC, SARACEN, b => !is_battle_reserve(b) && block_initiative(b) == 'C'); + + return battle; +} + +exports.ready = function (scenario, players) { + return players.length === 2; +} + +exports.setup = function (scenario, players) { + game = { + attacker: {}, + road_limit: {}, + last_used: {}, + location: {}, + log: [], + main_road: {}, + moved: {}, + moves: 0, + prompt: null, + reserves: [], + show_cards: false, + steps: {}, + who: null, + where: null, + undo: [], + } + setup_game(); + start_year(); + return game; +} + +exports.action = function (state, current, action, arg) { + game = state; + // TODO: check action and argument against action list + if (is_active_player(current)) { + let S = states[game.state]; + if (action in S) { + S[action](arg, current); + } else { + throw new Error("Invalid action: " + action); + } + } + return state; +} + +exports.resign = function (state, current) { + game = state; + if (game.state != 'game_over') { + log(""); + log(current + " resigned."); + game.active = "None"; + game.state = 'game_over'; + game.victory = current + " resigned."; + game.result = ENEMY[current]; + } +} + +exports.view = function(state, current) { + game = state; + + let view = { + log: game.log, + year: game.year, + active: game.active, + f_card: (game.show_cards || current == FRANK) ? game.f_card : 0, + s_card: (game.show_cards || current == SARACEN) ? game.s_card : 0, + hand: (current == FRANK) ? game.f_hand : (current == SARACEN) ? game.s_hand : [], + who: (game.active == current) ? game.who : null, + where: game.where, + known: {}, + secret: { Frank: {}, Saracen: {}, Assassins: {} }, + battle: null, + prompt: null, + actions: null, + }; + + states[game.state].prompt(view, current); + + if (states[game.state].show_battle) + view.battle = make_battle_view(); + + for (let b in BLOCKS) { + let a = game.location[b]; + if (!a) + continue; + if (a == F_POOL) // && current != FRANK) + continue; + if (a == S_POOL) // && current != SARACEN) + continue; + if (a == F_POOL || a == S_POOL) + a = "Pool"; + + let is_known = false; + if (current == block_owner(b)) + is_known = true; + if (b == ASSASSINS) + is_known = true; + + if (is_known) { + view.known[b] = [a, game.steps[b], game.moved[b] ? 1 : 0]; + } else { + let list = view.secret[BLOCKS[b].owner]; + if (!(a in list)) + list[a] = [0, 0]; + list[a][0]++; + if (game.moved[b]) + list[a][1]++; + } + } + + return view; +} diff --git a/thumbnail.jpg b/thumbnail.jpg Binary files differnew file mode 100644 index 0000000..1f6ebcb --- /dev/null +++ b/thumbnail.jpg @@ -0,0 +1,718 @@ +"use strict"; + +const FRANK = "Frank"; +const SARACEN = "Saracen"; +const ASSASSINS = "Assassins"; +const ENEMY = { Saracen: "Frank", Frank: "Saracen" } +const POOL = "Pool"; + +function toggle_blocks() { + document.getElementById("map").classList.toggle("hide_blocks"); +} + +let map_orientation = window.localStorage['crusader-rex/map-orientation'] || 'tall'; + +function tall_map() { + map_orientation = 'tall'; + document.querySelector(".map").classList.remove("wide"); + document.querySelector(".map").classList.add("tall"); + window.localStorage['crusader-rex/map-orientation'] = map_orientation; + update_map_layout(); + update_map(); + zoom_map(); +} + +function wide_map() { + map_orientation = 'wide'; + document.querySelector(".map").classList.add("wide"); + document.querySelector(".map").classList.remove("tall"); + window.localStorage['crusader-rex/map-orientation'] = map_orientation; + update_map_layout(); + update_map(); + zoom_map(); +} + +let game = null; + +let ui = { + cards: {}, + towns: {}, + known: {}, + secret: { Frank: {}, Saracen: {}, Assassins: {} }, + battle_menu: {}, + battle_block: {}, + present: new Set(), +} + +function on_focus_town(evt) { + let where = evt.target.town; + let text = where; + document.getElementById("status").textContent = text; +} + +function on_blur_town(evt) { + document.getElementById("status").textContent = ""; +} + +function on_click_town(evt) { + let where = evt.target.town; + if (game.actions && game.actions.town && game.actions.town.includes(where)) + socket.emit('action', 'town', where); +} + +const STEP_TEXT = [ 0, "I", "II", "III", "IIII" ]; +const HEIR_TEXT = [ 0, '\u00b9', '\u00b2', '\u00b3', '\u2074', '\u2075' ]; + +function block_name(who) { return BLOCKS[who].name; } +function block_home(who) { return BLOCKS[who].home; } +function block_owner(who) { return BLOCKS[who].owner; } + +function on_focus_secret_block(evt) { + let owner = evt.target.owner; + let text = owner; + document.getElementById("status").textContent = text; +} + +function on_blur_secret_block(evt) { + document.getElementById("status").textContent = ""; +} + +function on_click_secret_block(evt) { +} + +function on_focus_map_block(evt) { + let b = evt.target.block; + let s = game.known[b][1]; + let text = block_name(b) + " (" + block_home(b) + ") "; + if (BLOCKS[b].move) + text += BLOCKS[b].move + "-"; + text += STEP_TEXT[s] + "-" + BLOCKS[b].combat; + document.getElementById("status").textContent = text; +} + +function on_blur_map_block(evt) { + document.getElementById("status").textContent = ""; +} + +function on_click_map_block(evt) { + let b = evt.target.block; + if (game.actions && game.actions.block && game.actions.block.includes(b)) + socket.emit('action', 'block', b); +} + +function is_battle_reserve(who, list) { + for (let [b, s, m] of list) + if (who == b) + return true; + return false; +} + +function on_focus_battle_block(evt) { + let b = evt.target.block; + let msg = block_name(b); + if (is_battle_reserve(b, game.battle.FR)) + msg = "Frank Reserve"; + if (is_battle_reserve(b, game.battle.SR)) + msg = "Saracen Reserve"; + + if (game.actions && game.actions.battle_fire && game.actions.battle_fire.includes(b)) + msg = "Fire with " + msg; + else if (game.actions && game.actions.battle_retreat && game.actions.battle_retreat.includes(b)) + msg = "Retreat with " + msg; + else if (game.actions && game.actions.battle_hit && game.actions.battle_hit.includes(b)) + msg = "Take hit on " + msg; + + document.getElementById("status").textContent = msg; +} + +function on_blur_battle_block(evt) { + document.getElementById("status").textContent = ""; +} + +function on_click_battle_block(evt) { + let b = evt.target.block; + if (game.actions && game.actions.block && game.actions.block.includes(b)) + socket.emit('action', 'block', b); +} + +function on_focus_battle_fire(evt) { + document.getElementById("status").textContent = + "Fire with " + block_name(evt.target.block); +} + +function on_focus_battle_retreat(evt) { + document.getElementById("status").textContent = + "Retreat with " + block_name(evt.target.block); +} + +function on_focus_battle_harry(evt) { + document.getElementById("status").textContent = + "Harry with " + block_name(evt.target.block); +} + +function on_focus_battle_charge(evt) { + document.getElementById("status").textContent = + "Charge with " + block_name(evt.target.block); +} + +function on_focus_battle_hit(evt) { + document.getElementById("status").textContent = + "Take hit on " + block_name(evt.target.block); +} + +function on_blur_battle_button(evt) { + document.getElementById("status").textContent = ""; +} + +function on_click_battle_hit(evt) { socket.emit('action', 'battle_hit', evt.target.block); } +function on_click_battle_fire(evt) { socket.emit('action', 'battle_fire', evt.target.block); } +function on_click_battle_retreat(evt) { socket.emit('action', 'battle_retreat', evt.target.block); } +function on_click_battle_charge(evt) { socket.emit('action', 'battle_charge', evt.target.block); } +function on_click_battle_harry(evt) { socket.emit('action', 'battle_harry', evt.target.block); } + +function on_click_card(evt) { + let c = evt.target.id.split("+")[1] | 0; + if (game.actions && game.actions.play && game.actions.play.includes(c)) + socket.emit('action', 'play', c); +} + +function on_button_undo(evt) { send_action('undo'); } +function on_button_pass(evt) { send_action('pass'); } +function on_button_sea_move(evt) { send_action('sea_move'); } +function on_button_group_move(evt) { send_action('group_move'); } +function on_button_muster(evt) { send_action('muster'); } +function on_button_end_muster(evt) { send_action('end_muster'); } +function on_button_end_move_phase(evt) { send_action('end_move_phase'); } +function on_button_end_regroup(evt) { send_action('end_regroup'); } +function on_button_end_retreat(evt) { send_action('end_retreat'); } +function on_button_eliminate(evt) { send_action('eliminate'); } + +function build_battle_button(menu, b, c, click, enter, img_src) { + let img = new Image(); + img.draggable = false; + img.classList.add("action"); + img.classList.add(c); + img.setAttribute("src", img_src); + img.addEventListener("click", click); + img.addEventListener("mouseenter", enter); + img.addEventListener("mouseleave", on_blur_battle_button); + img.block = b; + menu.appendChild(img); +} + +function build_battle_block(b, block) { + let element = document.createElement("div"); + element.classList.add("block"); + element.classList.add("known"); + element.classList.add(BLOCKS[b].owner); + element.classList.add("block_" + block.image); + element.addEventListener("mouseenter", on_focus_battle_block); + element.addEventListener("mouseleave", on_blur_battle_block); + element.addEventListener("click", on_click_battle_block); + element.block = b; + ui.battle_block[b] = element; + + let menu_list = document.createElement("div"); + menu_list.classList.add("battle_menu_list"); + + build_battle_button(menu_list, b, "hit", + on_click_battle_hit, on_focus_battle_hit, + "/images/cross-mark.svg"); + build_battle_button(menu_list, b, "charge", + on_click_battle_charge, on_focus_battle_charge, + "/images/mounted-knight.svg"); + build_battle_button(menu_list, b, "fire", + on_click_battle_fire, on_focus_battle_fire, + "/images/pointy-sword.svg"); + build_battle_button(menu_list, b, "harry", + on_click_battle_harry, on_focus_battle_harry, + "/images/arrow-flights.svg"); + build_battle_button(menu_list, b, "retreat", + on_click_battle_retreat, on_focus_battle_retreat, + "/images/flying-flag.svg"); + + let menu = document.createElement("div"); + menu.classList.add("battle_menu"); + menu.appendChild(element); + menu.appendChild(menu_list); + ui.battle_menu[b] = menu; +} + +function build_known_block(b, block) { + let element = document.createElement("div"); + element.classList.add("block"); + element.classList.add("known"); + element.classList.add(BLOCKS[b].owner); + element.classList.add("block_" + block.image); + element.addEventListener("mouseenter", on_focus_map_block); + element.addEventListener("mouseleave", on_blur_map_block); + element.addEventListener("click", on_click_map_block); + element.block = b; + return element; +} + +function build_secret_block(b, block) { + let element = document.createElement("div"); + element.classList.add("block"); + element.classList.add("secret"); + element.classList.add(BLOCKS[b].owner); + element.addEventListener("mouseenter", on_focus_secret_block); + element.addEventListener("mouseleave", on_blur_secret_block); + element.addEventListener("click", on_click_secret_block); + element.owner = BLOCKS[b].owner; + return element; +} + +/* +let MAP_OFFSET_X = 30; +let MAP_OFFSET_Y = 30; +let MAP_HEIGHT = 1215; +*/ +let MAP_OFFSET_X = 0; +let MAP_OFFSET_Y = 0; +let MAP_HEIGHT = 1275; + +function town_x(t) { + if (map_orientation == 'tall') + return TOWNS[t].x - MAP_OFFSET_X; + else + return TOWNS[t].y - MAP_OFFSET_Y; +} + +function town_y(t) { + if (map_orientation == 'tall') + return TOWNS[t].y - MAP_OFFSET_Y; + else + return MAP_HEIGHT - TOWNS[t].x + MAP_OFFSET_X; +} + +function flip_x(x, y) { + if (map_orientation == 'tall') + return x - MAP_OFFSET_X; + else + return y - MAP_OFFSET_Y; +} + +function flip_y(x, y) { + if (map_orientation == 'tall') + return y - MAP_OFFSET_Y; + else + return MAP_HEIGHT - x + MAP_OFFSET_X; +} + +function build_town(t, town) { + let element = document.createElement("div"); + element.town = t; + element.classList.add("town"); + element.addEventListener("mouseenter", on_focus_town); + element.addEventListener("mouseleave", on_blur_town); + element.addEventListener("click", on_click_town); + ui.towns_element.appendChild(element); + return element; +} + +function update_map_layout() { + for (let t in TOWNS) { + let element = ui.towns[t]; + element.style.left = (town_x(t) - 35) + "px"; + element.style.top = (town_y(t) - 35) + "px"; + } +} + +function build_map() { + let element; + + ui.blocks_element = document.getElementById("blocks"); + ui.offmap_element = document.getElementById("offmap"); + ui.towns_element = document.getElementById("towns"); + + for (let c = 1; c <= 27; ++c) { + ui.cards[c] = document.getElementById("card+"+c); + ui.cards[c].addEventListener("click", on_click_card); + } + + for (let name in TOWNS) { + let town = TOWNS[name]; + ui.towns[name] = build_town(name, town); + ui.secret.Frank[name] = []; + ui.secret.Saracen[name] = []; + ui.secret.Assassins[name] = []; + } + ui.secret.Frank.offmap = []; + ui.secret.Saracen.offmap = []; + ui.secret.Assassins.offmap = []; + + for (let b in BLOCKS) { + let block = BLOCKS[b]; + build_battle_block(b, block); + ui.known[b] = build_known_block(b, block); + ui.secret[BLOCKS[b].owner].offmap.push(build_secret_block(b, block)); + } + + update_map_layout(); +} + +function update_steps(b, steps, element) { + element.classList.remove("r1"); + element.classList.remove("r2"); + element.classList.remove("r3"); + element.classList.add("r"+(BLOCKS[b].steps - steps)); +} + +function layout_blocks(town, secret, known) { + let wrap = TOWNS[town].wrap; + let s = secret.length; + let k = known.length; + let n = s + k; + let row, rows = []; + let i = 0; + + function new_line() { + rows.push(row = []); + i = 0; + } + + new_line(); + + while (secret.length > 0) { + if (i == wrap) + new_line(); + row.push(secret.shift()); + ++i; + } + + // Break early if secret and known fit in exactly two rows, and more than three blocks total + if (s > 0 && s <= wrap && k > 0 && k <= wrap && n > 3) + new_line(); + + while (known.length > 0) { + if (i == wrap) + new_line(); + row.push(known.shift()); + ++i; + } + + if (TOWNS[town].layout_minor > 0.5) + rows.reverse(); + + for (let j = 0; j < rows.length; ++j) + for (i = 0; i < rows[j].length; ++i) + position_block(town, j, rows.length, i, rows[j].length, rows[j][i]); +} + +function position_block(town, row, n_rows, col, n_cols, element) { + let space = TOWNS[town]; + let block_size = 60+6; + let padding = 4; + let offset = block_size + padding; + let row_size = (n_rows-1) * offset; + let col_size = (n_cols-1) * offset; + let x = space.x; + let y = space.y; + + if (space.layout_axis == 'X') { + x -= col_size * space.layout_major; + y -= row_size * space.layout_minor; + x += col * offset; + y += row * offset; + } else { + y -= col_size * space.layout_major; + x -= row_size * space.layout_minor; + y += col * offset; + x += row * offset; + } + + element.style.left = ((flip_x(x,y) - block_size/2)|0)+"px"; + element.style.top = ((flip_y(x,y) - block_size/2)|0)+"px"; +} + +function show_block(element) { + if (element.parentElement != ui.blocks_element) + ui.blocks_element.appendChild(element); +} + +function hide_block(element) { + if (element.parentElement != ui.offmap_element) + ui.offmap_element.appendChild(element); +} + +function show_block(element) { + if (element.parentElement != ui.blocks_element) + ui.blocks_element.appendChild(element); +} + +function hide_block(element) { + if (element.parentElement != ui.offmap_element) + ui.offmap_element.appendChild(element); +} + +function update_map() { + let overflow = { Frank: [], Saracen: [], Assassins: [] }; + let layout = {}; + + document.getElementById("turn").textContent = "Year " + game.year + " (" + (game.year-1186) + "/6)" ; + + for (let town in TOWNS) + layout[town] = { secret: [], known: [] }; + + // Move secret blocks to overflow queue if there are too many in a town + for (let town in TOWNS) { + for (let color of [FRANK, SARACEN, ASSASSINS]) { + if (game.secret[color]) { + let max = game.secret[color][town] ? game.secret[color][town][0] : 0; + while (ui.secret[color][town].length > max) { + overflow[color].push(ui.secret[color][town].pop()); + } + } + } + } + + // Add secret blocks if there are too few in a location + for (let town in TOWNS) { + for (let color of [FRANK, SARACEN, ASSASSINS]) { + if (game.secret[color]) { + let max = game.secret[color][town] ? game.secret[color][town][0] : 0; + while (ui.secret[color][town].length < max) { + if (overflow[color].length > 0) { + ui.secret[color][town].push(overflow[color].pop()); + } else { + let element = ui.secret[color].offmap.pop(); + show_block(element); + ui.secret[color][town].push(element); + } + } + } + } + } + + // Remove any blocks left in the overflow queue + for (let color of [FRANK, SARACEN, ASSASSINS]) { + while (overflow[color].length > 0) { + let element = overflow[color].pop(); + hide_block(element); + ui.secret[color].offmap.push(element); + } + } + + // Hide formerly known blocks + for (let b in BLOCKS) { + if (!(b in game.known)) { + hide_block(ui.known[b]); + } + } + + // Add secret blocks to layout + for (let town in TOWNS) { + for (let color of [FRANK, SARACEN, ASSASSINS]) { + let i = 0, n = 0, m = 0; + if (game.secret[color] && game.secret[color][town]) { + n = game.secret[color][town][0]; + m = game.secret[color][town][1]; + } + for (let element of ui.secret[color][town]) { + if (i++ < n - m) + element.classList.remove("moved"); + else + element.classList.add("moved"); + layout[town].secret.push(element); + } + } + } + + // Add known blocks to layout + for (let b in game.known) { + let town = game.known[b][0]; + if (town) { + let steps = game.known[b][1]; + let moved = game.known[b][2]; + let element = ui.known[b]; + + show_block(element); + layout[town].known.push(element); + update_steps(b, steps, element); + + if (moved) + element.classList.add("moved"); + else + element.classList.remove("moved"); + } + } + + // Layout blocks on map + for (let town in TOWNS) + layout_blocks(town, layout[town].secret, layout[town].known); + + for (let where in TOWNS) { + if (ui.towns[where]) { + ui.towns[where].classList.remove('highlight'); + ui.towns[where].classList.remove('where'); + } + } + if (game.actions && game.actions.town) + for (let where of game.actions.town) + ui.towns[where].classList.add('highlight'); + if (game.where) + ui.towns[game.where].classList.add('where'); + + for (let b in BLOCKS) { + ui.known[b].classList.remove('highlight'); + ui.known[b].classList.remove('selected'); + } + if (!game.battle) { + if (game.actions && game.actions.block) + for (let b of game.actions.block) + ui.known[b].classList.add('highlight'); + if (game.who) + ui.known[game.who].classList.add('selected'); + } +} + +function update_cards() { + let cards = game.hand; + for (let c = 1; c <= 27; ++c) { + ui.cards[c].classList.remove('enabled'); + if (cards && cards.includes(c)) + ui.cards[c].classList.add('show'); + else + ui.cards[c].classList.remove('show'); + } + + if (game.actions && game.actions.play) { + for (let c of game.actions.play) + ui.cards[c].classList.add('enabled'); + } + + if (!game.f_card) + document.querySelector("#frank_card").className = "small_card card_back"; + else + document.querySelector("#frank_card").className = "small_card " + CARDS[game.f_card].image; + if (!game.s_card) + document.querySelector("#saracen_card").className = "small_card card_back"; + else + document.querySelector("#saracen_card").className = "small_card " + CARDS[game.s_card].image; +} + +function update_battle() { + function fill_cell(name, list, reserve) { + let cell = window[name]; + + ui.present.clear(); + + for (let [block, steps, moved] of list) { + ui.present.add(block); + + if (block == game.who) + ui.battle_block[block].classList.add("selected"); + else + ui.battle_block[block].classList.remove("selected"); + + ui.battle_block[block].classList.remove("highlight"); + ui.battle_menu[block].classList.remove('hit'); + ui.battle_menu[block].classList.remove('charge'); + ui.battle_menu[block].classList.remove('fire'); + ui.battle_menu[block].classList.remove('harry'); + ui.battle_menu[block].classList.remove('retreat'); + + if (game.actions && game.actions.block && game.actions.block.includes(block)) + ui.battle_block[block].classList.add("highlight"); + if (game.actions && game.actions.battle_fire && game.actions.battle_fire.includes(block)) + ui.battle_menu[block].classList.add('fire'); + if (game.actions && game.actions.battle_retreat && game.actions.battle_retreat.includes(block)) + ui.battle_menu[block].classList.add('retreat'); + if (game.actions && game.actions.battle_harry && game.actions.battle_harry.includes(block)) + ui.battle_menu[block].classList.add('harry'); + if (game.actions && game.actions.battle_charge && game.actions.battle_charge.includes(block)) + ui.battle_menu[block].classList.add('charge'); + if (game.actions && game.actions.battle_hit && game.actions.battle_hit.includes(block)) + ui.battle_menu[block].classList.add('hit'); + if (game.actions && game.actions.battle_charge && game.actions.battle_charge.includes(block)) + ui.battle_menu[block].classList.add('charge'); + if (game.actions && game.actions.battle_treachery && game.actions.battle_treachery.includes(block)) + ui.battle_menu[block].classList.add('treachery'); + + update_steps(block, steps, ui.battle_block[block], false); + if (reserve) + ui.battle_block[block].classList.add("secret"); + else + ui.battle_block[block].classList.remove("secret"); + if (moved) + ui.battle_block[block].classList.add("moved"); + else + ui.battle_block[block].classList.remove("moved"); + if (reserve) + ui.battle_block[block].classList.remove("known"); + else + ui.battle_block[block].classList.add("known"); + } + + for (let b in BLOCKS) { + if (ui.present.has(b)) { + if (!cell.contains(ui.battle_menu[b])) + cell.appendChild(ui.battle_menu[b]); + } else { + if (cell.contains(ui.battle_menu[b])) + cell.removeChild(ui.battle_menu[b]); + } + } + } + + if (ui.player == FRANK) { + fill_cell("FR", game.battle.FR, true); + fill_cell("FA", game.battle.FA, false); + fill_cell("FB", game.battle.FB, false); + fill_cell("FC", game.battle.FC, false); + fill_cell("EA", game.battle.SA, false); + fill_cell("EB", game.battle.SB, false); + fill_cell("EC", game.battle.SC, false); + fill_cell("ER", game.battle.SR, true); + } else { + fill_cell("ER", game.battle.FR, true); + fill_cell("EA", game.battle.FA, false); + fill_cell("EB", game.battle.FB, false); + fill_cell("EC", game.battle.FC, false); + fill_cell("FA", game.battle.SA, false); + fill_cell("FB", game.battle.SB, false); + fill_cell("FC", game.battle.SC, false); + fill_cell("FR", game.battle.SR, true); + } +} + +function on_update(state, player) { + game = state; + + show_action_button("#pass_button", "pass"); + show_action_button("#undo_button", "undo"); + show_action_button("#group_move_button", "group_move"); + show_action_button("#sea_move_button", "sea_move"); + show_action_button("#muster_button", "muster"); + show_action_button("#end_muster_button", "end_muster"); + show_action_button("#end_move_phase_button", "end_move_phase"); + show_action_button("#end_regroup_button", "end_regroup"); + show_action_button("#end_retreat_button", "end_retreat"); + show_action_button("#eliminate_button", "eliminate"); + + document.getElementById("frank_vp").textContent = game.f_vp; + document.getElementById("saracen_vp").textContent = game.s_vp; + + update_cards(); + update_map(); + + if (game.battle) { + document.querySelector(".battle_header").textContent = game.battle.title; + document.querySelector(".battle_message").textContent = game.battle.flash; + document.querySelector(".battle").classList.add("show"); + update_battle(player); + } else { + document.querySelector(".battle").classList.remove("show"); + } +} + +build_map(); + +document.querySelector(".map").classList.add(map_orientation); + +drag_element_with_mouse(".battle", ".battle_header"); +scroll_with_middle_mouse(".grid_center", 3); +init_map_zoom(); +init_shift_zoom(); +init_client(["Frank", "Saracen"]); |