From b58d9ffdcb67564d751ad5a3206744be527063ee Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Sat, 12 Nov 2022 16:30:14 +0100 Subject: Start code. --- about.html | 4 +- data.js | 227 +++++++++++ play.html | 993 +++++++++++++++++++++++++++++++++++++++++++++++ play.js | 729 ++++++++++++++++++++++++++++++++++ rules.js | 1140 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ tools/gendata.js | 172 ++++++-- 6 files changed, 3231 insertions(+), 34 deletions(-) create mode 100644 data.js create mode 100644 play.html create mode 100644 play.js create mode 100644 rules.js diff --git a/about.html b/about.html index 5e0a726..818c243 100644 --- a/about.html +++ b/about.html @@ -19,9 +19,7 @@ Copyright © 2019 GMT Games, LLC. diff --git a/data.js b/data.js new file mode 100644 index 0000000..14da562 --- /dev/null +++ b/data.js @@ -0,0 +1,227 @@ +const data = { +seaports:[0,2,8,9,15,29,30,34], +locales:[ +{"name":"Reval","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Danish Estonia","ways":[[5,25],[3,31]],"box":{"x":601,"y":3564,"w":206,"h":91}}, +{"name":"Wesenberg","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Danish Estonia","ways":[[5,26],[17,30]],"box":{"x":1448,"y":3625,"w":304,"h":60}}, +{"name":"Narwia","type":"town","stronghold":0,"walls":0,"vp":0.5,"region":"Danish Estonia","ways":[[7,0],[38,0],[49,0],[46,17],[6,27],[33,28]],"box":{"x":2371,"y":3549,"w":123,"h":31}}, +{"name":"Warbola","type":"town","stronghold":0,"walls":0,"vp":0.5,"region":"Danish Estonia","ways":[[0,31],[4,32],[8,34]],"box":{"x":292,"y":3797,"w":142,"h":31}}, +{"name":"Harrien","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Danish Estonia","ways":[[3,32],[17,33]],"box":{"x":567,"y":3983,"w":200,"h":100}}, +{"name":"Revala","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Danish Estonia","ways":[[0,25],[1,26]],"box":{"x":1030,"y":3410,"w":200,"h":100}}, +{"name":"Wierland","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Danish Estonia","ways":[[2,27],[23,29]],"box":{"x":1999,"y":3680,"w":200,"h":100}}, +{"name":"Dorpat","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Crusader Livonia","ways":[[2,0],[38,0],[49,0],[11,2],[12,2,3],[22,3],[23,35]],"box":{"x":1625,"y":4589,"w":253,"h":91}}, +{"name":"Leal","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Crusader Livonia","ways":[[3,34],[15,36]],"box":{"x":108,"y":4266,"w":205,"h":91}}, +{"name":"Riga","type":"bishopric","stronghold":3,"walls":4,"vp":2,"region":"Crusader Livonia","ways":[[13,8]],"box":{"x":273,"y":6231,"w":205,"h":91}}, +{"name":"Adsel","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[13,9],[18,43],[21,43],[14,44]],"box":{"x":1504,"y":5612,"w":185,"h":60}}, +{"name":"Fellin","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[7,2],[12,2],[15,7],[17,7],[20,37]],"box":{"x":1013,"y":4583,"w":184,"h":61}}, +{"name":"Odenpäh","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[7,2,3],[11,2],[22,3],[14,45]],"box":{"x":1378,"y":5103,"w":250,"h":61}}, +{"name":"Wenden","type":"castle","stronghold":2,"walls":4,"vp":1,"region":"Crusader Livonia","ways":[[9,8],[10,9],[19,39],[21,40]],"box":{"x":909,"y":5759,"w":232,"h":60}}, +{"name":"Kirrumpäh","type":"town","stronghold":0,"walls":0,"vp":0.5,"region":"Crusader Livonia","ways":[[10,44],[12,45],[32,46]],"box":{"x":1877,"y":5389,"w":175,"h":30}}, +{"name":"Pernau","type":"town","stronghold":0,"walls":0,"vp":0.5,"region":"Crusader Livonia","ways":[[11,7],[17,7],[8,36]],"box":{"x":517,"y":4580,"w":118,"h":30}}, +{"name":"Rositten","type":"town","stronghold":0,"walls":0,"vp":0.5,"region":"Crusader Livonia","ways":[[21,41],[18,42]],"box":{"x":2046,"y":6307,"w":146,"h":30}}, +{"name":"Jerwen","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Crusader Livonia","ways":[[11,7],[15,7],[1,30],[4,33]],"box":{"x":1064,"y":3946,"w":200,"h":100}}, +{"name":"Lettgallia","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Crusader Livonia","ways":[[16,42],[10,43],[21,43],[39,49],[32,50]],"box":{"x":2048,"y":5777,"w":200,"h":100}}, +{"name":"Metsepole","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Crusader Livonia","ways":[[20,38],[13,39]],"box":{"x":509,"y":5226,"w":200,"h":100}}, +{"name":"Sackala","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Crusader Livonia","ways":[[11,37],[19,38]],"box":{"x":617,"y":4769,"w":200,"h":100}}, +{"name":"Tolowa","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Crusader Livonia","ways":[[13,40],[16,41],[10,43],[18,43]],"box":{"x":1541,"y":5933,"w":200,"h":100}}, +{"name":"Ugaunia","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Crusader Livonia","ways":[[7,3],[12,3],[49,47],[32,48]],"box":{"x":1957,"y":4940,"w":200,"h":100}}, +{"name":"Waiga","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Crusader Livonia","ways":[[6,29],[7,35]],"box":{"x":1535,"y":4113,"w":200,"h":100}}, +{"name":"Novgorod","type":"archbishopric","stronghold":3,"walls":3,"vp":3,"region":"Novgorodan Rus","ways":[[27,6],[47,6],[31,23],[41,61],[40,62]],"box":{"x":4318,"y":4315,"w":333,"h":112}}, +{"name":"Ladoga","type":"city","stronghold":3,"walls":3,"vp":2,"region":"Novgorodan Rus","ways":[[31,22],[30,24],[44,58]],"box":{"x":4619,"y":2817,"w":238,"h":90}}, +{"name":"Pskov","type":"city","stronghold":3,"walls":3,"vp":2,"region":"Novgorodan Rus","ways":[[49,4],[39,10],[32,51],[52,67],[37,68]],"box":{"x":2680,"y":5263,"w":205,"h":91}}, +{"name":"Rusa","type":"city","stronghold":3,"walls":3,"vp":2,"region":"Novgorodan Rus","ways":[[24,6],[47,6],[28,13]],"box":{"x":4329,"y":5166,"w":205,"h":92}}, +{"name":"Lovat","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[27,13],[36,14]],"box":{"x":4243,"y":5581,"w":187,"h":63}}, +{"name":"Luga","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[33,18]],"box":{"x":2667,"y":3295,"w":148,"h":62}}, +{"name":"Neva","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[25,24],[51,54],[45,56],[44,57]],"box":{"x":3924,"y":2934,"w":148,"h":62}}, +{"name":"Volkhov","type":"traderoute","stronghold":0,"walls":0,"vp":1,"region":"Novgorodan Rus","ways":[[25,22],[24,23]],"box":{"x":4591,"y":3783,"w":231,"h":63}}, +{"name":"Izborsk","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[14,46],[22,48],[18,50],[26,51]],"box":{"x":2240,"y":5431,"w":241,"h":62}}, +{"name":"Kaibolovo","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[29,18],[42,19],[2,28],[34,52]],"box":{"x":2904,"y":3522,"w":285,"h":62}}, +{"name":"Koporye","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[33,52],[51,53]],"box":{"x":3133,"y":3160,"w":241,"h":62}}, +{"name":"Porkhov","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[47,16],[37,69],[48,70]],"box":{"x":3515,"y":5467,"w":241,"h":63}}, +{"name":"Velikiye Luki","type":"fort","stronghold":1,"walls":3,"vp":1,"region":"Novgorodan Rus","ways":[[28,14],[50,71]],"box":{"x":3706,"y":6347,"w":351,"h":61}}, +{"name":"Dubrovno","type":"town","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[47,15],[26,68],[35,69]],"box":{"x":3153,"y":5214,"w":161,"h":31}}, +{"name":"Gdov","type":"town","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[2,0],[7,0],[49,0,1],[46,64]],"box":{"x":2427,"y":4149,"w":88,"h":30}}, +{"name":"Ostrov","type":"town","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[26,10],[50,11],[18,49]],"box":{"x":2746,"y":5717,"w":115,"h":30}}, +{"name":"Sablia","type":"town","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[42,20],[24,62],[47,63]],"box":{"x":3788,"y":4541,"w":104,"h":31}}, +{"name":"Tesovo","type":"town","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[42,21],[43,60],[24,61]],"box":{"x":3936,"y":4102,"w":121,"h":32}}, +{"name":"Zheltsy","type":"town","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[33,19],[40,20],[41,21],[46,65]],"box":{"x":3501,"y":4176,"w":128,"h":30}}, +{"name":"Ingria","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[51,55],[44,59],[41,60]],"box":{"x":3820,"y":3639,"w":200,"h":100}}, +{"name":"Izhora","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[30,57],[25,58],[43,59]],"box":{"x":4074,"y":3323,"w":200,"h":100}}, +{"name":"Karelia","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[30,56]],"box":{"x":3833,"y":2408,"w":200,"h":100}}, +{"name":"Plyussa River","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[2,17],[38,64],[42,65],[52,66]],"box":{"x":2829,"y":4234,"w":200,"h":100}}, +{"name":"Shelon River","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[24,6],[27,6],[37,15],[35,16],[40,63]],"box":{"x":3654,"y":4864,"w":200,"h":100}}, +{"name":"Sorot River","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[50,12],[35,70]],"box":{"x":3299,"y":5781,"w":200,"h":100}}, +{"name":"Uzmen","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[2,0],[7,0],[38,0,1],[26,4],[52,5],[22,47]],"box":{"x":2112,"y":4692,"w":200,"h":100}}, +{"name":"Velikaya River","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[39,11],[48,12],[36,71]],"box":{"x":3029,"y":6090,"w":200,"h":100}}, +{"name":"Vod","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[34,53],[30,54],[43,55]],"box":{"x":3488,"y":3345,"w":200,"h":100}}, +{"name":"Zhelcha River","type":"region","stronghold":0,"walls":0,"vp":0.5,"region":"Novgorodan Rus","ways":[[49,5],[46,66],[26,67]],"box":{"x":2782,"y":4586,"w":200,"h":100}}, +], +ways:[ +{"type":"waterway","locales":[2,7,38,49],"name":"Pleipat W"}, +{"type":"waterway","locales":[38,49],"name":"Pleipat E"}, +{"type":"waterway","locales":[7,11,12],"name":"Wirz"}, +{"type":"trackway","locales":[7,12,22],"name":"Crossroads"}, +{"type":"waterway","locales":[26,49]}, +{"type":"waterway","locales":[49,52]}, +{"type":"waterway","locales":[24,27,47]}, +{"type":"waterway","locales":[11,15,17]}, +{"type":"waterway","locales":[9,13]}, +{"type":"waterway","locales":[10,13]}, +{"type":"waterway","locales":[26,39]}, +{"type":"waterway","locales":[39,50]}, +{"type":"waterway","locales":[48,50]}, +{"type":"waterway","locales":[27,28]}, +{"type":"waterway","locales":[28,36]}, +{"type":"waterway","locales":[37,47]}, +{"type":"waterway","locales":[35,47]}, +{"type":"waterway","locales":[2,46]}, +{"type":"waterway","locales":[29,33]}, +{"type":"waterway","locales":[33,42]}, +{"type":"waterway","locales":[40,42]}, +{"type":"waterway","locales":[41,42]}, +{"type":"waterway","locales":[25,31]}, +{"type":"waterway","locales":[24,31]}, +{"type":"waterway","locales":[25,30]}, +{"type":"trackway","locales":[0,5]}, +{"type":"trackway","locales":[1,5]}, +{"type":"trackway","locales":[2,6]}, +{"type":"trackway","locales":[2,33]}, +{"type":"trackway","locales":[6,23]}, +{"type":"trackway","locales":[1,17]}, +{"type":"trackway","locales":[0,3]}, +{"type":"trackway","locales":[3,4]}, +{"type":"trackway","locales":[4,17]}, +{"type":"trackway","locales":[3,8]}, +{"type":"trackway","locales":[7,23]}, +{"type":"trackway","locales":[8,15]}, +{"type":"trackway","locales":[11,20]}, +{"type":"trackway","locales":[19,20]}, +{"type":"trackway","locales":[13,19]}, +{"type":"trackway","locales":[13,21]}, +{"type":"trackway","locales":[16,21]}, +{"type":"trackway","locales":[16,18]}, +{"type":"trackway","locales":[10,18,21]}, +{"type":"trackway","locales":[10,14]}, +{"type":"trackway","locales":[12,14]}, +{"type":"trackway","locales":[14,32]}, +{"type":"trackway","locales":[22,49]}, +{"type":"trackway","locales":[22,32]}, +{"type":"trackway","locales":[18,39]}, +{"type":"trackway","locales":[18,32]}, +{"type":"trackway","locales":[26,32]}, +{"type":"trackway","locales":[33,34]}, +{"type":"trackway","locales":[34,51]}, +{"type":"trackway","locales":[30,51]}, +{"type":"trackway","locales":[43,51]}, +{"type":"trackway","locales":[30,45]}, +{"type":"trackway","locales":[30,44]}, +{"type":"trackway","locales":[25,44]}, +{"type":"trackway","locales":[43,44]}, +{"type":"trackway","locales":[41,43]}, +{"type":"trackway","locales":[24,41]}, +{"type":"trackway","locales":[24,40]}, +{"type":"trackway","locales":[40,47]}, +{"type":"trackway","locales":[38,46]}, +{"type":"trackway","locales":[42,46]}, +{"type":"trackway","locales":[46,52]}, +{"type":"trackway","locales":[26,52]}, +{"type":"trackway","locales":[26,37]}, +{"type":"trackway","locales":[35,37]}, +{"type":"trackway","locales":[35,48]}, +{"type":"trackway","locales":[36,50]}, +], +lords:[ +{"side":"Teutonic","name":"Andreas","full_name":"Andreas von Felben","title":"Landmeister in Livonia","seats":[9,13],"marshal":2,"fealty":2,"service":4,"lordship":3,"command":3,"forces":{"knights":1,"sergeants":2,"men_at_arms":1},"assets":{"transport":2,"prov":2},"ships":1,"vassals":[0,1,2],"image":0}, +{"side":"Teutonic","name":"Heinrich","full_name":"Heinrich","title":"Bishop of Ösel-Wiek","seats":[8],"marshal":0,"fealty":3,"service":4,"lordship":2,"command":1,"forces":{"knights":1,"sergeants":1,"men_at_arms":1},"assets":{"ship":1,"coin":2,"prov":1},"ships":1,"vassals":[3,4],"image":1}, +{"side":"Teutonic","name":"Hermann","full_name":"Hermann","title":"Bishop of Dorpat","seats":[7,12],"marshal":1,"fealty":4,"service":4,"lordship":3,"command":3,"forces":{"knights":1,"sergeants":1,"men_at_arms":1,"militia":1},"assets":{"transport":1,"coin":1,"prov":1},"ships":0,"vassals":[5,6,7],"image":2}, +{"side":"Teutonic","name":"Knud & Abel","full_name":"Knud & Abel","title":"Princes of Denmark","seats":[0,null],"marshal":0,"fealty":2,"service":3,"lordship":3,"command":2,"forces":{"knights":1,"sergeants":1,"men_at_arms":2,"militia":1},"assets":{"ship":2,"prov":2},"ships":1,"vassals":[8,9,10],"image":3}, +{"side":"Teutonic","name":"Rudolf","full_name":"Rudolf von Kassel","title":"Castellan of Wenden","seats":[13],"marshal":0,"fealty":5,"service":2,"lordship":1,"command":3,"forces":{"knights":1,"sergeants":1,"men_at_arms":1},"assets":{"transport":1,"prov":1},"ships":0,"vassals":[11,12,13],"image":4}, +{"side":"Teutonic","name":"Yaroslav","full_name":"Yaroslav","title":"Exile of Pskov","seats":[12,26],"marshal":0,"fealty":4,"service":2,"lordship":1,"command":2,"forces":{"knights":1,"light_horse":1,"men_at_arms":1},"assets":{"transport":1,"prov":1},"ships":0,"vassals":[14],"image":5}, +{"side":"Russian","name":"Aleksandr","full_name":"Aleksandr","title":"Prince of Novgorod","seats":[24,27],"marshal":2,"fealty":0,"service":6,"lordship":4,"command":3,"forces":{"knights":3,"men_at_arms":2},"assets":{"transport":2},"ships":1,"vassals":[15,16,17,18,19],"image":0}, +{"side":"Russian","name":"Andrey","full_name":"Andrey","title":"Prince of Suzdal","seats":[24,27],"marshal":1,"fealty":4,"service":5,"lordship":3,"command":2,"forces":{"knights":3,"men_at_arms":2},"assets":{"transport":2},"ships":1,"vassals":[20,21,22,23],"image":1}, +{"side":"Russian","name":"Domash","full_name":"Domash","title":"Tysyatskiy of Novgorod","seats":[24],"marshal":0,"fealty":4,"service":4,"lordship":2,"command":2,"forces":{"sergeants":1,"light_horse":1,"men_at_arms":2,"militia":1},"assets":{"transport":4,"prov":4},"ships":1,"vassals":[24,25,26],"image":2}, +{"side":"Russian","name":"Gavrilo","full_name":"Gavrilo","title":"Voyevoda of Pskov","seats":[26],"marshal":0,"fealty":3,"service":4,"lordship":3,"command":2,"forces":{"knights":1,"light_horse":1,"men_at_arms":1,"militia":1},"assets":{"transport":2,"coin":1,"prov":2},"ships":1,"vassals":[27,28,29],"image":3}, +{"side":"Russian","name":"Karelians","full_name":"Karelians","title":"Tributaries of Novgorod","seats":[25],"marshal":0,"fealty":4,"service":2,"lordship":1,"command":2,"forces":{"light_horse":1,"militia":4},"assets":{"transport":1},"ships":1,"vassals":[],"image":4}, +{"side":"Russian","name":"Vladislav","full_name":"Vladislav","title":"Bailiff of Ladoga","seats":[25],"marshal":0,"fealty":5,"service":3,"lordship":2,"command":3,"forces":{"sergeants":1,"light_horse":1,"men_at_arms":2},"assets":{"transport":1,"prov":1},"ships":1,"vassals":[30,31,32,33],"image":5}, +], +vassals:[ +{"lord":0,"name":"Lettgallian Auxiliaries","service":1,"forces":{"light_horse":1,"militia":1},"image":0}, +{"lord":0,"name":"Summer Crusaders","service":2,"forces":{"knights":3},"capability":"Crusade","image":1}, +{"lord":0,"name":"Teutonic Vassals","service":3,"forces":{"knights":1,"men_at_arms":2},"image":2}, +{"lord":1,"name":"Heinrich von Lode","service":2,"forces":{"knights":1,"men_at_arms":1},"image":3}, +{"lord":1,"name":"Odward von Lode","service":2,"forces":{"knights":1,"men_at_arms":1},"image":4}, +{"lord":2,"name":"Helmond von Lüneburg","service":2,"forces":{"knights":1,"men_at_arms":1},"image":5}, +{"lord":2,"name":"Johannes von Dolen","service":2,"forces":{"knights":1,"men_at_arms":1},"image":6}, +{"lord":2,"name":"Ugaunian Auxiliaries","service":1,"forces":{"light_horse":1,"militia":1},"image":7}, +{"lord":3,"name":"Dietrich von Kivel","service":2,"forces":{"knights":1,"men_at_arms":1},"image":8}, +{"lord":3,"name":"Estonian Auxiliaries","service":1,"forces":{"light_horse":1,"militia":1},"image":9}, +{"lord":3,"name":"Otto von Lüneburg","service":2,"forces":{"knights":1,"men_at_arms":1},"image":10}, +{"lord":4,"name":"Ex-Sword Brethren","service":2,"forces":{"knights":1,"sergeants":1},"image":11}, +{"lord":4,"name":"Jerwen Teutonic Vassals","service":2,"forces":{"knights":1,"men_at_arms":1},"image":12}, +{"lord":4,"name":"Summer Crusaders","service":2,"forces":{"knights":2},"capability":"Crusade","image":13}, +{"lord":5,"name":"Mstislavich Partisans","service":1,"forces":{"militia":2},"image":14}, +{"lord":6,"name":"Mongols","service":3,"forces":{"asiatic_horse":2},"capability":"Steppe Warriors","image":0}, +{"lord":6,"name":"Mongols","service":3,"forces":{"asiatic_horse":2},"capability":"Steppe Warriors","image":1}, +{"lord":6,"name":"Pereyaslavl","service":4,"forces":{"men_at_arms":1},"image":1}, +{"lord":6,"name":"Rostov","service":3,"forces":{"men_at_arms":1},"image":2}, +{"lord":6,"name":"Yaroslavl","service":3,"forces":{"men_at_arms":1},"image":3}, +{"lord":7,"name":"Kipchaqs","service":3,"forces":{"asiatic_horse":3},"capability":"Steppe Warriors","image":4}, +{"lord":7,"name":"Kipchaqs","service":3,"forces":{"asiatic_horse":3},"capability":"Steppe Warriors","image":5}, +{"lord":7,"name":"Suzdal","service":4,"forces":{"men_at_arms":1},"image":5}, +{"lord":7,"name":"Vladimir","service":4,"forces":{"men_at_arms":1},"image":6}, +{"lord":8,"name":"Novgorod","service":2,"forces":{"militia":2},"image":7}, +{"lord":8,"name":"Novgorod","service":2,"forces":{"militia":2},"image":8}, +{"lord":8,"name":"Novgorod","service":2,"forces":{"militia":2},"image":8}, +{"lord":9,"name":"Borderland Russians","service":1,"forces":{"light_horse":1,"militia":1},"image":8}, +{"lord":9,"name":"Pskov Militia","service":2,"forces":{"militia":2},"image":9}, +{"lord":9,"name":"Pskov","service":4,"forces":{"men_at_arms":1},"image":10}, +{"lord":11,"name":"Izhoran Auxiliaries","service":1,"forces":{"militia":1},"image":11}, +{"lord":11,"name":"Ingrian Auxiliaries","service":1,"forces":{"militia":1},"image":12}, +{"lord":11,"name":"Vepsian Auxiliaries","service":1,"forces":{"militia":1},"image":13}, +{"lord":11,"name":"Vodian Auxiliaries","service":1,"forces":{"militia":1},"image":14}, +], +cards:[ +{"name":"T1","event":"Grand Prince","capability":"Treaty of Stensby","lords":[1,3]}, +{"name":"T2","event":"Torzhok","capability":"Raiders","lords":[0,1,2,3,4,5]}, +{"name":"T3","event":"Vodian Treachery","capability":"Converts","lords":[0,1,2,3,4,5]}, +{"name":"T4","event":"Bridge","capability":"Balistarii","lords":[0,1,2,3,4,5]}, +{"name":"T5","event":"Marsh","capability":"Balistarii","lords":[0,1,2,3,4,5]}, +{"name":"T6","event":"Ambush","capability":"Balistarii","lords":[0,1,2,3,4,5]}, +{"name":"T7","event":"Tverdilo","capability":"Warrior Monks","lords":[0,4]}, +{"name":"T8","event":"Teutonic Fervor","capability":"Hillforts","lords":null}, +{"name":"T9","event":"Hill","capability":"Halbbrüder","lords":[0,4]}, +{"name":"T10","event":"Field Organ","capability":"Halbbrüder","lords":[0,4]}, +{"name":"T11","event":"Pope Gregory","capability":"Crusade","lords":[0,4]}, +{"name":"T12","event":"Khan Baty","capability":"Ordensburgen","lords":null}, +{"name":"T13","event":"Heinrich Sees the Curia","capability":"William of Modena","lords":null}, +{"name":"T14","event":"Bountiful Harvest","capability":"Trebuchets","lords":[0,1,2,3,4,5]}, +{"name":"T15","event":"Mindaugas","capability":"Warrior Monks","lords":[0,4]}, +{"name":"T16","event":"Famine","capability":"Ransom","lords":null}, +{"name":"T17","event":"Dietrich von Grüningen","capability":"Stonemasons","lords":[0,1,2,3,4,5]}, +{"name":"T18","event":"Swedish Crusade","capability":"Cogs","lords":[0,1,3]}, +{"name":"TNo","event":"No Event","capability":null,"lords":null}, +{"name":"TNo","event":"No Event","capability":null,"lords":null}, +{"name":"TNo","event":"No Event","capability":null,"lords":null}, +{"name":"R1","event":"Bridge","capability":"Luchniki","lords":[8,9,10,11]}, +{"name":"R2","event":"Marsh","capability":"Luchniki","lords":[8,9,10,11]}, +{"name":"R3","event":"Pogost","capability":"Streltsy","lords":[6,7,8,9,11]}, +{"name":"R4","event":"Raven's Rock","capability":"Smerdi","lords":null}, +{"name":"R5","event":"Hill","capability":"Druzhina","lords":[6,7,9]}, +{"name":"R6","event":"Ambush","capability":"Druzhina","lords":[6,7,9]}, +{"name":"R7","event":"Famine","capability":"Ransom","lords":null}, +{"name":"R8","event":"Prince of Polotsk","capability":"Black Sea Trade","lords":null}, +{"name":"R9","event":"Osilian Revolt","capability":"Baltic Sea Trade","lords":null}, +{"name":"R10","event":"Batu Khan","capability":"Steppe Warriors","lords":[6,7]}, +{"name":"R11","event":"Valdemar","capability":"House of Suzdal","lords":[6,7]}, +{"name":"R12","event":"Mindaugas","capability":"Raiders","lords":[6,7,8,9,10,11]}, +{"name":"R13","event":"Pelgui","capability":"Streltsy","lords":[6,7,8,9,11]}, +{"name":"R14","event":"Prussian Revolt","capability":"Raiders","lords":[6,7,8,9,10,11]}, +{"name":"R15","event":"Death of the Pope","capability":"Archbishopric","lords":[6,7,8,9,10,11]}, +{"name":"R16","event":"Tempest","capability":"Lodya","lords":[6,7,8,9,10,11]}, +{"name":"R17","event":"Dietrich von Grüningen","capability":"Veliky Knyaz","lords":[6,7,8,9,10,11]}, +{"name":"R18","event":"Bountiful Harvest","capability":"Stone Kremlin","lords":[6,7,8,9,10,11]}, +{"name":"RNo","event":"No Event","capability":null,"lords":null}, +{"name":"RNo","event":"No Event","capability":null,"lords":null}, +{"name":"RNo","event":"No Event","capability":null,"lords":null}, +], +} +if (typeof module !== 'undefined') module.exports = data diff --git a/play.html b/play.html new file mode 100644 index 0000000..6b6026f --- /dev/null +++ b/play.html @@ -0,0 +1,993 @@ + + + + + + +NEVSKY + + + + + + + + + + +
+
Arts of War
+
+
+ +
+
+ +
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+ +
+ + + + diff --git a/play.js b/play.js new file mode 100644 index 0000000..e6f05dd --- /dev/null +++ b/play.js @@ -0,0 +1,729 @@ +"use strict" + +const MAP_DPI = 75 + +const round = Math.round +const floor = Math.floor +const ceil = Math.ceil + +function pack1_get(word, n) { + return (word >>> n) & 1 +} + +function pack4_get(word, n) { + n = n << 2 + return (word >>> n) & 15 +} + +function is_lord_action(lord) { + return !!(view.actions && view.actions.lord && view.actions.lord.includes(lord)) +} + +function is_service_action(lord) { + return !!(view.actions && view.actions.service && view.actions.service.includes(lord)) +} + +function is_vassal_action(vassal) { + return !!(view.actions && view.actions.vassal && view.actions.vassal.includes(vassal)) +} + +function is_locale_action(locale) { + return !!(view.actions && view.actions.locale && view.actions.locale.includes(locale)) +} + +const force_type_count = 7 +const force_type_name = [ "knights", "sergeants", "light_horse", "asiatic_horse", "men_at_arms", "militia", "serfs" ] + +const asset_type_count = 7 +const asset_type_name = [ "prov", "coin", "loot", "cart", "sled", "boat", "ship" ] +const asset_type_x3 = [ 1, 1, 1, 0, 0, 0, 0 ] + +const first_teutonic_region = 0 +const last_teutonic_region = 23 +const first_russian_region = 24 +const last_russian_region = 52 + +function is_teutonic_region(loc) { + return loc >= first_teutonic_region && loc <= last_teutonic_region +} + +function is_russian_region(loc) { + return loc >= first_russian_region && loc <= last_russian_region +} + +function count_teutonic_vp() { + let vp = 0 + for (let loc of view.conquered) + if (is_russian_region(loc)) + vp += data.locales[loc].vp << 1 + for (let loc of view.ravaged) + if (is_russian_region(loc)) + vp += 1 + return vp +} + +function count_russian_vp() { + let vp = view.veche_vp * 2 + for (let loc of view.conquered) + if (is_teutonic_region(loc)) + vp += data.locales[loc].vp << 1 + for (let loc of view.ravaged) + if (is_teutonic_region(loc)) + vp += 1 + return vp +} + +function is_card_in_use(c) { + if (view.global_cards.includes(c)) + return true + if (view.lords.cards.includes(c)) + return true + if (c === 18 || c === 19 || c === 20) + return true + if (c === 39 || c === 40 || c === 41) + return true + return false +} + +function for_each_teutonic_arts_of_war(fn) { + for (let i = 0; i < 21; ++i) + fn(i) +} + +function for_each_russian_arts_of_war(fn) { + for (let i = 21; i < 42; ++i) + fn(i) +} + +function for_each_friendly_arts_of_war(fn) { + if (player === "Teutons") + for_each_teutonic_arts_of_war(fn) + else + for_each_russian_arts_of_war(fn) +} + +function for_each_enemy_arts_of_war(fn) { + if (player !== "Teutons") + for_each_teutonic_arts_of_war(fn) + else + for_each_russian_arts_of_war(fn) +} + +const original_boxes = { + "way crossroads": [1500,4717,462,149], + "way wirz": [1295,4526,175,350], + "way peipus-east": [2232,4197,220,480], + "way peipus-north": [2053,3830,361,228], + // "way peipus-west": [1988,4141,218,520], + "calendar summer box1": [40,168,590,916], + "calendar summer box2": [650,168,590,916], + "calendar winter box3": [1313,168,590,916], + "calendar winter box4": [1922,168,590,916], + "calendar winter box5": [2587,168,590,916], + "calendar winter box6": [3196,168,590,916], + "calendar rasputitsa box7": [3860,168,590,916], + "calendar rasputitsa box8": [4470,168,590,916], + "calendar summer box9": [40,1120,590,916], + "calendar summer box10": [650,1120,590,916], + "calendar winter box11": [1313,1120,590,916], + "calendar winter box12": [1922,1120,590,916], + "calendar winter box13": [2587,1120,590,916], + "calendar winter box14": [3196,1120,590,916], + "calendar rasputitsa box15": [3860,1120,590,916], + "calendar rasputitsa box16": [4470,1120,590,916], + // "victory": [176,185,210,210], + // "turn": [402,185,210,210], +} + +const calendar_xy = [ + [0, 0], + [40,168], + [650,168], + [1313,168], + [1922,168], + [2587,168], + [3196,168], + [3860,168], + [4470,168], + [40,1120], + [650,1120], + [1313,1120], + [1922,1120], + [2587,1120], + [3196,1120], + [3860,1120], + [4470,1120], + [4470, 2052], +].map(([x,y])=>[x/4|0,y/4|0]) + +const locale_xy = [] + +const ui = { + locale: [], + locale_extra: [], + lord_cylinder: [], + lord_service: [], + lord_mat: [], + vassal_service: [], + forces: [], + routed: [], + assets: [], + c1: [], + c2: [], + arts_of_war: [], + boxes: {}, + veche: document.getElementById("veche"), + arts_of_war_dialog: document.getElementById("arts_of_war"), + arts_of_war_list: document.getElementById("arts_of_war_list"), + p1_global: document.getElementById("p1_global"), + p2_global: document.getElementById("p2_global"), + turn: document.getElementById("turn"), + vp1: document.getElementById("vp1"), + vp2: document.getElementById("vp2"), +} + +let locale_layout = new Array(data.locales.length).fill(0) +let calendar_layout = new Array(18).fill(0) + +function clean_name(name) { + return name.toLowerCase().replaceAll("&", "and").replaceAll(" ", "_") +} + +const extra_size_100 = { + town: [ 60, 42 ], + castle: [ 60, 42 ], + fort: [ 72, 42 ], + traderoute: [ 72, 42 ], + bishopric: [ 84, 60 ], + city: [ 132, 72 ], + archbishopric: [ 156, 96 ], +} + +const extra_size = { + town: [ 45, 32 ], + castle: [ 45, 32 ], + fort: [ 54, 32 ], + traderoute: [ 54, 32 ], + bishopric: [ 63, 45 ], + city: [ 100, 54 ], + archbishopric: [ 117, 72 ], +} + +function toggle_pieces() { + document.getElementById("pieces").classList.toggle("hide") +} + +function on_click_locale(evt) { + if (evt.button === 0) { + let id = evt.target.dataset.locale | 0 + send_action('locale', id) + } +} + +function on_focus_locale(evt) { + let id = evt.target.dataset.locale | 0 + document.getElementById("status").textContent = `(${id}) ${data.locales[id].name} - ${data.locales[id].type}` +} + +function on_click_cylinder(evt) { + if (evt.button === 0) { + let id = evt.target.dataset.lord | 0 + send_action('lord', id) + } +} + +function on_click_arts_of_war(evt) { +console.log("AOW CLICK", evt.target.dataset.arts_of_war) + if (evt.button === 0) { + let id = evt.target.dataset.arts_of_war | 0 + send_action('arts_of_war', id) + } +} + +function on_focus_cylinder(evt) { + let id = evt.target.dataset.lord | 0 + document.getElementById("status").textContent = `(${id}) ${data.lords[id].full_name} [${data.lords[id].command}] - ${data.lords[id].title}` +} + +function on_click_lord_service_marker(evt) { + if (evt.button === 0) { + let id = evt.target.dataset.lord | 0 + send_action('lord_service', id) + } +} + +function on_focus_lord_service_marker(evt) { + let id = evt.target.dataset.lord | 0 + document.getElementById("status").textContent = `(${id}) ${data.lords[id].full_name} - ${data.lords[id].title}` +} + +function on_click_vassal_service_marker(evt) { + if (evt.button === 0) { + let id = evt.target.dataset.vassal | 0 + send_action('vassal', id) + } +} + +function on_focus_vassal_service_marker(evt) { + let id = evt.target.dataset.vassal | 0 + let vassal = data.vassals[id] + let lord = data.lords[vassal.lord] + document.getElementById("status").textContent = `(${id}) ${lord.name} / ${vassal.name}` +} + +function on_blur(evt) { + document.getElementById("status").textContent = "" +} + +function on_focus_card_tip(c) { +} + +function on_blur_card_tip(c) { +} + +function sub_card_name(match, p1) { + let x = p1 | 0 + let n = data.cards[x].name + return `${n}` +} + +function on_focus_locale_tip(loc) { + ui.locale[loc].classList.add("tip") + ui.locale_extra[loc].classList.add("tip") +} + +function on_blur_locale_tip(loc) { + ui.locale[loc].classList.remove("tip") + ui.locale_extra[loc].classList.remove("tip") +} + +function on_click_locale_tip(loc) { + ui.locale[loc].scrollIntoView({ block:"center", inline:"center", behavior:"smooth" }) +} + +function sub_locale_name(match, p1) { + let x = p1 | 0 + let n = data.locales[x].name + return `${n}` +} + +function on_log(text) { + let p = document.createElement("div") + + if (text.match(/^>>/)) { + text = text.substring(2) + p.className = "ii" + } + + if (text.match(/^>/)) { + text = text.substring(1) + p.className = "i" + } + + text = text.replace(/&/g, "&") + text = text.replace(//g, ">") + + text = text.replace(/#(\d+)/g, sub_card_name) + text = text.replace(/%(\d+)/g, sub_locale_name) + + if (text.match(/^\.h1/)) { + text = text.substring(4) + p.className = "h1" + } + if (text.match(/^\.h2/)) { + text = text.substring(4) + if (text.startsWith("Teuton")) + p.className = "h2 teutonic" + else if (text.startsWith("Russian")) + p.className = "h2 russian" + else + p.className = "h2" + } + if (text.match(/^\.h3/)) { + text = text.substring(4) + p.className = "h3" + } + if (text.match(/^\.h4/)) { + text = text.substring(4) + p.className = "h4" + } + + p.innerHTML = text + return p +} + +function layout_locale_item(loc, e) { + let [x, y] = locale_xy[loc] + x += locale_layout[loc] * (46 + 6) + e.style.top = (y - 23) + "px" + e.style.left = (x - 23) + "px" + locale_layout[loc] ++ +} + +function layout_calendar_item(loc, e) { + let [x, y] = calendar_xy[loc] + y += 66 + calendar_layout[loc] * 42 + x += 24 + calendar_layout[loc] * 6 + e.style.top = (y + 4) + "px" + e.style.left = (x + 4) + "px" + calendar_layout[loc] ++ +} + +function add_force(parent, type) { + // TODO: reuse pool of elements? + build_div(parent, "unit " + force_type_name[type], "force", type) +} + +function add_asset(parent, type, n) { + // TODO: reuse pool of elements? + build_div(parent, "asset " + asset_type_name[type] + " x"+n, "asset", type) +} + +function update_forces(parent, forces) { + parent.replaceChildren() + for (let i = 0; i < force_type_count; ++i) { + let n = pack4_get(forces, i) + for (let k = 0; k < n; ++k) { + add_force(parent, i) + } + } +} + +function update_assets(parent, assets) { + parent.replaceChildren() + for (let i = 0; i < asset_type_count; ++i) { + let n = pack4_get(assets, i) + while (n >= 4) { + add_asset(parent, i, 4) + n -= 4 + } + if (asset_type_x3[i]) { + while (n >= 3) { + add_asset(parent, i, 3) + n -= 3 + } + } + while (n >= 2) { + add_asset(parent, i, 2) + n -= 2 + } + while (n >= 1) { + add_asset(parent, i, 1) + n -= 1 + } + } +} + +function update_vassals(parent, lord_ix) { + for (let v of data.lords[lord_ix].vassals) { + let e = ui.vassal_service[v] + if (view.vassals[v] === 0) { + e.classList.remove("hide") + parent.appendChild(e) + } else { + e.classList.add("hide") + } + e.classList.toggle("action", is_vassal_action(v)) + } +} + +function update_lord_mat(ix) { + update_assets(ui.assets[ix], view.lords.assets[ix]) + update_vassals(ui.assets[ix], ix) + update_forces(ui.forces[ix], view.lords.forces[ix]) + update_forces(ui.routed[ix], view.lords.routed_forces[ix]) +} + +function update_lord(ix) { + let locale = view.lords.locale[ix] + let service = view.lords.service[ix] + if (locale < 0) { + ui.lord_cylinder[ix].classList.add("hide") + ui.lord_service[ix].classList.add("hide") + ui.lord_mat[ix].classList.add("hide") + ui.lord_mat[ix].classList.remove("action") + return + } + if (locale < 100) { + layout_locale_item(locale, ui.lord_cylinder[ix]) + layout_calendar_item(service, ui.lord_service[ix]) + ui.lord_cylinder[ix].classList.remove("hide") + ui.lord_service[ix].classList.remove("hide") + ui.lord_mat[ix].classList.remove("hide") + update_lord_mat(ix) + } else { + layout_calendar_item(locale - 100, ui.lord_cylinder[ix]) + ui.lord_cylinder[ix].classList.remove("hide") + ui.lord_service[ix].classList.add("hide") + ui.lord_mat[ix].classList.add("hide") + } + ui.lord_cylinder[ix].classList.toggle("action", is_lord_action(ix)) + ui.lord_service[ix].classList.toggle("action", is_service_action(ix)) + + ui.lord_cylinder[ix].classList.toggle("selected", ix === view.who) + ui.lord_mat[ix].classList.toggle("selected", ix === view.who) +} + +function update_locale(loc) { + ui.locale[loc].classList.toggle("action", is_locale_action(loc)) + if (ui.locale_extra[loc]) + ui.locale_extra[loc].classList.toggle("action", is_locale_action(loc)) +} + +function update_arts_of_war() { + if (view.actions && view.actions.arts_of_war) { + ui.arts_of_war_dialog.classList.remove("hide") + ui.arts_of_war_list.replaceChildren() + for_each_friendly_arts_of_war(c => { + if (!is_card_in_use(c)) { + let elt = ui.arts_of_war[c] + ui.arts_of_war_list.appendChild(elt) + elt.classList.toggle("action", view.actions.arts_of_war.includes(c)) + elt.classList.toggle("disabled", !view.actions.arts_of_war.includes(c)) + } + }) + } else { + ui.arts_of_war_dialog.classList.add("hide") + for (let c = 0; c < 42; ++c) { + let elt = ui.arts_of_war[c] + elt.classList.remove("action") + elt.classList.remove("disabled") + } + } + + ui.p1_global.replaceChildren() + for_each_teutonic_arts_of_war(c => { + if (view.global_cards.includes(c)) + ui.p1_global.appendChild(ui.arts_of_war[c]) + }) + + ui.p2_global.replaceChildren() + for_each_russian_arts_of_war(c => { + if (view.global_cards.includes(c)) + ui.p2_global.appendChild(ui.arts_of_war[c]) + }) + + for (let ix = 0; ix < data.lords.length; ++ix) { + let side = ix < 6 ? "teutonic" : "russian" + let c = view.lords.cards[(ix << 1) + 0] + if (c < 0) + ui.c1[ix].classList = `c1 card ${side} hide` + else + ui.c1[ix].classList = `c1 card ${side} aow_${c}` + c = view.lords.cards[(ix << 1) + 1] + if (c < 0) + ui.c2[ix].classList = `c2 card ${side} hide` + else + ui.c2[ix].classList = `c2 card ${side} aow_${c}` + } +} + +function on_update() { + locale_layout.fill(0) + calendar_layout.fill(0) + + for (let ix = 0; ix < data.lords.length; ++ix) { + if (view.lords[ix] === null) { + ui.lord_cylinder[ix].classList.add("hide") + ui.lord_service[ix].classList.add("hide") + ui.lord_mat[ix].classList.add("hide") + } else { + ui.lord_cylinder[ix].classList.remove("hide") + update_lord(ix) + } + } + + for (let loc = 0; loc < data.locales.length; ++loc) { + update_locale(loc) + } + + if (view.turn & 1) + ui.turn.className = `marker circle turn campaign t${view.turn>>1}` + else + ui.turn.className = `marker circle turn levy t${view.turn>>1}` + + let vp1 = count_teutonic_vp() + let vp2 = count_russian_vp() + if ((vp1 >> 1) === (vp2 >> 1)) { + if (vp1 & 1) + ui.vp1.className = `marker circle victory teutonic stack v${vp1>>1} half` + else + ui.vp1.className = `marker circle victory teutonic stack v${vp1>>1}` + if (vp2 & 1) + ui.vp2.className = `marker circle victory russian stack v${vp2>>1} half` + else + ui.vp2.className = `marker circle victory russian stack v${vp2>>1}` + } else { + if (vp1 & 1) + ui.vp1.className = `marker circle victory teutonic v${vp1>>1} half` + else + ui.vp1.className = `marker circle victory teutonic v${vp1>>1}` + if (vp2 & 1) + ui.vp2.className = `marker circle victory russian v${vp2>>1} half` + else + ui.vp2.className = `marker circle victory russian v${vp2>>1}` + } + + update_arts_of_war() + + action_button("ship", "Ship") + action_button("boat", "Boat") + action_button("cart", "Cart") + action_button("sled", "Sled") + + action_button("capability", "Capability") + + action_button("done", "Done") + action_button("end_levy", "End levy") + action_button("end_muster", "End muster") + action_button("end_setup", "End setup") + action_button("undo", "Undo") +} + +function build_div(parent, className, dataname, datavalue, onclick) { + let e = document.createElement("div") + e.className = className + if (dataname) + e.dataset[dataname] = datavalue + if (onclick) + e.addEventListener("mousedown", onclick) + parent.appendChild(e) + return e +} + +function build_lord_mat(lord, ix, side, name) { + let parent = document.getElementById(side === 'teutonic' ? "p1_court" : "p2_court") + let mat = build_div(parent, `mat ${side} ${name} hide`) + let bg = build_div(mat, "background") + ui.forces[ix] = build_div(bg, "forces", "lord", ix) + ui.routed[ix] = build_div(bg, "routed", "lord", ix) + ui.assets[ix] = build_div(bg, "assets", "lord", ix) + ui.c1[ix] = build_div(mat, `c1 card ${side} hide`, "lord", ix) + ui.c2[ix] = build_div(mat, `c2 card ${side} hide`, "lord", ix) + ui.lord_mat[ix] = mat +} + +function build_arts_of_war(side, c) { + let card = ui.arts_of_war[c] = document.createElement("div") + card.className = `card ${side} aow_${c}` + card.dataset.arts_of_war = c + card.addEventListener("mousedown", on_click_arts_of_war) +} + +function build_map() { + data.locales.forEach((locale, ix) => { + let e = ui.locale[ix] = document.createElement("div") + let region = clean_name(locale.region) + e.className = "locale " + locale.type + " " + region + // XXX e.classList.add("action") + let x = round(locale.box.x * MAP_DPI / 300) + let y = round(locale.box.y * MAP_DPI / 300) + let w = floor((locale.box.x+locale.box.w) * MAP_DPI / 300) - x + let h = floor((locale.box.y+locale.box.h) * MAP_DPI / 300) - y + if (locale.type === 'town') { + locale_xy[ix] = [ round(x + w / 2), y - 24 ] + x -= 11 + y -= 5 + w += 16 + h += 5 + } else if (locale.type === 'region') { + locale_xy[ix] = [ round(x + w / 2), round(y + h / 2) ] + x -= 3 + y -= 4 + } else { + locale_xy[ix] = [ round(x + w / 2), y - 36 ] + x -= 2 + y -= 2 + w -= 2 + h -= 2 + } + e.style.left = x + "px" + e.style.top = y + "px" + e.style.width = w + "px" + e.style.height = h + "px" + e.dataset.locale = ix + e.addEventListener("mousedown", on_click_locale) + e.addEventListener("mouseenter", on_focus_locale) + e.addEventListener("mouseleave", on_blur) + document.getElementById("locales").appendChild(e) + + if (locale.type !== 'region') { + e = ui.locale_extra[ix] = document.createElement("div") + e.className = "locale_extra " + locale.type + " " + region + // XXX e.classList.add("action") + let cx = x + (w >> 1) + 4 + let ew = extra_size[locale.type][0] + let eh = extra_size[locale.type][1] + e.style.top = (y - eh) + "px" + e.style.left = (cx - ew/2) + "px" + e.style.width = ew + "px" + e.style.height = eh + "px" + e.dataset.locale = ix + e.addEventListener("mousedown", on_click_locale) + e.addEventListener("mouseenter", on_focus_locale) + e.addEventListener("mouseleave", on_blur) + document.getElementById("locales").appendChild(e) + } + }) + + let x = 160 + let y = 2740 + data.lords.forEach((lord, ix) => { + let e = ui.lord_cylinder[ix] = document.createElement("div") + e.className = "cylinder lord " + clean_name(lord.side) + " " + clean_name(lord.name) + " hide" + e.dataset.lord = ix + e.addEventListener("mousedown", on_click_cylinder) + e.addEventListener("mouseenter", on_focus_cylinder) + e.addEventListener("mouseleave", on_blur) + document.getElementById("pieces").appendChild(e) + + e = ui.lord_service[ix] = document.createElement("div") + e.className = "service_marker lord image" + lord.image + " " + clean_name(lord.side) + " " + clean_name(lord.name) + " hide" + e.dataset.lord = ix + e.addEventListener("mousedown", on_click_lord_service_marker) + e.addEventListener("mouseenter", on_focus_lord_service_marker) + e.addEventListener("mouseleave", on_blur) + document.getElementById("pieces").appendChild(e) + + build_lord_mat(lord, ix, clean_name(lord.side), clean_name(lord.name)) + + x += 70 + }) + + data.vassals.forEach((vassal, ix) => { + let lord = data.lords[vassal.lord] + let e = ui.vassal_service[ix] = document.createElement("div") + e.className = "service_marker vassal image" + vassal.image + " " + clean_name(lord.side) + " " + clean_name(vassal.name) + " hide" + e.dataset.vassal = ix + e.addEventListener("mousedown", on_click_vassal_service_marker) + e.addEventListener("mouseenter", on_focus_vassal_service_marker) + e.addEventListener("mouseleave", on_blur) + document.getElementById("pieces").appendChild(e) + }) + + for (let name in original_boxes) { + let x = round(original_boxes[name][0] * MAP_DPI / 300) + let y = round(original_boxes[name][1] * MAP_DPI / 300) + let w = round(original_boxes[name][2] * MAP_DPI / 300) - 8 + let h = round(original_boxes[name][3] * MAP_DPI / 300) - 8 + let e = ui.boxes[name] = document.createElement("div") + e.className = "box " + name + // XXX e.classList.add("action") + e.style.left = x + "px" + e.style.top = y + "px" + e.style.width = w + "px" + e.style.height = h + "px" + document.getElementById("boxes").appendChild(e) + } + + for (let c = 0; c < 21; ++c) + build_arts_of_war("teutonic", c) + for (let c = 21; c < 42; ++c) + build_arts_of_war("russian", c) +} + +build_map() +// drag_element_with_mouse("#battle", "#battle_header") +drag_element_with_mouse("#arts_of_war", "#arts_of_war_header") +scroll_with_middle_mouse("main") diff --git a/rules.js b/rules.js new file mode 100644 index 0000000..5f280d2 --- /dev/null +++ b/rules.js @@ -0,0 +1,1140 @@ +"use strict" + +const TEUTONS = "Teutons" +const RUSSIANS = "Russians" + +const P1 = TEUTONS +const P2 = RUSSIANS + +let game = null +let view = null +let states = {} + +exports.roles = [ P1, P2 ] + +exports.scenarios = [ + "Pleskau - 1240", + "Watland - 1241", + "Peipus - 1242", + "Return of the Prince - 1241 to 1242", + "Return of the Prince - Nicolle Variant", + "Crusade on Novgorod - 1240 to 1242", + "Pleskau - 1240 (Quickstart)", +] + +exports.scenarios = [ + "Pleskau", + "Watland", + "Peipus", + "Return of the Prince", + "Return of the Prince (Nicolle Variant)", + "Crusade on Novgorod", + "Pleskau (Quickstart)", +] + +// unit types +const KNIGHTS = 0 +const SERGEANTS = 1 +const LIGHT_HORSE = 2 +const ASIATIC_HORSE = 3 +const MEN_AT_ARMS = 4 +const MILITIA = 5 +const SERFS = 6 + +// asset types +const PROV = 0 +const COIN = 1 +const LOOT = 2 +const CART = 3 +const SLED = 4 +const BOAT = 5 +const SHIP = 6 + +const data = require("./data.js") + +function find_lord(name) { return data.lords.findIndex(x => x.name === name) } +function find_locale(name) { return data.locales.findIndex(x => x?.name === name) } + +const lord_name = data.lords.map(lord => lord.name) + +const lord_count = data.lords.length +const vassal_count = data.vassals.length +const last_vassal = vassal_count - 1 +const last_lord = lord_count - 1 + +const LORD_ANDREAS = find_lord("Andreas") +const LORD_HEINRICH = find_lord("Heinrich") +const LORD_HERMANN = find_lord("Hermann") +const LORD_KNUD_ABEL = find_lord("Knud & Abel") +const LORD_RUDOLF = find_lord("Rudolf") +const LORD_YAROSLAV = find_lord("Yaroslav") + +const LORD_ALEKSANDR = find_lord("Aleksandr") +const LORD_ANDREY = find_lord("Andrey") +const LORD_DOMASH = find_lord("Domash") +const LORD_GAVRILO = find_lord("Gavrilo") +const LORD_KARELIANS = find_lord("Karelians") +const LORD_VLADISLAV = find_lord("Vladislav") + +const LOC_REVAL = find_locale("Reval") +const LOC_WESENBERG = find_locale("Wesenberg") +const LOC_DORPAT = find_locale("Dorpat") +const LOC_LEAL = find_locale("Leal") +const LOC_RIGA = find_locale("Riga") +const LOC_ADSEL = find_locale("Adsel") +const LOC_FELLIN = find_locale("Fellin") +const LOC_ODENPAH = find_locale("Odenpäh") +const LOC_WENDEN = find_locale("Wenden") +const LOC_NOVGOROD = find_locale("Novgorod") +const LOC_LADOGA = find_locale("Ladoga") +const LOC_PSKOV = find_locale("Pskov") +const LOC_RUSA = find_locale("Rusa") +const LOC_LOVAT = find_locale("Lovat") +const LOC_LUGA = find_locale("Luga") +const LOC_NEVA = find_locale("Neva") +const LOC_VOLKHOV = find_locale("Volkhov") +const LOC_IZBORSK = find_locale("Izborsk") +const LOC_KAIBOLOVO = find_locale("Kaibolovo") +const LOC_KOPORYE = find_locale("Koporye") +const LOC_PORKHOV = find_locale("Porkhov") +const LOC_VELIKIYE_LUKI = find_locale("Velikiye Luki") + +const LOC_DUBROVNO = find_locale("Dubrovno") + +const NOBODY = -1 +const NOWHERE = -1 +const NOTHING = -1 +const NEVER = -1 +const CALENDAR = 100 + +const SUMMER = 0 +const EARLY_WINTER = 1 +const LATE_WINTER = 2 +const RASPUTITSA = 3 + +const SEASONS = [ + SUMMER, SUMMER, EARLY_WINTER, EARLY_WINTER, LATE_WINTER, LATE_WINTER, RASPUTITSA, RASPUTITSA, + SUMMER, SUMMER, EARLY_WINTER, EARLY_WINTER, LATE_WINTER, LATE_WINTER, RASPUTITSA, RASPUTITSA, +] + +const TURN_NAME = [ + "1 - Summer 1240", + "2 - Summer 1240", + "3 - Early Winter 1240", + "4 - Early Winter 1240", + "5 - Late Winter 1241", + "6 - Late Winter 1241", + "7 - Rasputitsa 1241", + "8 - Rasputitsa 1241", + "9 - Summer 1241", + "10 - Summer 1241", + "11 - Early Winter 1241", + "12 - Early Winter 1241", + "13 - Late Winter 1242", + "14 - Late Winter 1242", + "15 - Rasputitsa 1242", + "16 - Rasputitsa 1242", +] + +const USABLE_TRANSPORT = [ + [ CART, BOAT, SHIP ], + [ SLED ], + [ SLED ], + [ BOAT, SHIP ], +] + +function current_season() { + return SEASONS[game.turn >> 1] +} + +function current_turn_name() { + return TURN_NAME[game.turn >> 1] +} + +// === GAME STATE === + +let first_friendly_lord = 0 +let last_friendly_lord = 5 +let first_enemy_lord = 6 +let last_enemy_lord = 11 + +function update_aliases() { + if (game.active === P1) { + first_friendly_lord = 0 + last_friendly_lord = 5 + first_enemy_lord = 6 + last_enemy_lord = 11 + } else { + first_friendly_lord = 6 + last_friendly_lord = 11 + first_enemy_lord = 0 + last_enemy_lord = 5 + } +} + +function load_state(state) { + if (game !== state) { + game = state + update_aliases() + } +} + +function push_state(next) { + if (!states[next]) + throw Error("No such state: " + next) + game.stack.push([game.state, game.who, game.count]) + game.state = next +} + +function pop_state() { + ;[ game.state, game.who, game.count ] = game.stack.pop() +} + +function set_active(new_active) { + if (game.active !== new_active) { + game.active = new_active + update_active_aliases() + } +} + +function set_active_enemy() { + game.active = enemy_player() + update_aliases() +} + +function enemy_player() { + if (game.active === P1) return P2 + if (game.active === P2) return P1 + return null +} + +function get_lord_locale(lord) { + return game.lords.locale[lord] +} + +function get_lord_service(lord) { + return game.lords.service[lord] +} + +function get_lord_capability(lord, n) { + return game.lords.cards[(lord << 1) + n] +} + +function set_lord_capability(lord, n, x) { + game.lords.cards[(lord << 1) + n] = x +} + +function get_lord_assets(lord, n) { + return pack4_get(game.lords.assets[lord], n) +} + +function get_lord_forces(lord, n) { + return pack4_get(game.lords.forces[lord], n) +} + +function get_lord_routed_forces(lord, n) { + return pack4_get(game.lords.routed_forces[lord], n) +} + +function get_lord_moved(lord) { + return pack1_get(game.lords.moved, lord) +} + +function set_lord_locale(lord, locale) { + game.lords.locale[lord] = locale +} + +function set_lord_service(lord, service) { + game.lords.service[lord] = service +} + +function set_lord_assets(lord, n, x) { + if (x < 0) x = 0 + if (x > 8) x = 8 + game.lords.assets[lord] = pack4_set(game.lords.assets[lord], n, x) +} + +function add_lord_assets(lord, n, x) { + set_lord_assets(lord, n, get_lord_assets(lord, n) + x) +} + +function set_lord_forces(lord, n, x) { + if (x < 0) x = 0 + if (x > 15) x = 15 + game.lords.forces[lord] = pack4_set(game.lords.forces[lord], n, x) +} + +function add_lord_forces(lord, n, x) { + set_lord_forces(lord, n, get_lord_forces(lord, n) + x) +} + +function set_lord_routed_forces(lord, n, x) { + if (x < 0) x = 0 + if (x > 15) x = 15 + game.lords.routed_forces[lord] = pack4_set(game.lords.routed_forces[lord], n, x) +} + +function add_lord_routed_forces(lord, n, x) { + set_lord_routed_forces(lord, n, get_lord_routed_forces(lord, n) + x) +} + +function set_lord_moved(lord, x) { + game.lords.moved = pack1_set(game.lords.moved, lord, x) +} + +function get_lord_vassal_count(lord) { + return data.lords[lord].vassals.length +} + +function get_lord_vassal_service(lord, n) { + let v = data.lords[lord].vassals[n] + return game.vassals[v] +} + +function set_lord_vassal_service(lord, n, x) { + let v = data.lords[lord].vassals[n] + game.vassals[v] = x +} + +// === GAME STATE HELPERS === + +function is_card_in_use(c) { + if (set_has(game.global_cards, c)) + return true + if (game.lords.cards.includes(c)) + return true + if (c === 18 || c === 19 || c === 20) + return true + if (c === 39 || c === 40 || c === 41) + return true + return false +} + +function is_lord_on_map(lord) { + let loc = get_lord_locale(lord) + return loc !== NOWHERE && loc < CALENDAR +} + +function is_lord_on_calendar(lord) { + let loc = get_lord_locale(lord) + return loc >= CALENDAR +} + +function is_lord_ready(lord) { + let loc = get_lord_locale(lord) + return (loc >= CALENDAR && loc <= CALENDAR + (game.turn >> 1)) +} + +function is_vassal_ready(vassal) { + return game.vassals[vassal] === 0 +} + +function is_lord_at_friendly_locale(lord) { + let loc = get_lord_locale(lord) + return is_friendly_locale(loc) +} + +function is_friendly_locale(loc) { + // TODO + return loc !== NOWHERE && loc < CALENDAR +} + +function for_each_friendly_arts_of_war(fn) { + if (game.active === P1) + for (let i = 0; i < 18; ++i) + fn(i) + else + for (let i = 21; i < 39; ++i) + fn(i) +} + +function can_add_transport(who, what) { + // TODO: limit to transports usable in current season? + return get_lord_assets(who, what) < 8 +} + +function roll_die(reason) { + let die = random(6) + 1 + log(`Rolled ${die}${reason}.`) + return die +} + +// === SETUP === + +function setup_lord_on_calendar(lord, turn) { + set_lord_locale(lord, CALENDAR + turn) +} + +function muster_lord(lord, locale, service) { + let info = data.lords[lord] + + if (!service) + service = (game.turn >> 1) + info.service + + set_lord_locale(lord, locale) + set_lord_service(lord, service) + + set_lord_assets(lord, PROV, info.assets.prov | 0) + set_lord_assets(lord, COIN, info.assets.coin | 0) + set_lord_assets(lord, LOOT, info.assets.loot | 0) + + set_lord_assets(lord, CART, info.assets.cart | 0) + set_lord_assets(lord, SLED, info.assets.sled | 0) + set_lord_assets(lord, BOAT, info.assets.boat | 0) + set_lord_assets(lord, SHIP, info.assets.ship | 0) + + set_lord_forces(lord, KNIGHTS, info.forces.knights | 0) + set_lord_forces(lord, SERGEANTS, info.forces.serfs | 0) + set_lord_forces(lord, LIGHT_HORSE, info.forces.light_horse | 0) + set_lord_forces(lord, ASIATIC_HORSE, info.forces.asiatic_horse | 0) + set_lord_forces(lord, MEN_AT_ARMS, info.forces.men_at_arms | 0) + set_lord_forces(lord, MILITIA, info.forces.militia | 0) + set_lord_forces(lord, SERFS, info.forces.serfs | 0) + + for (let v of info.vassals) + game.vassals[v] = 0 +} + +function muster_vassal(lord, vassal) { + let info = data.vassals[vassal] + + game.vassals[vassal] = 1 + + add_lord_forces(lord, KNIGHTS, info.forces.knights | 0) + add_lord_forces(lord, SERGEANTS, info.forces.serfs | 0) + add_lord_forces(lord, LIGHT_HORSE, info.forces.light_horse | 0) + add_lord_forces(lord, ASIATIC_HORSE, info.forces.asiatic_horse | 0) + add_lord_forces(lord, MEN_AT_ARMS, info.forces.men_at_arms | 0) + add_lord_forces(lord, MILITIA, info.forces.militia | 0) + add_lord_forces(lord, SERFS, info.forces.serfs | 0) +} + +exports.setup = function (seed, scenario, options) { + game = { + seed, + scenario, + options, + log: [], + undo: [], + + active: P1, + state: 'setup_lords', + stack: [], + + turn: 0, + p1_hand: [], + p2_hand: [], + p1_plan: [], + p2_plan: [], + lords: { + locale: Array(lord_count).fill(NOWHERE), + service: Array(lord_count).fill(NEVER), + assets: Array(lord_count).fill(0), + forces: Array(lord_count).fill(0), + routed_forces: Array(lord_count).fill(0), + cards: Array(lord_count << 1).fill(NOTHING), + lieutenants: [], + moved: 0, + }, + vassals: Array(vassal_count).fill(0), + legate: NOWHERE, + veche_vp: 0, + veche_coin: 0, + conquered: [], + ravaged: [], + global_cards: [], + + who: NOBODY, + where: NOWHERE, + what: NOTHING, + levy: 0, // lordship used + count: 0, + } + + log_h1(scenario) + + switch (scenario) { + default: + case "Pleskau": + setup_pleskau() + break + case "Watland": + setup_watland() + break + case "Peipus": + setup_peipus() + break + case "Return of the Prince": + setup_return_of_the_prince() + break + case "Return of the Prince (Nicolle Variant)": + setup_return_of_the_prince_nicolle() + break + case "Crusade on Novgorod": + setup_crusade_on_novgorod() + break + case "Pleskau (Quickstart)": + setup_quickstart() + break + } + + return game +} + +function setup_pleskau() { + game.turn = 1 << 1 + game.veche_vp = 1 + muster_lord(LORD_HERMANN, LOC_DORPAT, 4) + muster_lord(LORD_KNUD_ABEL, LOC_REVAL, 3) + muster_lord(LORD_YAROSLAV, LOC_ODENPAH, 2) + muster_lord(LORD_GAVRILO, LOC_PSKOV, 4) + muster_lord(LORD_VLADISLAV, LOC_NEVA, 3) + setup_lord_on_calendar(LORD_RUDOLF, 1) + setup_lord_on_calendar(LORD_DOMASH, 1) +} + +function setup_quickstart() { + setup_pleskau() + // TODO: automated muster +} + +function setup_watland() { + game.turn = 4 << 1 + game.veche_vp = 1 + game.veche_coin = 1 + + set_add(game.conquered, LOC_IZBORSK) + set_add(game.conquered, LOC_PSKOV) + set_add(game.ravaged, LOC_PSKOV) + set_add(game.ravaged, LOC_DUBROVNO) + + muster_lord(LORD_ANDREAS, LOC_FELLIN, 7) + muster_lord(LORD_KNUD_ABEL, LOC_WESENBERG, 6) + muster_lord(LORD_YAROSLAV, LOC_PSKOV, 5) + muster_lord(LORD_DOMASH, LOC_NOVGOROD, 7) + + setup_lord_on_calendar(LORD_HEINRICH, 4) + setup_lord_on_calendar(LORD_RUDOLF, 4) + setup_lord_on_calendar(LORD_VLADISLAV, 4) + setup_lord_on_calendar(LORD_KARELIANS, 4) + setup_lord_on_calendar(LORD_ANDREY, 5) + setup_lord_on_calendar(LORD_ALEKSANDR, 7) + setup_lord_on_calendar(LORD_HERMANN, 8) +} + +states.setup_lords = { + prompt() { + view.prompt = "Setup your Lords." + let done = true + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (is_lord_on_map(lord) && !get_lord_moved(lord)) { + if (data.lords[lord].assets.transport > 0) { + gen_action_lord(lord) + done = false + } + } + } + if (done) { + view.prompt += " All done." + view.actions.end_setup = 1 + } + }, + lord(lord) { + push_undo() + push_state('muster_lord_transport') + set_lord_moved(lord, 1) + game.who = lord + game.count = data.lords[lord].assets.transport + }, + end_setup() { + clear_undo() + end_setup() + }, +} + +function end_setup_lords() { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) + set_lord_moved(lord, 0) + if (game.active === P1) { + set_active_enemy() + } else { + set_active_enemy() + goto_levy_arts_of_war() + } +} + +// === LEVY: ARTS OF WAR === + +function goto_levy_arts_of_war() { + log_h1("Levy " + current_turn_name()) + game.state = 'levy_arts_of_war' + end_levy_arts_of_war() +} + +function end_levy_arts_of_war() { + goto_levy_pay() +} + +states.levy_arts_of_war = { +} + +// === LEVY: PAY === + +function goto_levy_pay() { + game.state = 'levy_pay' + end_levy_pay() +} + +function end_levy_pay() { + goto_levy_disband() +} + +states.levy_pay = { +} + +// === LEVY: DISBAND === + +function goto_levy_disband() { + game.state = 'levy_disband' + end_levy_disband() +} + +function end_levy_disband() { + goto_levy_muster() +} + +states.levy_disband = { +} + +// === LEVY: MUSTER === + +function goto_levy_muster() { + game.state = 'levy_muster' +} + +function end_levy_muster() { + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) + set_lord_moved(lord, 0) + if (game.active === P1) { + set_active_enemy() + } else { + set_active_enemy() + goto_levy_call_to_arms() + } +} + +states.levy_muster = { + prompt() { + view.prompt = "Muster your Lords." + let done = true + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (is_lord_at_friendly_locale(lord) && !get_lord_moved(lord)) { + gen_action_lord(lord) + done = false + } + } + if (done) { + view.prompt += " All done." + view.actions.end_muster = 1 + } + }, + lord(lord) { + push_undo() + push_state('levy_muster_lord') + game.who = lord + game.count = data.lords[lord].lordship + }, + end_muster() { + clear_undo() + end_levy_muster() + }, +} + +states.levy_muster_lord = { + prompt() { + view.prompt = `Muster ${lord_name[game.who]}.` + + if (game.count > 0) { + view.prompt += ` ${game.count} lordship left.` + + // Roll to muster Ready Lord at Seat + for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { + if (lord === ALEKSANDR) + continue + if (lord === ANDREY && game.who !== ALEKSANDR) + continue + if (is_lord_ready(lord)) + // TODO: has available seat + gen_action_lord(lord) + } + + // Muster Ready Vassal Forces + for (let vassal of data.lords[game.who].vassals) { + if (is_vassal_ready(vassal)) + gen_action_vassal(vassal) + } + + // Add Transport + if (data.lords[game.who].ships) { + if (can_add_transport(game.who, SHIP)) + view.actions.ship = 1 + } + if (can_add_transport(game.who, BOAT)) + view.actions.boat = 1 + if (can_add_transport(game.who, CART)) + view.actions.cart = 1 + if (can_add_transport(game.who, SLED)) + view.actions.sled = 1 + + // Add Capability + view.actions.capability = 1 + } else { + view.prompt += " All done." + } + + view.actions.done = 1 + }, + + lord(other) { + clear_undo() + --game.count + let die = roll_die(` to muster ${lord_name[other]}`) + // TODO: roll for lord + if (die <= data.lords[other].fealty) { + logi(`Success!`) + push_state('muster_lord_at_seat') + game.who = other + } else { + logi(`Failed.`) + } + }, + + vassal(vassal) { + push_undo() + --game.count + muster_vassal(game.who, vassal) + }, + + ship() { + push_undo() + --game.count + add_lord_assets(game.who, SHIP, 1) + }, + boat() { + push_undo() + --game.count + add_lord_assets(game.who, BOAT, 1) + }, + cart() { + push_undo() + --game.count + add_lord_assets(game.who, CART, 1) + }, + sled() { + push_undo() + --game.count + add_lord_assets(game.who, SLED, 1) + }, + + capability() { + push_undo() + --game.count + push_state('muster_capability') + }, + + done() { + set_lord_moved(game.who, 1) + pop_state() + }, +} + +states.muster_lord_at_seat = { + prompt() { + view.prompt = `Select seat for ${lord_name[game.who]}.` + for (let loc of data.lords[game.who].seats) + if (is_friendly_locale(loc)) + gen_action_locale(loc) + }, + locale(loc) { + push_undo() + logi(`Mustered at %${loc}.`) + set_lord_moved(game.who, 1) + muster_lord(game.who, loc) + game.state = 'muster_lord_transport' + game.count = data.lords[game.who].assets.transport + }, +} + +states.muster_lord_transport = { + prompt() { + view.prompt = `Select Transport for ${lord_name[game.who]}.` + view.prompt += ` ${game.count} left.` + if (data.lords[game.who].ships) { + if (can_add_transport(game.who, SHIP)) + view.actions.ship = 1 + } + if (can_add_transport(game.who, BOAT)) + view.actions.boat = 1 + if (can_add_transport(game.who, CART)) + view.actions.cart = 1 + if (can_add_transport(game.who, SLED)) + view.actions.sled = 1 + view.actions.done = 0 + }, + ship() { + push_undo() + add_lord_assets(game.who, SHIP, 1) + if (--game.count === 0) + pop_state() + }, + boat() { + push_undo() + add_lord_assets(game.who, BOAT, 1) + if (--game.count === 0) + pop_state() + }, + cart() { + push_undo() + add_lord_assets(game.who, CART, 1) + if (--game.count === 0) + pop_state() + }, + sled() { + push_undo() + add_lord_assets(game.who, SLED, 1) + if (--game.count === 0) + pop_state() + }, +} + +states.muster_capability = { + prompt() { + view.prompt = `Select a new capability for ${lord_name[game.who]}.` + for_each_friendly_arts_of_war(c => { + if (!is_card_in_use(c)) { + if (!data.cards[c].lords || set_has(data.cards[c].lords, game.who)) + gen_action_arts_of_war(c) + } + }) + }, + arts_of_war(c) { + push_undo() + if (!data.cards[c].lords) { + set_add(game.global_cards, c) + } else { + if (get_lord_capability(game.who, 0) < 0) + set_lord_capability(game.who, 0, c) + else if (get_lord_capability(game.who, 1) < 0) + set_lord_capability(game.who, 1, c) + else { + game.what = c + game.state = 'muster_capability_discard' + return + } + } + pop_state() + }, +} + +// === LEVY: CALL TO ARMS === + +function goto_levy_call_to_arms() { + game.state = 'levy_call_to_arms' + end_levy_call_to_arms() +} + +function end_levy_call_to_arms() { + goto_campaign_plan() +} + +states.levy_call_to_arms = { +} + +// === CAMPAIGN === + +function goto_campaign_plan() { + game.state = 'campaign_plan' +} + +// === GAME OVER === + +function goto_game_over(result, victory) { + game.state = 'game_over' + game.active = "None" + game.result = result + game.victory = victory + log_br() + log(game.victory) + return true +} + +states.game_over = { + get inactive() { + return game.victory + }, + prompt() { + view.prompt = game.victory + } +} + +exports.resign = function (state, current) { + load_state(state) + if (game.state !== 'game_over') { + for (let opponent of exports.roles) { + if (opponent !== current) { + goto_game_over(opponent, current + " resigned.") + break + } + } + } + return game +} + +// === UNCOMMON TEMPLATE === + +function log_br() { + if (game.log.length > 0 && game.log[game.log.length-1] !== "") + game.log.push("") +} + +function log(msg) { + game.log.push(msg) +} + +function logi(msg) { + game.log.push(">" + msg) +} + +function log_h1(msg) { + log_br() + log(".h1 " + msg) + log_br() +} + +function log_h2(msg) { + log_br() + log(".h2 " + msg) + log_br() +} + +function log_h3(msg) { + log_br() + log(".h3 " + msg) + log_br() +} + +function log_h4(msg) { + log_br() + log(".h4 " + msg) +} + + +function gen_action(action, argument) { + if (!(action in view.actions)) + view.actions[action] = [] + set_add(view.actions[action], argument) +} + +function gen_action_locale(locale) { + gen_action('locale', locale) +} + +function gen_action_lord(lord) { + gen_action('lord', lord) +} + +function gen_action_service(service) { + gen_action('service', service) +} + +function gen_action_vassal(vassal) { + gen_action('vassal', vassal) +} + +function gen_action_arts_of_war(c) { + gen_action('arts_of_war', c) +} + +exports.view = function(state, current) { + game = state + view = { + log: game.log, + turn: game.turn, + lords: game.lords, + vassals: game.vassals, + legate: game.legate, + veche_vp: game.veche_vp, + veche_coin: game.veche_coin, + global_cards: game.global_cards, + conquered: game.conquered, + ravaged: game.ravaged, + who: game.who, + where: game.where, + } + if (game.state === 'game_over') { + view.prompt = game.victory + } else if (current === 'Observer' || game.active !== current) { + let inactive = states[game.state].inactive || game.state + view.prompt = `Waiting for ${game.active} \u2014 ${inactive}...` + } else { + view.actions = {} + if (states[game.state]) + states[game.state].prompt() + else + view.prompt = "Unknown state: " + game.state + if (view.actions.undo === undefined) { + if (game.undo && game.undo.length > 0) + view.actions.undo = 1 + else + view.actions.undo = 0 + } + } + return view +} + +exports.action = function (state, current, action, arg) { + load_state(state) + Object.seal(game) // XXX: don't allow adding properties + let S = states[game.state] + if (S && action in S) { + S[action](arg) + } else { + if (action === 'undo' && game.undo && game.undo.length > 0) + pop_undo() + else + throw new Error("Invalid action: " + action) + } + return game +} + +// === COMMON TEMPLATE === + +function random(range) { + // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf + return (game.seed = game.seed * 200105 % 34359738337) % range +} + +// Packed array of small numbers in one word + +function pack1_get(word, n) { + return (word >>> n) & 1 +} + +function pack4_get(word, n) { + n = n << 2 + return (word >>> n) & 15 +} + +function pack1_set(word, n, x) { + return (word & ~(1 << n)) | (x << n) +} + +function pack4_set(word, n, x) { + n = n << 2 + return (word & ~(15 << n)) | (x << n) +} + +// Sorted array treated as Set (for JSON) +function set_index(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return m + } + return -1 +} + +function set_has(set, item) { + return set_index(set, item) >= 0 +} + +function set_add(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return + } + set.splice(a, 0, item) +} + +function set_delete(set, item) { + let i = set_index(set, item) + if (i >= 0) + set.splice(i, 1) +} + +function set_clear(set) { + set.length = 0 +} + +function set_toggle(set, item) { + if (set_has(set, item)) + set_delete(set, item) + else + set_add(set, item) +} + +function deep_copy(original) { + if (Array.isArray(original)) { + let n = original.length + let copy = new Array(n) + for (let i = 0; i < n; ++i) { + let v = original[i] + if (typeof v === "object" && v !== null) + copy[i] = deep_copy(v) + else + copy[i] = v + } + return copy + } else { + let copy = {} + for (let i in original) { + let v = original[i] + if (typeof v === "object" && v !== null) + copy[i] = deep_copy(v) + else + copy[i] = v + } + return copy + } +} + +function push_undo() { + let copy = {} + for (let k in game) { + let v = game[k] + if (k === "undo") continue + else if (k === "log") v = v.length + else if (typeof v === "object" && v !== null) v = deep_copy(v) + copy[k] = v + } + game.undo.push(copy) +} + +function pop_undo() { + let save_log = game.log + let save_undo = game.undo + let state = save_undo.pop() + save_log.length = state.log + state.log = save_log + state.undo = save_undo + load_state(state) +} + +function clear_undo() { + game.undo.length = 0 +} diff --git a/tools/gendata.js b/tools/gendata.js index 2968023..d72e43b 100644 --- a/tools/gendata.js +++ b/tools/gendata.js @@ -86,13 +86,35 @@ function print(str) { var locmap = {} // 0=offmap, 1-N=map locales, 100-M=calendar boxes -var locales = [null] +var locales = [] var ways = [] var waterways = [] var trackways = [] const scale = 1 +const vp_map = { + archbishopric: 3, + city: 2, + fort: 1, + bishopric: 2, + castle: 1, + traderoute: 1, + town: 0.5, + region: 0.5, +} + +const wall_map = { + archbishopric: 3, + city: 3, + fort: 3, + traderoute: 0, + bishopric: 4, + castle: 4, + town: 0, + region: 0, +} + function defloc(region, stronghold, type, name) { let [x, y, w, h] = boxes[name] x = Math.round(x * scale) @@ -100,7 +122,9 @@ function defloc(region, stronghold, type, name) { w = Math.round(w * scale) h = Math.round(h * scale) locmap[name] = locales.length - locales.push({ name, type, stronghold, region, ways: [], box: { x, y, w, h } }) + let vp = vp_map[type] + let walls = wall_map[type] + locales.push({ name, type, stronghold, walls, vp, region, ways: [], box: { x, y, w, h } }) } function defway(type, list) { @@ -289,9 +313,7 @@ let lords = [ forces: { knights: 1, sergeants: 2, - light_horse: 0, men_at_arms: 1, - militia: 0, }, assets: { transport: 2, @@ -314,9 +336,7 @@ let lords = [ forces: { knights: 1, sergeants: 1, - light_horse: 0, men_at_arms: 1, - militia: 0, }, assets: { ship: 1, @@ -340,9 +360,8 @@ let lords = [ forces: { knights: 1, sergeants: 1, - light_horse: 0, - men_at_arms: 2, - militia: 0, + men_at_arms: 1, + militia: 1, }, assets: { transport: 1, @@ -366,9 +385,8 @@ let lords = [ forces: { knights: 1, sergeants: 1, - light_horse: 3, - men_at_arms: 0, - militia: 0, + men_at_arms: 2, + militia: 1, }, assets: { ship: 2, @@ -391,9 +409,7 @@ let lords = [ forces: { knights: 1, sergeants: 1, - light_horse: 0, men_at_arms: 1, - militia: 0, }, assets: { transport: 1, @@ -415,10 +431,8 @@ let lords = [ command: 2, forces: { knights: 1, - sergeants: 0, light_horse: 1, men_at_arms: 1, - militia: 0, }, assets: { transport: 1, @@ -440,10 +454,7 @@ let lords = [ command: 3, forces: { knights: 3, - sergeants: 0, - light_horse: 0, men_at_arms: 2, - militia: 0, }, assets: { transport: 2, @@ -464,10 +475,7 @@ let lords = [ command: 2, forces: { knights: 3, - sergeants: 0, - light_horse: 0, men_at_arms: 2, - militia: 0, }, assets: { transport: 2, @@ -487,7 +495,6 @@ let lords = [ lordship: 2, command: 2, forces: { - knights: 0, sergeants: 1, light_horse: 1, men_at_arms: 2, @@ -513,7 +520,6 @@ let lords = [ command: 2, forces: { knights: 1, - sergeants: 0, light_horse: 1, men_at_arms: 1, militia: 1, @@ -538,10 +544,7 @@ let lords = [ lordship: 1, command: 2, forces: { - knights: 0, - sergeants: 0, light_horse: 1, - men_at_arms: 0, militia: 4, }, assets: { @@ -562,11 +565,9 @@ let lords = [ lordship: 2, command: 3, forces: { - knights: 0, sergeants: 1, light_horse: 1, men_at_arms: 2, - militia: 0, }, assets: { transport: 1, @@ -577,6 +578,114 @@ let lords = [ ] +let AOW = {} +let cards = [] + +function cmpnum(a,b) { return a - b } + +function arts_of_war_event(name, event) { + let c = { name, event, capability: null, lords: null } + cards.push(c) + AOW[name] = c +} + +function arts_of_war_capability(name, capability, lord_names) { + AOW[name].capability = capability + if (lord_names === "ALL") { + AOW[name].lords = null + } + else if (lord_names === "any") { + let side = name[0] === 'T' ? "Teutonic" : "Russian" + lord_names = lords.filter(l => l.side === side).map(l => l.name) + AOW[name].lords = lord_names.map(n => lords.findIndex(l => l.name === n)).sort(cmpnum) + } + else { + AOW[name].lords = lord_names.map(n => lords.findIndex(l => l.name === n)).sort(cmpnum) + } +} + +arts_of_war_event("T1", "Grand Prince") +arts_of_war_event("T2", "Torzhok") +arts_of_war_event("T3", "Vodian Treachery") +arts_of_war_event("T4", "Bridge") +arts_of_war_event("T5", "Marsh") +arts_of_war_event("T6", "Ambush") +arts_of_war_event("T7", "Tverdilo") +arts_of_war_event("T8", "Teutonic Fervor") +arts_of_war_event("T9", "Hill") +arts_of_war_event("T10", "Field Organ") +arts_of_war_event("T11", "Pope Gregory") +arts_of_war_event("T12", "Khan Baty") +arts_of_war_event("T13", "Heinrich Sees the Curia") +arts_of_war_event("T14", "Bountiful Harvest") +arts_of_war_event("T15", "Mindaugas") +arts_of_war_event("T16", "Famine") +arts_of_war_event("T17", "Dietrich von Grüningen") +arts_of_war_event("T18", "Swedish Crusade") +arts_of_war_event("TNo", "No Event") +arts_of_war_event("TNo", "No Event") +arts_of_war_event("TNo", "No Event") + +arts_of_war_capability("T1", "Treaty of Stensby", [ "Heinrich", "Knud & Abel" ]) +arts_of_war_capability("T2", "Raiders", "any") +arts_of_war_capability("T3", "Converts", "any") +arts_of_war_capability("T4", "Balistarii", "any") +arts_of_war_capability("T5", "Balistarii", "any") +arts_of_war_capability("T6", "Balistarii", "any") +arts_of_war_capability("T7", "Warrior Monks", [ "Andreas", "Rudolf" ]) +arts_of_war_capability("T8", "Hillforts", "ALL") +arts_of_war_capability("T9", "Halbbrüder", [ "Andreas", "Rudolf" ]) +arts_of_war_capability("T10", "Halbbrüder", [ "Andreas", "Rudolf" ]) +arts_of_war_capability("T11", "Crusade", [ "Andreas", "Rudolf" ]) +arts_of_war_capability("T12", "Ordensburgen", "ALL") +arts_of_war_capability("T13", "William of Modena", "ALL") +arts_of_war_capability("T14", "Trebuchets", "any") +arts_of_war_capability("T15", "Warrior Monks", [ "Andreas", "Rudolf" ]) +arts_of_war_capability("T16", "Ransom", "ALL") +arts_of_war_capability("T17", "Stonemasons", "any") +arts_of_war_capability("T18", "Cogs", [ "Heinrich", "Knud & Abel", "Andreas" ]) + +arts_of_war_event("R1", "Bridge") +arts_of_war_event("R2", "Marsh") +arts_of_war_event("R3", "Pogost") +arts_of_war_event("R4", "Raven's Rock") +arts_of_war_event("R5", "Hill") +arts_of_war_event("R6", "Ambush") +arts_of_war_event("R7", "Famine") +arts_of_war_event("R8", "Prince of Polotsk") +arts_of_war_event("R9", "Osilian Revolt") +arts_of_war_event("R10", "Batu Khan") +arts_of_war_event("R11", "Valdemar") +arts_of_war_event("R12", "Mindaugas") +arts_of_war_event("R13", "Pelgui") +arts_of_war_event("R14", "Prussian Revolt") +arts_of_war_event("R15", "Death of the Pope") +arts_of_war_event("R16", "Tempest") +arts_of_war_event("R17", "Dietrich von Grüningen") +arts_of_war_event("R18", "Bountiful Harvest") +arts_of_war_event("RNo", "No Event") +arts_of_war_event("RNo", "No Event") +arts_of_war_event("RNo", "No Event") + +arts_of_war_capability("R1", "Luchniki", [ "Vladislav", "Karelians", "Gavrilo", "Domash" ]) +arts_of_war_capability("R2", "Luchniki", [ "Vladislav", "Karelians", "Gavrilo", "Domash" ]) +arts_of_war_capability("R3", "Streltsy", [ "Aleksandr", "Andrey", "Domash", "Gavrilo", "Vladislav" ]) // NOT Karelians +arts_of_war_capability("R4", "Smerdi", "ALL") +arts_of_war_capability("R5", "Druzhina", [ "Aleksandr", "Andrey", "Gavrilo" ]) +arts_of_war_capability("R6", "Druzhina", [ "Aleksandr", "Andrey", "Gavrilo" ]) +arts_of_war_capability("R7", "Ransom", "ALL") +arts_of_war_capability("R8", "Black Sea Trade", "ALL") +arts_of_war_capability("R9", "Baltic Sea Trade", "ALL") +arts_of_war_capability("R10", "Steppe Warriors", [ "Aleksandr", "Andrey" ]) +arts_of_war_capability("R11", "House of Suzdal", [ "Aleksandr", "Andrey" ]) +arts_of_war_capability("R12", "Raiders", "any") +arts_of_war_capability("R13", "Streltsy", [ "Aleksandr", "Andrey", "Domash", "Gavrilo", "Vladislav" ]) // NOT Karelians +arts_of_war_capability("R14", "Raiders", "any") +arts_of_war_capability("R15", "Archbishopric", "any") +arts_of_war_capability("R16", "Lodya", "any") +arts_of_war_capability("R17", "Veliky Knyaz", "any") +arts_of_war_capability("R18", "Stone Kremlin", "any") + let vassals = [] for (let lord of lords) lord.vassals = [] @@ -698,8 +807,9 @@ dumplist("locales", locales) dumplist("ways", ways) dumplist("lords", lords) dumplist("vassals", vassals) +dumplist("cards", cards) print("}") print("if (typeof module !== 'undefined') module.exports = data") -fs.writeFileSync("build_counters3.sh", script.join("\n") + "\n") -fs.writeFileSync("../data.js", data.join("\n") + "\n") +fs.writeFileSync("tools/build_counters3.sh", script.join("\n") + "\n") +fs.writeFileSync("data.js", data.join("\n") + "\n") -- cgit v1.2.3