summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--about.html4
-rw-r--r--data.js227
-rw-r--r--play.html993
-rw-r--r--play.js729
-rw-r--r--rules.js1140
-rw-r--r--tools/gendata.js172
6 files changed, 3231 insertions, 34 deletions
diff --git a/about.html b/about.html
index 5e0a726..818c243 100644
--- a/about.html
+++ b/about.html
@@ -19,9 +19,7 @@ Copyright © 2019
<a href="https://www.gmtgames.com/p-1009-nevsky-teutons-and-rus-in-collision-1240-1242-2nd-edition.aspx">GMT Games, LLC</a>.
<ul>
-<li><a href="/nevsky/info/rules2.html">Rules of Play (HTML)</a>
-<li><a href="/nevsky/info/rulebook.html">Rules of Play</a>
-<li><a href="/nevsky/info/playbook.html">Background Book</a>
+<li><a href="/nevsky/info/rules.html">Rules of Play</a>
<li><a href="/nevsky/info/pac.html">Reference Sheets</a>
<li><a href="/nevsky/info/cards.html">Arts of War</a>
</ul>
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 @@
+<!DOCTYPE html>
+<!-- vim:set nowrap: -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">
+<meta charset="utf-8">
+<title>NEVSKY</title>
+<link rel="icon" href="favicon.png">
+<link rel="stylesheet" href="/fonts/fonts.css">
+<link rel="stylesheet" href="/common/play.css">
+<script defer src="/common/play.js"></script>
+<script defer src="data.js"></script>
+<script defer src="play.js"></script>
+<style>
+
+main { background-color: dimgray; }
+#roles { background-color: gray; }
+header { background-color: silver; }
+header.your_turn { background-color: orange; }
+#role_Teutons .role_name { background-color: #e1e6e8; }
+#role_Russians .role_name { background-color: #e1d6c1; }
+#turn_info { background-color: gainsboro; }
+.role_supply { float: right; }
+
+#log { background-color: whitesmoke; }
+#log .h1 { background-color: silver; font-weight: bold; padding-top:2px; padding-bottom:2px; text-align: center; }
+#log .h2 { background-color: gainsboro; padding-top:2px; padding-bottom:2px; text-align: center; }
+#log .h3 { background-color: lavender; padding-top:2px; padding-bottom:2px; text-align: center; }
+#log > .i { padding-left: 20px; }
+#log > .ii { padding-left: 32px; }
+#log > div > .i { padding-left: 12px; }
+#log .card_tip { font-style: italic; }
+#log .card_tip:hover { text-decoration: underline; }
+#log .locale_tip:hover { cursor: pointer; text-decoration: underline; }
+
+.action {
+ cursor: pointer;
+}
+
+#log {
+ font-variant-numeric: tabular-nums;
+}
+
+/* DIALOGS */
+
+#arts_of_war {
+ background-color: white;
+ border: 1px solid black;
+ position: fixed;
+ top: 80px;
+ left: 36px;
+ box-shadow: 0px 5px 10px 0px rgba(0,0,0,0.5);
+ z-index: 50;
+ user-select: none;
+}
+
+#arts_of_war_header {
+ font-weight: bold;
+ cursor: move;
+ border-bottom: 1px solid black;
+ background-color: lightsteelblue;
+ padding: 3px 1em;
+}
+
+#arts_of_war_list {
+ padding: 12px;
+ width: calc(186px * 4 + 12px * 3 + 12px);
+ height: 400px;
+ background-color: slategray;
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ overflow-y: scroll;
+ gap: 12px;
+}
+
+/* MATS */
+
+.court {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ margin: 16px auto;
+ gap: 16px;
+}
+
+.mat {
+ position: relative;
+ width: 364px;
+ height: 500px;
+ z-index: 1;
+}
+
+.mat .background {
+ position: absolute;
+ width: 360px;
+ height: 360px;
+ border-radius: 12px;
+ border-width: 2px;
+ border-style: solid;
+ background-color: #d1c07e;
+ background-position: center;
+ background-size: 360px 360px;
+ background-repeat: no-repeat;
+ z-index: 4;
+}
+
+.mat.selected .background {
+ box-shadow: 0 0 0 1px #584800, 0 0 0 3px yellow;
+}
+
+.mat .c1, .mat .c2 {
+ position: absolute;
+ width: 186px;
+ height: 261px;
+ background-color: green;
+}
+
+.mat .c1 { z-index: 3; top: 238px; left: 4px; }
+.mat .c2 { z-index: 2; top: 234px; right: 4px; }
+
+body.shift .mat .card:hover {
+ z-index: 200;
+}
+
+.mat .forces, .mat .routed, .mat .assets, .mat .vassals {
+ position: absolute;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: start;
+ justify-items: center;
+ align-content: center;
+ align-items: center;
+ gap: 4px 6px;
+ z-index: 5;
+ //background-color: #f004;
+}
+
+.mat .forces {
+ justify-content: center;
+}
+
+.mat .forces {
+ gap: 2px;
+}
+
+.mat .forces { top: 90px; left: 60px; width: 240px; height: 96px; }
+.mat .routed { top: 194px; left: 12px; width: 336px; height: 48px; }
+.mat .assets { top: 249px; left: 18px; width: 324px; height: 96px; }
+
+.mat.teutonic.andreas .background { background-image:url(images/mat_teutonic_andreas.png) }
+.mat.teutonic.heinrich .background { background-image:url(images/mat_teutonic_heinrich.png) }
+.mat.teutonic.hermann .background { background-image:url(images/mat_teutonic_hermann.png) }
+.mat.teutonic.knud_and_abel .background { background-image:url(images/mat_teutonic_knud_and_abel.png) }
+.mat.teutonic.rudolf .background { background-image:url(images/mat_teutonic_rudolf.png) }
+.mat.teutonic.yaroslav .background { background-image:url(images/mat_teutonic_yaroslav.png) }
+
+.mat.russian.aleksandr .background { background-image:url(images/mat_russian_aleksandr.png) }
+.mat.russian.andrey .background { background-image:url(images/mat_russian_andrey.png) }
+.mat.russian.domash .background { background-image:url(images/mat_russian_domash.png) }
+.mat.russian.gavrilo .background { background-image:url(images/mat_russian_gavrilo.png) }
+.mat.russian.karelians .background { background-image:url(images/mat_russian_karelians.png) }
+.mat.russian.vladislav .background { background-image:url(images/mat_russian_vladislav.png) }
+
+/* TUCKED CARDS */
+
+.tuck_under_map {
+ margin: 0 auto 36px auto;
+ width: 1275px;
+ min-height: 132px;
+ display: grid;
+ grid-template-columns: auto auto;
+}
+
+.tuck_under_map .card {
+ height: 132px;
+ background-position: 0 100%;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ z-index: 1;
+}
+
+.tuck_under_map .card:hover {
+ height: 253px;
+ margin-bottom: -132px;
+ z-index: 2;
+}
+
+#p1_global, #p2_global {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+
+#p1_global {
+ margin-left: 24px;
+ justify-content: start;
+}
+
+#p2_global {
+ margin-right: 24px;
+ justify-content: end;
+}
+
+/* ASSETS */
+
+.asset {
+ width: 42px;
+ height: 42px;
+ background-size: cover;
+ border: 2px solid transparent;
+ border-radius: 8px;
+ box-shadow: 0 0 0 1px #444, 0 1px 4px #0008;
+}
+
+.asset.action {
+ box-shadow: 0 0 0 1px #444, 0 0 0 3px white;
+}
+
+.asset.selected {
+ box-shadow: 0 0 0 1px #444, 0 0 0 3px yellow;
+}
+
+.asset.boat.x1 { background-image: url(images/asset_boat_x1.png); }
+.asset.boat.x2 { background-image: url(images/asset_boat_x2.png) }
+.asset.boat.x4 { background-image: url(images/asset_boat_x4.png) }
+.asset.cart.x1 { background-image: url(images/asset_cart_x1.png); }
+.asset.cart.x2 { background-image: url(images/asset_cart_x2.png) }
+.asset.cart.x4 { background-image: url(images/asset_cart_x4.png) }
+.asset.coin.x1 { background-image: url(images/asset_coin_x1.png); }
+.asset.coin.x2 { background-image: url(images/asset_coin_x2.png) }
+.asset.coin.x3 { background-image: url(images/asset_coin_x3.png) }
+.asset.coin.x4 { background-image: url(images/asset_coin_x4.png) }
+.asset.loot.x1 { background-image: url(images/asset_loot_x1.png); }
+.asset.loot.x2 { background-image: url(images/asset_loot_x2.png) }
+.asset.loot.x3 { background-image: url(images/asset_loot_x3.png) }
+.asset.loot.x4 { background-image: url(images/asset_loot_x4.png) }
+.asset.prov.x1 { background-image: url(images/asset_prov_x1.png); }
+.asset.prov.x2 { background-image: url(images/asset_prov_x2.png) }
+.asset.prov.x3 { background-image: url(images/asset_prov_x3.png) }
+.asset.prov.x4 { background-image: url(images/asset_prov_x4.png) }
+.asset.ship.x1 { background-image: url(images/asset_ship_x1.png); }
+.asset.ship.x2 { background-image: url(images/asset_ship_x2.png) }
+.asset.ship.x4 { background-image: url(images/asset_ship_x4.png) }
+.asset.sled.x1 { background-image: url(images/asset_sled_x1.png); }
+.asset.sled.x2 { background-image: url(images/asset_sled_x2.png) }
+.asset.sled.x4 { background-image: url(images/asset_sled_x4.png) }
+
+.marker {
+ border: 2px solid aqua;
+ border-radius: 8px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.marker.square {
+ width: 42px;
+ height: 42px;
+}
+
+.marker.small {
+ width: 36px;
+ height: 36px;
+ border-radius: 6px;
+}
+
+.marker.rectangle {
+ width: 90px;
+ height: 42px;
+}
+
+.marker.circle {
+ width: 51px;
+ height: 51px;
+ background-size: 51px 51px;
+ border-radius: 50%;
+}
+
+.marker.pursuit.rot180 {
+ transform: rotate(180deg);
+ border-color: #a68c61 #e7cb9e #e7cb9e #a68c61;
+ box-shadow: 0 0 0 1px #4e370a, -1px -2px 4px #0008;
+}
+
+.marker.turn.levy { background-image: url(images/marker_levy.png) }
+.marker.turn.campaign { background-image: url(images/marker_campaign.png) }
+.marker.storm { background-image: url(images/marker_storm.png) }
+.marker.battle { background-image: url(images/marker_battle.png) }
+.marker.conquered.teutonic { background-image: url(images/marker_conquered_teutonic.png) }
+.marker.conquered.russian { background-image: url(images/marker_conquered_russian.png) }
+.marker.ravaged.teutonic { background-image: url(images/marker_ravaged_teutonic.png) }
+.marker.ravaged.russian { background-image: url(images/marker_ravaged_russian.png) }
+.marker.siege.teutonic { background-image: url(images/marker_siege_teutonic.png) }
+.marker.siege.russian { background-image: url(images/marker_siege_russian.png) }
+.marker.victory.teutonic { background-image: url(images/marker_victory_teutonic.png) }
+.marker.victory.russian { background-image: url(images/marker_victory_russian.png) }
+.marker.victory.half.teutonic { background-image: url(images/marker_victory_half_teutonic.png) }
+.marker.victory.half.russian { background-image: url(images/marker_victory_half_russian.png) }
+.marker.pursuit.teutonic { background-image: url(images/marker_pursuit_teutonic.png) }
+.marker.pursuit.russian { background-image: url(images/marker_pursuit_russian.png) }
+.marker.castle.russian { background-image: url(images/marker_castle_russian.png) }
+.marker.castle.teutonic { background-image: url(images/marker_castle_teutonic.png) }
+.marker.sea_trade_blocked { background-image: url(images/marker_sea_trade_blocked.a.png) }
+
+.marker.number.teutonic.n1 { background-image: url(images/marker_1_teutonic.png) }
+.marker.number.teutonic.n2 { background-image: url(images/marker_2_teutonic.png) }
+.marker.number.teutonic.n3 { background-image: url(images/marker_3_teutonic.png) }
+.marker.number.teutonic.n4 { background-image: url(images/marker_4_teutonic.png) }
+.marker.number.teutonic.n5 { background-image: url(images/marker_5_teutonic.png) }
+.marker.number.teutonic.n6 { background-image: url(images/marker_6_teutonic.png) }
+.marker.number.russian.n1 { background-image: url(images/marker_1_russian.png) }
+.marker.number.russian.n2 { background-image: url(images/marker_2_russian.png) }
+.marker.number.russian.n3 { background-image: url(images/marker_3_russian.png) }
+.marker.number.russian.n4 { background-image: url(images/marker_4_russian.png) }
+.marker.number.russian.n5 { background-image: url(images/marker_5_russian.png) }
+.marker.number.russian.n6 { background-image: url(images/marker_6_russian.png) }
+
+/* UNITS */
+
+.unit {
+ background-size: contain;
+ background-repeat: no-repeat;
+ filter: drop-shadow(0px 2px 2px #0004);
+}
+
+.unit.action {
+ filter:
+ drop-shadow(2px 0px 0px white)
+ drop-shadow(0px 2px 0px white)
+ drop-shadow(0px -2px 0px white)
+ drop-shadow(-2px 0px 0px white);
+}
+
+.unit.knights, .unit.sergeants, .unit.light_horse, .unit.asiatic_horse {
+ width: 24px;
+ height: 28px;
+ width: 30px;
+ height: 35px;
+}
+
+.unit.men_at_arms, .unit.militia, .unit.serfs {
+ width: 34px;
+ height: 28px;
+ width: 43px;
+ height: 35px;
+}
+
+.unit.knights { background-image: url(images/unit_knights.svg) }
+.unit.sergeants { background-image: url(images/unit_sergeants.svg) }
+.unit.light_horse { background-image: url(images/unit_light_horse.svg) }
+.unit.asiatic_horse { background-image: url(images/unit_asiatic_horse.svg) }
+.unit.men_at_arms { background-image: url(images/unit_men_at_arms.svg) }
+.unit.militia { background-image: url(images/unit_militia.svg) }
+.unit.serfs { background-image: url(images/unit_serfs.svg) }
+
+/* CARDS */
+
+.hand {
+ margin: 24px;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+ min-height: 350px;
+ max-width: 1275px;
+ gap: 16px;
+}
+
+.card {
+ background-size: 186px 261px;
+ width: 186px;
+ height: 261px;
+ border-radius: 8px;
+ transition: 100ms;
+}
+
+.card_info {
+ border-bottom: 1px solid black;
+}
+
+.card_info .card {
+ margin: 12px auto;
+}
+
+.card.disabled {
+ filter: grayscale(100%) brightness(66%);
+}
+
+/* MAP */
+
+#mapwrap {
+ width: 1275px;
+ height: 1650px;
+ box-shadow: 0px 1px 10px #0008;
+ z-index: 3;
+}
+
+#map {
+ background-repeat: no-repeat;
+ background-size: cover;
+ width: 1275px;
+ height: 1650px;
+ overflow: clip;
+}
+
+#map { background-image: url(map75.jpg) }
+@media (min-resolution: 97dpi) {
+ #map { background-image: url(map150.jpg) }
+}
+
+.box {
+ position: absolute;
+ border: 3px solid transparent;
+}
+
+.veche_border {
+ position: absolute;
+}
+
+.box.calendar {
+}
+
+.box.victory {
+ border-radius: 50%;
+}
+
+.box.turn {
+ border-radius: 50%;
+}
+
+.box.way.action {
+ border-color: dodgerblue;
+}
+
+.box.way.crossroads.action {
+ border-color: saddlebrown;
+}
+
+.box.way.crossroads { border-radius: 36px; }
+.box.way.wirz { border-radius: 36px 36px 75% 75% }
+.box.way.peipus-east { border-radius: 75% 36px 75% 36px }
+.box.way.peipus-north { border-radius: 36px 36px 36px 75% }
+
+.locale {
+ position: absolute;
+ border: 3px solid transparent;
+}
+
+.locale.action {
+ border-color: white;
+ //box-shadow: 0 0 6px white, inset 0 0 6px 0px white;
+}
+
+.locale.region {
+ border-radius: 50%;
+}
+
+.locale.town {
+ border-radius: 8px;
+}
+
+.locale_extra {
+ position: absolute;
+}
+
+.locale_extra.action {
+ box-shadow: 0 0 8px 2px white, inset 0 0 12px 2px white;
+}
+
+.locale_extra.traderoute { border-radius: 50% 30% 10% 10% }
+.locale_extra.town { border-radius: 50% 50% 0 0 }
+.locale_extra.fort { border-radius: 30% 30% 0 0 }
+.locale_extra.bishopric { border-radius: 50% 50% 20% 20% }
+.locale_extra.city { border-radius: 50% 50% 0 0 }
+.locale_extra.archbishopric { border-radius: 50% 50% 25% 25%; }
+
+.locale.tip, .locale_extra.tip {
+ background-color: #ff06;
+ box-shadow: 0 0 8px #ff06;
+}
+
+#veche { position: absolute; }
+
+.veche_label {
+ pointer-events: none;
+ border: 3px solid transparent;
+}
+
+.veche_border {
+ pointer-events: none;
+}
+
+#veche { left: 1046px; top: 1460px; width: 215px; height: 161px; }
+#veche_border_w { left: 1046px; top: 1460px; width: 50px; height: 157px; }
+#veche_border_e { left: 1208px; top: 1460px; width: 50px; height: 157px; }
+#veche_border_sw { left: 1046px; top: 1618px; width: 59px; height: 0; }
+#veche_border_se { left: 1197px; top: 1618px; width: 64px; height: 0; }
+
+#veche_label_top { left: 1097px; top: 1457px; width: 107px; height: 20px; }
+#veche_label_bottom { left: 1104px; top: 1605px; width: 89px; height: 13px; }
+
+#veche_border_w { border-top: 3px solid transparent; border-left: 3px solid transparent; }
+#veche_border_e { border-top: 3px solid transparent; border-right: 3px solid transparent; }
+#veche_border_sw { border-bottom: 3px solid transparent; }
+#veche_border_se { border-bottom: 3px solid transparent; }
+
+#veche.action {
+ box-shadow: 0 0 8px white;
+}
+
+#veche.action ~ .veche_label {
+ box-shadow: 0 0 8px white;
+ border-color: white;
+}
+
+#veche.action ~ .veche_border {
+ border-color: white;
+}
+
+#vp2.stack:not(.half) ~ #vp1.stack:not(.half) { transform: translate(0px,-36px); }
+#vp2.stack.half ~ #vp1.stack.half { transform: translate(0px,-36px); }
+#vp2.stack.half ~ #vp1.stack:not(.half) { transform: translate(-12px,-30px); }
+#vp2.stack:not(.half) ~ #vp1.stack.half { transform: translate(12px,-30px); }
+
+.marker.victory, .marker.turn {
+ position: absolute;
+}
+
+.v0 { top: 132px; left: 2px; }
+
+.v1, .v2, .v3, .v4, .v5, .v6, .v7, .v8, .t1, .t2, .t3, .t4, .t5, .t6, .t7, .t8 { top: 46px }
+.v9, .v10, .v11, .v12, .v13, .v14, .v15, .v16, .t9, .t10, .t11, .t12, .t13, .t14, .t15, .t16 { top: 284px }
+
+.v1, .v9 { left: 44px }
+.v2, .v10 { left: 196px }
+.v3, .v11 { left: 362px }
+.v4, .v12 { left: 514px }
+.v5, .v13 { left: 680px }
+.v6, .v14 { left: 832px }
+.v7, .v15 { left: 999px }
+.v8, .v16 { left: 1151px }
+
+.t1, .t9 { left: 100px }
+.t2, .t10 { left: 252px }
+.t3, .t11 { left: 418px }
+.t4, .t12 { left: 570px }
+.t5, .t13 { left: 737px }
+.t6, .t14 { left: 888px }
+.t7, .t15 { left: 1055px }
+.t8, .t16 { left: 1207px }
+
+/* 100dpi
+.v1, .v2, .v3, .v4, .v5, .v6, .v7, .v8, .t1, .t2, .t3, .t4, .t5, .t6, .t7, .t8 { top: 68px; }
+.v9, .v10, .v11, .v12, .v13, .v14, .v15, .v16, .t9, .t10, .t11, .t12, .t13, .t14, .t15, .t16 { top: 386px; }
+.v1, .v9 { left: 65px }
+.v2, .v10 { left: 269px }
+.v3, .v11 { left: 490px }
+.v4, .v12 { left: 693px }
+.v5, .v13 { left: 914px }
+.v6, .v14 { left: 1117px }
+.v7, .v15 { left: 1339px }
+.v8, .v16 { left: 1542px }
+.t1, .t9 { left: 141px }
+.t2, .t10 { left: 344px }
+.t3, .t11 { left: 566px }
+.t4, .t12 { left: 769px }
+.t5, .t13 { left: 990px }
+.t6, .t14 { left: 1193px }
+.t7, .t15 { left: 1415px }
+.t8, .t16 { left: 1617px }
+*/
+
+/* PIECES */
+
+#legate, .cylinder, .service_marker {
+ transition-property: top, left;
+ transition-duration: 700ms;
+ transition-timing-function: ease;
+}
+
+#legate {
+ position: absolute;
+ width: 32px;
+ height: 64px;
+ background-image: url(images/legate.svg);
+ top: 815px;
+ left: 200px;
+ filter: drop-shadow(0px 2px 4px #0004);
+}
+
+#legate.action {
+ filter:
+ drop-shadow(2px 0px 0px white)
+ drop-shadow(0px 2px 0px white)
+ drop-shadow(0px -2px 0px white)
+ drop-shadow(-2px 0px 0px white);
+}
+
+.cylinder {
+ position: absolute;
+ width: 44px;
+ height: 48px;
+ background-size: 44px 48px;
+ filter: drop-shadow(0px 2px 4px #0004);
+}
+
+.cylinder.action {
+ filter:
+ drop-shadow(2px 0px 0px white)
+ drop-shadow(0px 2px 0px white)
+ drop-shadow(0px -2px 0px white)
+ drop-shadow(-2px 0px 0px white);
+}
+
+.cylinder.selected {
+ filter:
+ drop-shadow(2px 0px 0px yellow)
+ drop-shadow(0px 2px 0px yellow)
+ drop-shadow(0px -2px 0px yellow)
+ drop-shadow(-2px 0px 0px yellow);
+}
+
+.cylinder.andreas { background-image: url(images/lord_teutonic_1.svg) }
+.cylinder.hermann.marshal { background-image: url(images/lord_teutonic_2.svg) }
+.cylinder.hermann { background-image: url(images/lord_teutonic_3.svg) }
+.cylinder.heinrich { background-image: url(images/lord_teutonic_4.svg) }
+.cylinder.rudolf { background-image: url(images/lord_teutonic_5.svg) }
+.cylinder.knud_and_abel { background-image: url(images/lord_teutonic_6.svg) }
+.cylinder.yaroslav { background-image: url(images/lord_teutonic_7.svg) }
+
+.cylinder.aleksandr { background-image: url(images/lord_russian_1.svg) }
+.cylinder.andrey { background-image: url(images/lord_russian_2.svg) }
+.cylinder.domash { background-image: url(images/lord_russian_3.svg) }
+.cylinder.gavrilo { background-image: url(images/lord_russian_4.svg) }
+.cylinder.vladislav { background-image: url(images/lord_russian_5.svg) }
+.cylinder.karelians { background-image: url(images/lord_russian_6.svg) }
+.cylinder.andrey.marshal { background-image: url(images/lord_russian_7.svg) }
+
+.marker.teutonic,
+.service_marker.teutonic.lord {
+ background-color: #444;
+ border-color: #555 #222 #222 #555;
+ box-shadow: 0 0 0 1px #111, 0 2px 4px #0008;
+}
+
+.marker.russian,
+.service_marker.russian.lord {
+ background-color: #fff;
+ border-color: #eee #ccc #ccc #eee;
+ box-shadow: 0 0 0 1px #555, 0 2px 4px #0008;
+}
+
+.service_marker {
+ background-repeat: no-repeat;
+ width: 90px;
+ height: 42px;
+ border-radius: 8px;
+ box-shadow: 0 1px 6px #0008;
+ border-width: 2px;
+ border-style: solid;
+}
+
+#pieces .service_marker {
+ position: absolute;
+}
+
+.service_marker.teutonic.lord.action,
+.service_marker.teutonic.vassal.action {
+ box-shadow: 0 0 0 3px white;
+}
+
+.service_marker.russian.lord.action,
+.service_marker.russian.vassal.action {
+ box-shadow: 0 0 0 3px pink;
+}
+
+.service_marker.teutonic.lord.selected,
+.service_marker.teutonic.vassal.selected,
+.service_marker.russian.lord.selected,
+.service_marker.russian.vassal.selected {
+ box-shadow: 0 0 0 3px yellow;
+}
+
+.service_marker.lord { background-size: 180px 252px }
+.service_marker.vassal { background-size: 180px 630px }
+
+.service_marker.lord.teutonic { background-image:url(images/service_lords_teutonic.png) }
+.service_marker.lord.russian { background-image:url(images/service_lords_russian.png) }
+.service_marker.vassal.teutonic { background-image:url(images/service_vassals_teutonic.png) }
+.service_marker.vassal.russian { background-image:url(images/service_vassals_russian.png) }
+
+.service_marker.image0{background-position:0 -0px}
+.service_marker.image1{background-position:0 -42px}
+.service_marker.image2{background-position:0 -84px}
+.service_marker.image3{background-position:0 -126px}
+.service_marker.image4{background-position:0 -168px}
+.service_marker.image5{background-position:0 -210px}
+.service_marker.image6{background-position:0 -252px}
+.service_marker.image7{background-position:0 -294px}
+.service_marker.image8{background-position:0 -336px}
+.service_marker.image9{background-position:0 -378px}
+.service_marker.image10{background-position:0 -420px}
+.service_marker.image11{background-position:0 -462px}
+.service_marker.image12{background-position:0 -504px}
+.service_marker.image13{background-position:0 -546px}
+.service_marker.image14{background-position:0 -588px}
+.service_marker.image0:hover{background-position:100% -0px}
+.service_marker.image1:hover{background-position:100% -42px}
+.service_marker.image2:hover{background-position:100% -84px}
+.service_marker.image3:hover{background-position:100% -126px}
+.service_marker.image4:hover{background-position:100% -168px}
+.service_marker.image5:hover{background-position:100% -210px}
+.service_marker.image6:hover{background-position:100% -252px}
+.service_marker.image7:hover{background-position:100% -294px}
+.service_marker.image8:hover{background-position:100% -336px}
+.service_marker.image9:hover{background-position:100% -378px}
+.service_marker.image10:hover{background-position:100% -420px}
+.service_marker.image11:hover{background-position:100% -462px}
+.service_marker.image12:hover{background-position:100% -504px}
+.service_marker.image13:hover{background-position:100% -546px}
+.service_marker.image14:hover{background-position:100% -588px}
+
+body.shift .service_marker:hover { transform: scale(2); z-index: 200; }
+body.shift .cylinder:hover { transform: scale(2); z-index: 200; }
+body.shift .marker:hover { transform: scale(2); z-index: 200; }
+
+/* BACKGROUND COLORS AND BORDERS */
+/* :r !node tools/colors.mjs */
+
+.mat .background { background-color: #d1c07e; border-color: #f2e19d #b1a05f #b1a05f #f2e19d; box-shadow: 0 0 0 1px #584800, 1px 2px 4px #0008; }
+.card.teutonic { background-color: #e1e6e8; border-color: #fbffff #c1c5c7 #c1c5c7 #fbffff; box-shadow: 0 0 0 1px #666a6c, 1px 2px 4px #0008; }
+.card.russian { background-color: #e1d6c1; border-color: #fff7e1 #c1b6a1 #c1b6a1 #fff7e1; box-shadow: 0 0 0 1px #665c4a, 1px 2px 4px #0008; }
+.service_marker.teutonic.vassal { background-color: #777474; border-color: #959292 #5a5858 #5a5858 #959292; box-shadow: 0 0 0 1px #0f0d0d, 1px 2px 4px #0008; }
+.service_marker.russian.vassal { background-color: #f0ead8; border-color: #ffffed #cfc9b8 #cfc9b8 #ffffed; box-shadow: 0 0 0 1px #736e5e, 1px 2px 4px #0008; }
+.asset.sled { background-color: #e5dcc1; border-color: #fffde2 #c5bca1 #c5bca1 #fffde2; box-shadow: 0 0 0 1px #69614a, 1px 2px 4px #0008; }
+.asset.boat { background-color: #adceed; border-color: #cdefff #8eaecc #8eaecc #cdefff; box-shadow: 0 0 0 1px #38556f, 1px 2px 4px #0008; }
+.asset.cart.x1 { background-color: #daba8b; border-color: #fbdaaa #ba9b6d #ba9b6d #fbdaaa; box-shadow: 0 0 0 1px #5f4315, 1px 2px 4px #0008; }
+.asset.cart.x2 { background-color: #d1a973; border-color: #f2c992 #b18a55 #b18a55 #f2c992; box-shadow: 0 0 0 1px #563400, 1px 2px 4px #0008; }
+.asset.cart.x4 { background-color: #c4975b; border-color: #e5b67a #a4793c #a4793c #e5b67a; box-shadow: 0 0 0 1px #4b2500, 1px 2px 4px #0008; }
+.asset.coin.x1 { background-color: #d2d5d4; border-color: #f3f6f5 #b2b5b4 #b2b5b4 #f3f6f5; box-shadow: 0 0 0 1px #595c5b, 1px 2px 4px #0008; }
+.asset.coin.x2 { background-color: #d2d5d4; border-color: #f3f6f5 #b2b5b4 #b2b5b4 #f3f6f5; box-shadow: 0 0 0 1px #595c5b, 1px 2px 4px #0008; }
+.asset.coin.x3 { background-color: #b3b5b4; border-color: #d3d5d4 #949695 #949695 #d3d5d4; box-shadow: 0 0 0 1px #3f4040, 1px 2px 4px #0008; }
+.asset.coin.x4 { background-color: #b3b5b4; border-color: #d3d5d4 #949695 #949695 #d3d5d4; box-shadow: 0 0 0 1px #3f4040, 1px 2px 4px #0008; }
+.asset.prov.x1 { background-color: #ffe293; border-color: #fffdad #dec173 #dec173 #fffdad; box-shadow: 0 0 0 1px #80650a, 1px 2px 4px #0008; }
+.asset.prov.x2 { background-color: #ffe293; border-color: #fffdad #dec173 #dec173 #fffdad; box-shadow: 0 0 0 1px #80650a, 1px 2px 4px #0008; }
+.asset.prov.x3 { background-color: #ffcd66; border-color: #ffee88 #dead43 #dead43 #ffee88; box-shadow: 0 0 0 1px #7f5200, 1px 2px 4px #0008; }
+.asset.prov.x4 { background-color: #ffcd66; border-color: #ffee88 #dead43 #dead43 #ffee88; box-shadow: 0 0 0 1px #7f5200, 1px 2px 4px #0008; }
+.asset.ship.x1 { background-color: #79b7e4; border-color: #98d7ff #5a98c3 #5a98c3 #98d7ff; box-shadow: 0 0 0 1px #004066, 1px 2px 4px #0008; }
+.asset.ship.x2 { background-color: #79b7e4; border-color: #98d7ff #5a98c3 #5a98c3 #98d7ff; box-shadow: 0 0 0 1px #004066, 1px 2px 4px #0008; }
+.asset.ship.x4 { background-color: #5da9dd; border-color: #7dc9ff #3d8abc #3d8abc #7dc9ff; box-shadow: 0 0 0 1px #00335f, 1px 2px 4px #0008; }
+.asset.loot.x1 { background-color: #f0b64f; border-color: #ffd771 #cf9628 #cf9628 #ffd771; box-shadow: 0 0 0 1px #703d00, 1px 2px 4px #0008; }
+.asset.loot.x2 { background-color: #eda44c; border-color: #ffc46e #cb8526 #cb8526 #ffc46e; box-shadow: 0 0 0 1px #6c2c00, 1px 2px 4px #0008; }
+.asset.loot.x3 { background-color: #eb924a; border-color: #ffb26b #c97326 #c97326 #ffb26b; box-shadow: 0 0 0 1px #681800, 1px 2px 4px #0008; }
+.asset.loot.x4 { background-color: #e1884a; border-color: #ffa86a #bf6928 #bf6928 #ffa86a; box-shadow: 0 0 0 1px #5f0c00, 1px 2px 4px #0008; }
+.marker.battle { background-color: #d0bf7d; border-color: #f1e09c #b09f5e #b09f5e #f1e09c; box-shadow: 0 0 0 1px #574700, 1px 2px 4px #0008; }
+.marker.storm { background-color: #d0bf7d; border-color: #f1e09c #b09f5e #b09f5e #f1e09c; box-shadow: 0 0 0 1px #574700, 1px 2px 4px #0008; }
+.marker.pursuit { background-color: #c6ab7f; border-color: #e7cb9e #a68c61 #a68c61 #e7cb9e; box-shadow: 0 0 0 1px #4e370a, 1px 2px 4px #0008; }
+.marker.turn.campaign { background-color: #6a8aa8; border-color: #88a9c8 #4d6c89 #4d6c89 #88a9c8; box-shadow: 0 0 0 1px #001c34, 1px 2px 4px #0008; }
+.marker.turn.levy { background-color: #967348; border-color: #b59165 #78562b #78562b #b59165; box-shadow: 0 0 0 1px #240800, 1px 2px 4px #0008; }
+.marker.teutonic.victory { background-color: #ffd400; border-color: #fff64b #deb300 #deb300 #fff64b; box-shadow: 0 0 0 1px #805600, 1px 2px 4px #0008; }
+.marker.teutonic.siege { background-color: #a39382; border-color: #c3b2a0 #857565 #857565 #c3b2a0; box-shadow: 0 0 0 1px #312416, 1px 2px 4px #0008; }
+.marker.russian.conquered { background-color: #649655; border-color: #82b573 #477838 #477838 #82b573; box-shadow: 0 0 0 1px #002500, 1px 2px 4px #0008; }
+.marker.russian.enemy_lords_removed { background-color: #ffd400; border-color: #fff64b #deb300 #deb300 #fff64b; box-shadow: 0 0 0 1px #805600, 1px 2px 4px #0008; }
+.marker.russian.victory { background-color: #2d8b47; border-color: #4faa64 #006d2a #006d2a #4faa64; box-shadow: 0 0 0 1px #001a00, 1px 2px 4px #0008; }
+.marker.walls { background-color: #e3dedc; border-color: #fffefc #c3bebc #c3bebc #fffefc; box-shadow: 0 0 0 1px #686362, 1px 2px 4px #0008; }
+.marker.russian.number { background-color: #c6992f; border-color: #e7b954 #a67a00 #a67a00 #e7b954; box-shadow: 0 0 0 1px #4d2400, 1px 2px 4px #0008; }
+.marker.teutonic.number { background-color: #a02532; border-color: #c2474e #7e0017 #7e0017 #c2474e; box-shadow: 0 0 0 1px #1a0000, 1px 2px 4px #0008; }
+
+/* CARD IMAGES */
+
+.card.russian.aow_back{background-image:url(cards.1x/aow_russian_back.jpg)}
+.card.teutonic.aow_back{background-image:url(cards.1x/aow_teutonic_back.jpg)}
+
+.card.aow_0{background-image:url(cards.1x/aow_teutonic_01.jpg)}
+.card.aow_1{background-image:url(cards.1x/aow_teutonic_02.jpg)}
+.card.aow_2{background-image:url(cards.1x/aow_teutonic_03.jpg)}
+.card.aow_3{background-image:url(cards.1x/aow_teutonic_04.jpg)}
+.card.aow_4{background-image:url(cards.1x/aow_teutonic_05.jpg)}
+.card.aow_5{background-image:url(cards.1x/aow_teutonic_06.jpg)}
+.card.aow_6{background-image:url(cards.1x/aow_teutonic_07.jpg)}
+.card.aow_7{background-image:url(cards.1x/aow_teutonic_08.jpg)}
+.card.aow_8{background-image:url(cards.1x/aow_teutonic_09.jpg)}
+.card.aow_9{background-image:url(cards.1x/aow_teutonic_10.jpg)}
+.card.aow_10{background-image:url(cards.1x/aow_teutonic_11.jpg)}
+.card.aow_11{background-image:url(cards.1x/aow_teutonic_12.jpg)}
+.card.aow_12{background-image:url(cards.1x/aow_teutonic_13.jpg)}
+.card.aow_13{background-image:url(cards.1x/aow_teutonic_14.jpg)}
+.card.aow_14{background-image:url(cards.1x/aow_teutonic_15.jpg)}
+.card.aow_15{background-image:url(cards.1x/aow_teutonic_16.jpg)}
+.card.aow_16{background-image:url(cards.1x/aow_teutonic_17.jpg)}
+.card.aow_17{background-image:url(cards.1x/aow_teutonic_18.jpg)}
+.card.aow_18{background-image:url(cards.1x/aow_teutonic_none.jpg)}
+.card.aow_19{background-image:url(cards.1x/aow_teutonic_none.jpg)}
+.card.aow_20{background-image:url(cards.1x/aow_teutonic_none.jpg)}
+
+.card.aow_21{background-image:url(cards.1x/aow_russian_01.jpg)}
+.card.aow_22{background-image:url(cards.1x/aow_russian_02.jpg)}
+.card.aow_23{background-image:url(cards.1x/aow_russian_03.jpg)}
+.card.aow_24{background-image:url(cards.1x/aow_russian_04.jpg)}
+.card.aow_25{background-image:url(cards.1x/aow_russian_05.jpg)}
+.card.aow_26{background-image:url(cards.1x/aow_russian_06.jpg)}
+.card.aow_27{background-image:url(cards.1x/aow_russian_07.jpg)}
+.card.aow_28{background-image:url(cards.1x/aow_russian_08.jpg)}
+.card.aow_29{background-image:url(cards.1x/aow_russian_09.jpg)}
+.card.aow_30{background-image:url(cards.1x/aow_russian_10.jpg)}
+.card.aow_31{background-image:url(cards.1x/aow_russian_11.jpg)}
+.card.aow_32{background-image:url(cards.1x/aow_russian_12.jpg)}
+.card.aow_33{background-image:url(cards.1x/aow_russian_13.jpg)}
+.card.aow_34{background-image:url(cards.1x/aow_russian_14.jpg)}
+.card.aow_35{background-image:url(cards.1x/aow_russian_15.jpg)}
+.card.aow_36{background-image:url(cards.1x/aow_russian_16.jpg)}
+.card.aow_37{background-image:url(cards.1x/aow_russian_17.jpg)}
+.card.aow_38{background-image:url(cards.1x/aow_russian_18.jpg)}
+.card.aow_39{background-image:url(cards.1x/aow_russian_none.jpg)}
+.card.aow_40{background-image:url(cards.1x/aow_russian_none.jpg)}
+.card.aow_41{background-image:url(cards.1x/aow_russian_none.jpg)}
+
+.card.cc_russian_back{background-image:url(cards.1x/cc_russian_back.jpg)}
+.card.cc_russian_aleksandr{background-image:url(cards.1x/cc_russian_aleksandr.jpg)}
+.card.cc_russian_andrey{background-image:url(cards.1x/cc_russian_andrey.jpg)}
+.card.cc_russian_domash{background-image:url(cards.1x/cc_russian_domash.jpg)}
+.card.cc_russian_gavrilo{background-image:url(cards.1x/cc_russian_gavrilo.jpg)}
+.card.cc_russian_karelians{background-image:url(cards.1x/cc_russian_karelians.jpg)}
+.card.cc_russian_vladislav{background-image:url(cards.1x/cc_russian_vladislav.jpg)}
+.card.cc_russian_pass{background-image:url(cards.1x/cc_russian_pass.jpg)}
+
+.card.cc_teutonic_back{background-image:url(cards.1x/cc_teutonic_back.jpg)}
+.card.cc_teutonic_andreas{background-image:url(cards.1x/cc_teutonic_andreas.jpg)}
+.card.cc_teutonic_heinrich{background-image:url(cards.1x/cc_teutonic_heinrich.jpg)}
+.card.cc_teutonic_hermann{background-image:url(cards.1x/cc_teutonic_hermann.jpg)}
+.card.cc_teutonic_knud_and_abel{background-image:url(cards.1x/cc_teutonic_knud_and_abel.jpg)}
+.card.cc_teutonic_rudolf{background-image:url(cards.1x/cc_teutonic_rudolf.jpg)}
+.card.cc_teutonic_yaroslav{background-image:url(cards.1x/cc_teutonic_yaroslav.jpg)}
+.card.cc_teutonic_pass{background-image:url(cards.1x/cc_teutonic_pass.jpg)}
+
+@media (min-resolution: 97dpi) {
+.card.russian.aow_back{background-image:url(cards.1x/aow_russian_back.jpg)}
+.card.teutonic.aow_back{background-image:url(cards.1x/aow_teutonic_back.jpg)}
+.card.aow_0{background-image:url(cards.1x/aow_russian_01.jpg)}
+.card.aow_1{background-image:url(cards.1x/aow_russian_02.jpg)}
+.card.aow_2{background-image:url(cards.1x/aow_russian_03.jpg)}
+.card.aow_3{background-image:url(cards.1x/aow_russian_04.jpg)}
+.card.aow_4{background-image:url(cards.1x/aow_russian_05.jpg)}
+.card.aow_5{background-image:url(cards.1x/aow_russian_06.jpg)}
+.card.aow_6{background-image:url(cards.1x/aow_russian_07.jpg)}
+.card.aow_7{background-image:url(cards.1x/aow_russian_08.jpg)}
+.card.aow_8{background-image:url(cards.1x/aow_russian_09.jpg)}
+.card.aow_9{background-image:url(cards.1x/aow_russian_10.jpg)}
+.card.aow_10{background-image:url(cards.1x/aow_russian_11.jpg)}
+.card.aow_11{background-image:url(cards.1x/aow_russian_12.jpg)}
+.card.aow_12{background-image:url(cards.1x/aow_russian_13.jpg)}
+.card.aow_13{background-image:url(cards.1x/aow_russian_14.jpg)}
+.card.aow_14{background-image:url(cards.1x/aow_russian_15.jpg)}
+.card.aow_15{background-image:url(cards.1x/aow_russian_16.jpg)}
+.card.aow_16{background-image:url(cards.1x/aow_russian_17.jpg)}
+.card.aow_17{background-image:url(cards.1x/aow_russian_18.jpg)}
+.card.aow_18{background-image:url(cards.1x/aow_russian_none.jpg)}
+.card.aow_19{background-image:url(cards.1x/aow_russian_none.jpg)}
+.card.aow_20{background-image:url(cards.1x/aow_russian_none.jpg)}
+.card.aow_21{background-image:url(cards.1x/aow_teutonic_01.jpg)}
+.card.aow_22{background-image:url(cards.1x/aow_teutonic_02.jpg)}
+.card.aow_23{background-image:url(cards.1x/aow_teutonic_03.jpg)}
+.card.aow_24{background-image:url(cards.1x/aow_teutonic_04.jpg)}
+.card.aow_25{background-image:url(cards.1x/aow_teutonic_05.jpg)}
+.card.aow_26{background-image:url(cards.1x/aow_teutonic_06.jpg)}
+.card.aow_27{background-image:url(cards.1x/aow_teutonic_07.jpg)}
+.card.aow_28{background-image:url(cards.1x/aow_teutonic_08.jpg)}
+.card.aow_29{background-image:url(cards.1x/aow_teutonic_09.jpg)}
+.card.aow_30{background-image:url(cards.1x/aow_teutonic_10.jpg)}
+.card.aow_31{background-image:url(cards.1x/aow_teutonic_11.jpg)}
+.card.aow_32{background-image:url(cards.1x/aow_teutonic_12.jpg)}
+.card.aow_33{background-image:url(cards.1x/aow_teutonic_13.jpg)}
+.card.aow_34{background-image:url(cards.1x/aow_teutonic_14.jpg)}
+.card.aow_35{background-image:url(cards.1x/aow_teutonic_15.jpg)}
+.card.aow_36{background-image:url(cards.1x/aow_teutonic_16.jpg)}
+.card.aow_37{background-image:url(cards.1x/aow_teutonic_17.jpg)}
+.card.aow_38{background-image:url(cards.1x/aow_teutonic_18.jpg)}
+.card.aow_39{background-image:url(cards.1x/aow_teutonic_none.jpg)}
+.card.aow_40{background-image:url(cards.1x/aow_teutonic_none.jpg)}
+.card.aow_41{background-image:url(cards.1x/aow_teutonic_none.jpg)}
+
+.card.cc_russian_back{background-image:url(cards.2x/cc_russian_back.jpg)}
+.card.cc_russian_aleksandr{background-image:url(cards.2x/cc_russian_aleksandr.jpg)}
+.card.cc_russian_andrey{background-image:url(cards.2x/cc_russian_andrey.jpg)}
+.card.cc_russian_domash{background-image:url(cards.2x/cc_russian_domash.jpg)}
+.card.cc_russian_gavrilo{background-image:url(cards.2x/cc_russian_gavrilo.jpg)}
+.card.cc_russian_karelians{background-image:url(cards.2x/cc_russian_karelians.jpg)}
+.card.cc_russian_vladislav{background-image:url(cards.2x/cc_russian_vladislav.jpg)}
+.card.cc_russian_pass{background-image:url(cards.2x/cc_russian_pass.jpg)}
+.card.cc_teutonic_back{background-image:url(cards.2x/cc_teutonic_back.jpg)}
+.card.cc_teutonic_andreas{background-image:url(cards.2x/cc_teutonic_andreas.jpg)}
+.card.cc_teutonic_heinrich{background-image:url(cards.2x/cc_teutonic_heinrich.jpg)}
+.card.cc_teutonic_hermann{background-image:url(cards.2x/cc_teutonic_hermann.jpg)}
+.card.cc_teutonic_knud_and_abel{background-image:url(cards.2x/cc_teutonic_knud_and_abel.jpg)}
+.card.cc_teutonic_rudolf{background-image:url(cards.2x/cc_teutonic_rudolf.jpg)}
+.card.cc_teutonic_yaroslav{background-image:url(cards.2x/cc_teutonic_yaroslav.jpg)}
+.card.cc_teutonic_pass{background-image:url(cards.2x/cc_teutonic_pass.jpg)}
+}
+
+</style>
+</head>
+<body>
+
+<div id="arts_of_war" class="hide">
+<div id="arts_of_war_header">Arts of War</div>
+<div id="arts_of_war_list"></div>
+</div>
+
+<header>
+ <div id="toolbar">
+ <div class="menu">
+ <div class="menu_title"><img src="/images/cog.svg"></div>
+ <div class="menu_popup">
+ <a class="menu_item" target="_blanK" href="/nevsky/info/rules.html">Rules of Play</a>
+ <a class="menu_item" target="_blanK" href="/nevsky/info/pac.html">Reference Sheets</a>
+ <a class="menu_item" target="_blanK" href="/nevsky/info/cards.html">Arts of War</a>
+ <div class="resign menu_separator"></div>
+ <div class="resign menu_item" onclick="confirm_resign()">Resign</div>
+ <div class="debug menu_separator"></div>
+ <div class="debug menu_item" onclick="send_save()">&#x1F41E; Save</div>
+ <div class="debug menu_item" onclick="send_restore()">&#x1F41E; Restore</div>
+ <div class="debug menu_item" onclick="send_restart('Pleskau')">&#x26a0; Pleskau</div>
+ <div class="debug menu_item" onclick="send_restart('Watland')">&#x26a0; Watland</div>
+ <div class="debug menu_item" onclick="send_restart('Peipus')">&#x26a0; Peipus</div>
+ <div class="debug menu_item" onclick="send_restart('Quickstart')">&#x26a0; Quickstart</div>
+ </div>
+ </div>
+ <div class="icon_button" onclick="toggle_pieces()"><img src="/images/earth-africa-europe.svg"></div>
+ <div class="icon_button" onclick="toggle_log()"><img src="/images/scroll-quill.svg"></div>
+ </div>
+ <div id="prompt"></div>
+ <div id="actions"></div>
+</header>
+
+<aside>
+ <div id="roles">
+ <div class="role" id="role_Teutons">
+ <div class="role_name">
+ Teutons
+ <div class="role_user">-</div>
+ </div>
+ </div>
+ <div class="role" id="role_Russians">
+ <div class="role_name">
+ Russians
+ <div class="role_user">-</div>
+ </div>
+ </div>
+ <div class="card_info"><div id="last_card" class="card russian cc_russian_back"></div></div>
+ </div>
+ <div id="log"></div>
+</aside>
+
+<main>
+<div id="mapwrap">
+<div id="map">
+
+<div id="veche"></div>
+<div class="box veche_label" id="veche_label_top"></div>
+<div class="box veche_label" id="veche_label_bottom"></div>
+<div class="veche_border" id="veche_border_w"></div>
+<div class="veche_border" id="veche_border_e"></div>
+<div class="veche_border" id="veche_border_sw"></div>
+<div class="veche_border" id="veche_border_se"></div>
+
+<div id="locales"></div>
+<div id="boxes"></div>
+
+<div id="pieces">
+ <div id="turn" class="marker circle turn levy t1"></div>
+ <div id="vp2" class="marker circle victory russian v0 stack"></div>
+ <div id="vp1" class="marker circle victory teutonic v0 stack"></div>
+ <div id="legate"></div>
+</div>
+
+</div>
+</div>
+
+<div class="tuck_under_map">
+<div id="p1_global" class="global"></div>
+<div id="p2_global" class="global"></div>
+</div>
+
+<div id="p1_court" class="court"></div>
+<div id="p2_court" class="court"></div>
+<div id="hand" class="hand"></div>
+
+</main>
+
+<footer id="status"></footer>
+
+</body>
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 `<span class="card_tip" onmouseenter="on_focus_card_tip(${x})" onmouseleave="on_blur_card_tip(${x})">${n}</span>`
+}
+
+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 `<span class="locale_tip" onmouseenter="on_focus_locale_tip(${x})" onmouseleave="on_blur_locale_tip(${x})" onclick="on_click_locale_tip(${x})">${n}</span>`
+}
+
+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, "&amp;")
+ text = text.replace(/</g, "&lt;")
+ text = text.replace(/>/g, "&gt;")
+
+ 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")