summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2024-08-24 01:17:17 +0200
committerTor Andersson <tor@ccxvii.net>2024-08-24 01:19:42 +0200
commit3820856482017e0f206b0481856a86a2b4fc589c (patch)
treeff57b98648670583d0796d6ee0cbe500f7502f75
parent1c90c9ab2b7a97078d6a853c9d37d19e8a437ec8 (diff)
downloadvijayanagara-3820856482017e0f206b0481856a86a2b4fc589c.tar.gz
Start client.
-rw-r--r--play.css436
-rw-r--r--play.html115
-rw-r--r--play.js1247
-rw-r--r--rules.js75
-rw-r--r--tools/layout.svg378
-rw-r--r--tools/parse-layout.js187
6 files changed, 2435 insertions, 3 deletions
diff --git a/play.css b/play.css
new file mode 100644
index 0000000..f33c156
--- /dev/null
+++ b/play.css
@@ -0,0 +1,436 @@
+main { background-color: #777; }
+
+#role_DS { background-color: silver }
+#role_BK { background-color: turquoise }
+#role_VK { background-color: gold }
+#role_Solo { background-image: linear-gradient(120deg, gray, turquoise, gold) }
+
+.role.active span { text-decoration: underline; }
+
+/* LAYOUT */
+
+#card_tip {
+ position: fixed;
+ z-index: 100;
+ right: 240px;
+ top: 60px;
+}
+
+#card_panel {
+ display: flex;
+ flex-wrap: wrap;
+ align-content: start;
+ justify-content: center;
+ gap: 16px;
+ padding: 16px;
+}
+
+@media (max-width: 800px) {
+ #card_panel {
+ justify-content: start;
+ }
+}
+
+#table {
+ display: grid;
+ width: 100%;
+ grid-template-columns: 1fr min-content min-content 1fr;
+ grid-template-rows: min-content max-content;
+}
+
+#mapwrap {
+ grid-column: 2;
+ grid-row: 1;
+}
+
+#card_panel {
+ grid-column: 2;
+ grid-row: 2;
+}
+
+#mapwrap[data-fit="both"] + #card_panel {
+ max-width: 250px;
+ grid-column: 3;
+ grid-row: 1;
+}
+
+@media (min-width: 2000px) {
+ #card_panel {
+ max-width: 250px;
+ grid-column: 3;
+ grid-row: 1;
+ }
+}
+
+/* LOG */
+
+#log .h1 { background-image: linear-gradient(60deg, gray, turquoise, gold); text-shadow: 0 0 4px white; }
+
+#log { font-variant-numeric: tabular-nums; }
+
+/* MAP */
+
+#mapwrap {
+ box-shadow: 0px 1px 10px #0008;
+ width: 1275px;
+ height: 1650px;
+ margin-bottom: 24px;
+}
+
+#mapwrap[data-fit="width"], #mapwrap[data-fit="both"] {
+ margin-bottom: 0;
+}
+
+#map {
+ width: 1275px;
+ height: 1650px;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+#map { background-image: url("map75.jpg") }
+@media (min-resolution: 97dpi) {
+ #map { background-image: url("map150.jpg") }
+}
+
+#svgmap { position: absolute; }
+#tokens { position: absolute; }
+#pieces { position: absolute; }
+#boxes { position: absolute; }
+
+#map.hide_pieces #pieces { display: none; }
+#map.hide_tokens #tokens { display: none; }
+
+/* SPACES */
+
+path { fill: transparent; stroke-width: 4; }
+path.action { fill: white; fill-opacity: 0.3; stroke: white; }
+path.selected { stroke: yellow; }
+path.tip { stroke: white; stroke-dasharray: 4 4; }
+
+.space {
+ position: absolute;
+ border: 2px solid lime;
+}
+
+.space.province {
+ border-radius: 50%;
+}
+
+.box{position:absolute;box-sizing:border-box;border:4px solid transparent}
+.box.action{border-color:lemonchiffon;}
+.box.selected{border-color:yellow;}
+.box.tip { border-color: white; }
+
+/* PIECES */
+
+.piece {
+ position: absolute;
+ pointer-events: none;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ transition-property: top, left;
+ transition-duration: 700ms;
+ transition-timing-function: ease;
+ filter: drop-shadow(0 1px 1px #0006);
+}
+.piece.action {
+ pointer-events: auto;
+ filter:
+ drop-shadow(0 -2px 0 white)
+ drop-shadow(0 2px 0 white)
+ drop-shadow(-2px 0 0 white)
+ drop-shadow(2px 0 0 white)
+ ;
+}
+.piece.selected {
+ pointer-events: auto;
+ filter:
+ drop-shadow(0 -2px 0 yellow)
+ drop-shadow(0 2px 0 yellow)
+ drop-shadow(-2px 0 0 yellow)
+ drop-shadow(2px 0 0 yellow)
+ ;
+}
+
+.cylinder { width: 44px; height: 48px; }
+.disk { width: 44px; height: 38px; }
+.governor, .amir, .raja { width: 29px; height: 36px; }
+.cube { width: 29px; height: 36px; }
+
+.ds.cylinder { background-image: url(pieces/ds_cylinder.svg) }
+.ds.cube { background-image: url(pieces/ds_troop.svg) }
+.ds.disk { background-image: url(pieces/ds_disk.svg) }
+.ds.governor { background-image: url(pieces/ds_governor.svg) }
+.bk.cylinder { background-image: url(pieces/bk_cylinder.svg) }
+.bk.amir { background-image: url(pieces/bk_amir.svg) }
+.bk.amir.rebel { background-image: url(pieces/bk_amir_rebel.svg) }
+.bk.disk { background-image: url(pieces/bk_disk.svg) }
+.ve.cylinder { background-image: url(pieces/ve_cylinder.svg) }
+.ve.raja { background-image: url(pieces/ve_raja.svg) }
+.ve.raja.rebel { background-image: url(pieces/ve_raja_rebel.svg) }
+.ve.disk { background-image: url(pieces/ve_disk.svg) }
+.mongol.cube { background-image: url(pieces/mongol_invader.svg) }
+
+.disk { border-radius: 15px; }
+.noble { border-radius: 8px; }
+.cube { border-radius: 14px 14px / 10px 10px; }
+
+/* TOKENS */
+
+.token {
+ pointer-events: none;
+ position: absolute;
+ transition-property: top, left;
+ transition-duration: 700ms;
+ transition-timing-function: ease;
+ background-repeat: no-repeat;
+ border-radius: 8px;
+ border: 2px outset gray;
+ box-shadow: 0 0 0px 1px #222;
+ background-size: 45px 45px;
+ width: 45px;
+ height: 45px;
+}
+
+.token.action {
+ pointer-events: auto;
+}
+
+.token.tributary, .token.ds_ctl, .token.bk_ctl, .token.ve_ctl { border-radius: 50% }
+
+/* CARDS */
+
+.card {
+ width: 250px;
+ height: 350px;
+ border-radius: 16px;
+ background-color: #c6bb8d;
+ background-size: cover;
+ background-repeat: no-repeat;
+ box-shadow: 0 0 0 1px #223f21, 1px 1px 4px #0008;
+}
+
+.card.card_back {
+ background-color: orange;
+}
+
+.card.action {
+ box-shadow: 0 0 0 3px white;
+}
+
+.card.momentum {
+ width: 186px;
+ height: 261px;
+ border-radius: 12px;
+}
+
+#deck_outer { position: relative; }
+#deck_size {
+ position: absolute;
+ right: 24px;
+ bottom: 16px;
+ font-size: 24px;
+ font-weight: bold;
+ color: white;
+}
+
+#of_gods_and_kings { position: relative; }
+#of_gods_and_kings_label {
+ position: absolute;
+ top: 6px;
+ left: 6px;
+ right: 6px;
+ border-radius: 16px;
+ text-align: center;
+ background-color: #5c4f23;
+ font-size: 16px;
+ font-weight: bold;
+ color: white;
+}
+
+#this_card { position: relative; }
+
+#this_card.c #shaded_event { border-top-color: transparent; }
+#this_card.c #unshaded_event { border-image: radial-gradient(100px 30px at bottom, transparent 65%, white) 3 }
+
+#unshaded_event, #shaded_event {
+ display: none;
+ position: absolute;
+ box-sizing: border-box;
+ border: 3px solid white;
+}
+
+#shaded_event.action, #unshaded_event.action {
+ display: block;
+}
+
+#unshaded_event {
+ left: 3px;
+ right: 3px;
+ top: 200px;
+ height: 70px;
+ border-radius: 12px;
+}
+
+#shaded_event {
+ left: 3px;
+ right: 3px;
+ bottom: 4px;
+ height: 80px;
+ border-radius: 12px;
+}
+
+/* TOKEN IMAGES */
+
+.token.tributary { background-color: #2a2c26 }
+.token.ds_ctl { background-color: #433e1d }
+.token.bk_ctl { background-color: #15908a }
+.token.ve_ctl { background-color: #fc7b0d }
+#token_ds_vp { background-color: #2a2c26 }
+#token_bk_vp { background-color: #15908a }
+#token_ve_vp { background-color: #fc7b0d }
+#token_bk_influence { background-color: #02766f }
+#token_ve_influence { background-color: #fdbb47 }
+#token_mongol_cavalry { background-color: #58291f }
+.token.cavalry.charge { background-color: #6c4a2f }
+.token.cavalry.screen { background-color: #363d29 }
+
+.token.tributary { border-color: #2a2c26 }
+.token.ds_ctl { border-color: #433e1d }
+.token.bk_ctl { border-color: #15908a }
+.token.ve_ctl { border-color: #fc7b0d }
+#token_ds_vp { border-color: #2a2c26 }
+#token_bk_vp { border-color: #15908a }
+#token_ve_vp { border-color: #fc7b0d }
+#token_bk_influence { border-color: #02766f }
+#token_ve_influence { border-color: #fdbb47 }
+#token_mongol_cavalry { border-color: #58291f }
+.token.cavalry.charge { border-color: #6c4a2f }
+.token.cavalry.screen { border-color: #363d29 }
+
+.token.tributary { background-image: url(pieces/Tributary.png) }
+.token.ds_ctl { background-image: url(pieces/Flag_DS.png) }
+.token.bk_ctl { background-image: url(pieces/Flag_BK.png) }
+.token.ve_ctl { background-image: url(pieces/Flag_VE.png) }
+#token_ds_vp { background-image: url(pieces/Victory_DS.png) }
+#token_bk_vp { background-image: url(pieces/Victory_BK.png) }
+#token_ve_vp { background-image: url(pieces/Victory_VE.png) }
+#token_bk_influence { background-image: url(pieces/Influence_BK.png) }
+#token_ve_influence { background-image: url(pieces/Influence_VE.png) }
+#token_mongol_cavalry { background-image: url(pieces/Cavalry_Mongol.png) }
+.token.cavalry.charge { background-image: url(pieces/Cavalry_Charge.png) }
+.token.cavalry.screen { background-image: url(pieces/Cavalry_Screen.png) }
+
+/* CARD IMAGES */
+
+.card.card_back{background-image:url(cards100/card_back.jpg)}
+.card.card_1{background-image:url(cards100/event_1.jpg)}
+.card.card_2{background-image:url(cards100/event_2.jpg)}
+.card.card_3{background-image:url(cards100/event_3.jpg)}
+.card.card_4{background-image:url(cards100/event_4.jpg)}
+.card.card_5{background-image:url(cards100/event_5.jpg)}
+.card.card_6{background-image:url(cards100/event_6.jpg)}
+.card.card_7{background-image:url(cards100/event_7.jpg)}
+.card.card_8{background-image:url(cards100/event_8.jpg)}
+.card.card_9{background-image:url(cards100/event_9.jpg)}
+.card.card_10{background-image:url(cards100/event_10.jpg)}
+.card.card_11{background-image:url(cards100/event_11.jpg)}
+.card.card_12{background-image:url(cards100/event_12.jpg)}
+.card.card_13{background-image:url(cards100/event_13.jpg)}
+.card.card_14{background-image:url(cards100/event_14.jpg)}
+.card.card_15{background-image:url(cards100/event_15.jpg)}
+.card.card_16{background-image:url(cards100/event_16.jpg)}
+.card.card_17{background-image:url(cards100/event_17.jpg)}
+.card.card_18{background-image:url(cards100/event_18.jpg)}
+.card.card_19{background-image:url(cards100/event_19.jpg)}
+.card.card_20{background-image:url(cards100/event_20.jpg)}
+.card.card_21{background-image:url(cards100/event_21.jpg)}
+.card.card_22{background-image:url(cards100/event_22.jpg)}
+.card.card_23{background-image:url(cards100/event_23.jpg)}
+.card.card_24{background-image:url(cards100/event_24.jpg)}
+.card.card_25{background-image:url(cards100/event_25.jpg)}
+.card.card_26{background-image:url(cards100/event_26.jpg)}
+.card.card_27{background-image:url(cards100/event_27.jpg)}
+.card.card_28{background-image:url(cards100/event_28.jpg)}
+.card.card_29{background-image:url(cards100/event_29.jpg)}
+.card.card_30{background-image:url(cards100/event_30.jpg)}
+.card.card_31{background-image:url(cards100/event_31.jpg)}
+.card.card_32{background-image:url(cards100/event_32.jpg)}
+.card.card_33{background-image:url(cards100/event_33.jpg)}
+.card.card_34{background-image:url(cards100/event_34.jpg)}
+.card.card_35{background-image:url(cards100/event_35.jpg)}
+.card.card_36{background-image:url(cards100/event_36.jpg)}
+.card.card_37{background-image:url(cards100/Mongol_Invasion_BK_front.jpg)}
+.card.card_38{background-image:url(cards100/Mongol_Invasion_BK_front.jpg)}
+.card.card_39{background-image:url(cards100/Mongol_Invasion_BK_front.jpg)}
+.card.card_40{background-image:url(cards100/Mongol_Invasion_BK_front.jpg)}
+.card.card_41{background-image:url(cards100/Mongol_Invasion_BK_front.jpg)}
+.card.card_42{background-image:url(cards100/Mongol_Invasion_VE_front.jpg)}
+.card.card_43{background-image:url(cards100/Mongol_Invasion_VE_front.jpg)}
+.card.card_44{background-image:url(cards100/Mongol_Invasion_VE_front.jpg)}
+.card.card_45{background-image:url(cards100/Mongol_Invasion_VE_front.jpg)}
+.card.card_46{background-image:url(cards100/Timurid_Empire_front.jpg)}
+.card.card_47{background-image:url(cards100/Timurid_Empire_front.jpg)}
+.card.card_48{background-image:url(cards100/Succession1_front.jpg)}
+.card.card_49{background-image:url(cards100/Succession2_front.jpg)}
+.card.card_50{background-image:url(cards100/Succession3_front.jpg)}
+.card.card_dynasty_khalji{background-image:url(cards100/Dynasty_front.jpg)}
+.card.card_dynasty_khalji{background-image:url(cards100/Dynasty_back.jpg)}
+
+@media (min-resolution: 97dpi) {
+.card.card_back{background-image:url(cards200/card_back.jpg)}
+.card.card_1{background-image:url(cards200/event_1.jpg)}
+.card.card_2{background-image:url(cards200/event_2.jpg)}
+.card.card_3{background-image:url(cards200/event_3.jpg)}
+.card.card_4{background-image:url(cards200/event_4.jpg)}
+.card.card_5{background-image:url(cards200/event_5.jpg)}
+.card.card_6{background-image:url(cards200/event_6.jpg)}
+.card.card_7{background-image:url(cards200/event_7.jpg)}
+.card.card_8{background-image:url(cards200/event_8.jpg)}
+.card.card_9{background-image:url(cards200/event_9.jpg)}
+.card.card_10{background-image:url(cards200/event_10.jpg)}
+.card.card_11{background-image:url(cards200/event_11.jpg)}
+.card.card_12{background-image:url(cards200/event_12.jpg)}
+.card.card_13{background-image:url(cards200/event_13.jpg)}
+.card.card_14{background-image:url(cards200/event_14.jpg)}
+.card.card_15{background-image:url(cards200/event_15.jpg)}
+.card.card_16{background-image:url(cards200/event_16.jpg)}
+.card.card_17{background-image:url(cards200/event_17.jpg)}
+.card.card_18{background-image:url(cards200/event_18.jpg)}
+.card.card_19{background-image:url(cards200/event_19.jpg)}
+.card.card_20{background-image:url(cards200/event_20.jpg)}
+.card.card_21{background-image:url(cards200/event_21.jpg)}
+.card.card_22{background-image:url(cards200/event_22.jpg)}
+.card.card_23{background-image:url(cards200/event_23.jpg)}
+.card.card_24{background-image:url(cards200/event_24.jpg)}
+.card.card_25{background-image:url(cards200/event_25.jpg)}
+.card.card_26{background-image:url(cards200/event_26.jpg)}
+.card.card_27{background-image:url(cards200/event_27.jpg)}
+.card.card_28{background-image:url(cards200/event_28.jpg)}
+.card.card_29{background-image:url(cards200/event_29.jpg)}
+.card.card_30{background-image:url(cards200/event_30.jpg)}
+.card.card_31{background-image:url(cards200/event_31.jpg)}
+.card.card_32{background-image:url(cards200/event_32.jpg)}
+.card.card_33{background-image:url(cards200/event_33.jpg)}
+.card.card_34{background-image:url(cards200/event_34.jpg)}
+.card.card_35{background-image:url(cards200/event_35.jpg)}
+.card.card_36{background-image:url(cards200/event_36.jpg)}
+.card.card_37{background-image:url(cards200/Mongol_Invasion_BK_front.jpg)}
+.card.card_38{background-image:url(cards200/Mongol_Invasion_BK_front.jpg)}
+.card.card_39{background-image:url(cards200/Mongol_Invasion_BK_front.jpg)}
+.card.card_40{background-image:url(cards200/Mongol_Invasion_BK_front.jpg)}
+.card.card_41{background-image:url(cards200/Mongol_Invasion_BK_front.jpg)}
+.card.card_42{background-image:url(cards200/Mongol_Invasion_VE_front.jpg)}
+.card.card_43{background-image:url(cards200/Mongol_Invasion_VE_front.jpg)}
+.card.card_44{background-image:url(cards200/Mongol_Invasion_VE_front.jpg)}
+.card.card_45{background-image:url(cards200/Mongol_Invasion_VE_front.jpg)}
+.card.card_46{background-image:url(cards200/Timurid_Empire_front.jpg)}
+.card.card_47{background-image:url(cards200/Timurid_Empire_front.jpg)}
+.card.card_48{background-image:url(cards200/Succession1_front.jpg)}
+.card.card_49{background-image:url(cards200/Succession2_front.jpg)}
+.card.card_50{background-image:url(cards200/Succession3_front.jpg)}
+.card.card_dynasty_khalji{background-image:url(cards200/Dynasty_front.jpg)}
+.card.card_dynasty_khalji{background-image:url(cards200/Dynasty_back.jpg)}
+}
diff --git a/play.html b/play.html
new file mode 100644
index 0000000..f0b502b
--- /dev/null
+++ b/play.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!-- vim:set nowrap: -->
+<html lang="en">
+<head>
+<meta name="viewport" content="width=device-width, user-scalable=no, interactive-widget=resizes-content, viewport-fit=cover">
+<meta name="theme-color" content="#444">
+<meta charset="UTF-8">
+<title>VIJAYANAGARA</title>
+<link id="favicon" rel="icon" href="images/Flags_VE.png">
+<link rel="stylesheet" href="/fonts/fonts.css">
+<link rel="stylesheet" href="/common/client.css">
+<link rel="stylesheet" href="play.css">
+<script defer src="/common/client.js"></script>
+<script defer src="data.js"></script>
+<script defer src="play.js"></script>
+</head>
+<body>
+
+<div id="card_tip" class="hide"></div>
+
+<header>
+ <div id="toolbar">
+ <details>
+ <summary>
+ <img src="/images/cog.svg">
+ </summary>
+ <menu>
+ <li><a href="info/learn-to-play.html" target="_blank")">Learn to Play</a>
+ <li><a href="info/rulebook.html" target="_blank")">Rulebook</a>
+ <li><a href="info/reference.html" target="_blank")">Referenece</a>
+ <li><a href="info/guides.html" target="_blank")">Strategy</a>
+ <li><a href="info/cards.html" target="_blank")">Cards</a>
+ </menu>
+ </details>
+ <details id="negotiate_menu">
+ <summary>
+ <img src="images/shaking-hands.svg">
+ </summary>
+ <menu>
+ <li id="transfer_resources_menu" onclick="send_action('transfer_resources')">Transfer Resources
+ <li id="transfer_cavalry_menu" onclick="send_action('transfer_cavalry')">Transfer Cavalry
+ <li id="ask_resources_menu" onclick="send_action('ask_resources')">Ask for Resources
+ <li id="ask_cavalry_menu" onclick="send_action('ask_cavalry')">Ask for Cavalry
+ </menu>
+ </details>
+ <button onclick="toggle_pieces()"><img src="/images/earth-asia-oceania.svg"></button>
+ </div>
+</header>
+
+<aside>
+ <div id="roles"></div>
+ <div id="log"></div>
+</aside>
+
+<main data-max-zoom="1" data-map-width="1275" data-map-height="1650">
+
+<div id="table">
+
+<div id="mapwrap">
+ <div id="map">
+ <svg id="svgmap" width="1650" height="2550" viewBox="0 0 1650 2550">
+ <!-- TODO: region paths -->
+ </svg>
+
+ <div id="spaces">
+ <div id="mongol_invaders" class="box" style="top:1266px;left:684px;width:141px;height:140px"></div>
+ <div id="available_ds" class="box" style="top:1266px;left:684px;width:141px;height:140px"></div>
+ <div id="available_bk" class="box" style="top:1266px;left:684px;width:141px;height:140px"></div>
+ <div id="available_ve" class="box" style="top:1266px;left:684px;width:141px;height:140px"></div>
+ </div>
+
+ <div id="tokens">
+ <div class="token" id="token_ds_vp" style="left:1204px;top:20px"></div>
+ <div class="token" id="token_bk_vp" style="left:23px;top:8px"></div>
+ <div class="token" id="token_ve_vp" style="left:23px;top:36px"></div>
+
+ <div class="token" id="token_bk_influence" style="left:25px;top:1195px"></div>
+ <div class="token" id="token_ve_influence" style="left:25px;top:1300px"></div>
+ <div class="token" id="token_mongol_cavalry" style="left:89px;top:220px"></div>
+ <div class="token cavalry charge" id="cavalry_1" style="left:700px;top:100px"></div>
+ <div class="token cavalry screen" id="cavalry_2" style="left:700px;top:110px"></div>
+ <div class="token cavalry charge" id="cavalry_3" style="left:700px;top:120px"></div>
+ <div class="token cavalry screen" id="cavalry_4" style="left:700px;top:130px"></div>
+ <div class="token cavalry charge" id="cavalry_5" style="left:700px;top:140px"></div>
+ <div class="token cavalry screen" id="cavalry_6" style="left:700px;top:150px"></div>
+ <div class="token cavalry charge" id="cavalry_7" style="left:700px;top:160px"></div>
+ <div class="token cavalry screen" id="cavalry_8" style="left:700px;top:170px"></div>
+
+ </div>
+
+ <div id="pieces">
+ <div id="ds_cylinder" class="piece ds cylinder" style="left:875px;top:1480px;z-index:3"></div>
+ <div id="bk_cylinder" class="piece bk cylinder" style="left:875px;top:1440px;z-index:2"></div>
+ <div id="ve_cylinder" class="piece ve cylinder" style="left:875px;top:1400px;z-index:1"></div>
+ <div id="ds_resources" class="piece ds cylinder" style="left:812px;top:20px;z-index:1"></div>
+ <div id="bk_resources" class="piece bk cylinder" style="left:419px;top:20px;z-index:1"></div>
+ <div id="ve_resources" class="piece ve cylinder" style="left:484px;top:20px;z-index:1"></div>
+ </div>
+ </div>
+</div>
+
+<div id="card_panel">
+ <div id="this_card" class="card card_1"><div id="unshaded_event"></div><div id="shaded_event"></div></div>
+ <div id="deck_outer" class="card card_back"><div id="deck_size">35</div></div>
+ <div id="dynasty_card" class="card card_dynasty_khalji"></div>
+ <div id="of_gods_and_kings" class="card card_26"><div id="of_gods_and_kings_label">~ Of Gods and Kings ~</div></div>
+</div>
+
+</div>
+
+</main>
+
+<footer id="status"></footer>
+
+</body>
diff --git a/play.js b/play.js
new file mode 100644
index 0000000..d35e3d3
--- /dev/null
+++ b/play.js
@@ -0,0 +1,1247 @@
+"use strict"
+
+/*
+ global view, data, player, send_action, action_button, scroll_with_middle_mouse
+*/
+
+/* COMMON */
+
+function set_has(set, item) {
+ let a = 0
+ let b = set.length - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = set[m]
+ if (item < x)
+ b = m - 1
+ else if (item > x)
+ a = m + 1
+ else
+ return true
+ }
+ return false
+}
+
+function map_get(map, key, missing) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m << 1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else
+ return map[(m << 1) + 1]
+ }
+ return missing
+}
+
+/* DATA */
+
+// Factions
+const DS = 0
+const BK = 1
+const VE = 2
+
+const NAME_DS = "DS"
+const NAME_BK = "BK"
+const NAME_VE = "VE"
+
+// Sequence of Play options
+const ELIGIBLE = 0
+const SOP_LIMITED_COMMAND = 1
+const SOP_COMMAND_DECREE = 2
+const SOP_EVENT_OR_COMMAND = 3
+const SOP_PASS = 4
+const INELIGIBLE = 5
+
+// Spaces
+const S_ANDHRA = 0
+const S_BENGAL = 1
+const S_GONDWANA = 2
+const S_GUJARAT = 3
+const S_JAUNPUR = 4
+const S_KARNATAKA = 5
+const S_MADHYADESH = 6
+const S_MAHARASHTRA = 7
+const S_MALWA = 8
+const S_ORISSA = 9
+const S_RAJPUT_KINGDOMS = 10
+const S_SINDH = 11
+const S_TAMILAKAM = 12
+const S_DELHI = 13
+const S_MOUNTAIN_PASSES = 14
+const S_PUNJAB = 15
+const S_MONGOL_INVADERS = 16
+const S_DS_AVAILABLE = 17
+const S_BK_AVAILABLE = 18
+const S_VE_AVAILABLE = 19
+const S_BK_INF_2 = 20
+const S_BK_INF_4 = 21
+const S_VE_INF_1 = 22
+const S_VE_INF_2 = 23
+const S_VE_INF_3 = 24
+const S_VE_INF_4 = 25
+
+const space_name = [
+ "Andhra",
+ "Bengal",
+ "Gondwana",
+ "Gujarat",
+ "Jaunpur",
+ "Karnataka",
+ "Madhyadesh",
+ "Maharashtra",
+ "Malwa",
+ "Orissa",
+ "Rajput Kingdoms",
+ "Sindh",
+ "Tamilakam",
+ "Delhi",
+ "Mountain Passes",
+ "Punjab",
+ "Mongol Invaders",
+ "DS Available",
+ "BK Available",
+ "VE Available",
+]
+
+/* LAYOUT DATA */
+// :r !node tools/parse-layout.js
+const layout = {
+ "circles": {
+ "provinces": {
+ "Sindh": {
+ "cx": 65,
+ "cy": 393,
+ "rx": 34,
+ "ry": 34
+ },
+ "Rajput Kingdoms": {
+ "cx": 329,
+ "cy": 403,
+ "rx": 34,
+ "ry": 34
+ },
+ "Malwa": {
+ "cx": 605,
+ "cy": 572,
+ "rx": 34,
+ "ry": 34
+ },
+ "Jaunpur": {
+ "cx": 894,
+ "cy": 455,
+ "rx": 34,
+ "ry": 34
+ },
+ "Bengal": {
+ "cx": 1192,
+ "cy": 536,
+ "rx": 34,
+ "ry": 34
+ },
+ "Orissa": {
+ "cx": 1034,
+ "cy": 858,
+ "rx": 34,
+ "ry": 34
+ },
+ "Gondwana": {
+ "cx": 913,
+ "cy": 737,
+ "rx": 34,
+ "ry": 34
+ },
+ "Madhyadesh": {
+ "cx": 670,
+ "cy": 817,
+ "rx": 34,
+ "ry": 34
+ },
+ "Andhra": {
+ "cx": 743,
+ "cy": 1090,
+ "rx": 34,
+ "ry": 34
+ },
+ "Maharashtra": {
+ "cx": 438,
+ "cy": 969,
+ "rx": 34,
+ "ry": 34
+ },
+ "Gujarat": {
+ "cx": 220,
+ "cy": 678,
+ "rx": 34,
+ "ry": 34
+ },
+ "Karnataka": {
+ "cx": 550,
+ "cy": 1278,
+ "rx": 34,
+ "ry": 34
+ },
+ "Tamilakam": {
+ "cx": 704,
+ "cy": 1399,
+ "rx": 34,
+ "ry": 34
+ }
+ },
+ "mongol_invasion_regions": {
+ "Mountain Passes": {
+ "cx": 302,
+ "cy": 140,
+ "rx": 83,
+ "ry": 28
+ },
+ "Punjab": {
+ "cx": 478,
+ "cy": 220,
+ "rx": 58,
+ "ry": 19
+ },
+ "Delhi": {
+ "cx": 647,
+ "cy": 375,
+ "rx": 148,
+ "ry": 148
+ }
+ }
+ },
+ "rects": {
+ "available_boxes": {
+ "Mongol Invaders": {
+ "x": 24,
+ "y": 100,
+ "w": 177,
+ "h": 110
+ },
+ "DS Available": {
+ "x": 796,
+ "y": 91,
+ "w": 388,
+ "h": 223
+ },
+ "BK Available": {
+ "x": 21,
+ "y": 908,
+ "w": 238,
+ "h": 224
+ },
+ "VE Available": {
+ "x": 21,
+ "y": 1405,
+ "w": 239,
+ "h": 225
+ }
+ },
+ "tracks": {
+ "Track 24": {
+ "x": 1198,
+ "y": 403,
+ "w": 61,
+ "h": 61
+ },
+ "Track 18": {
+ "x": 1197,
+ "y": 14,
+ "w": 62,
+ "h": 63
+ },
+ "Track 0": {
+ "x": 15,
+ "y": 14,
+ "w": 62,
+ "h": 63
+ },
+ "BK Influence 0": {
+ "x": 18,
+ "y": 1186,
+ "w": 61,
+ "h": 63
+ },
+ "BK Influence 1": {
+ "x": 89,
+ "y": 1186,
+ "w": 61,
+ "h": 63
+ },
+ "BK Influence 2": {
+ "x": 160,
+ "y": 1186,
+ "w": 61,
+ "h": 63
+ },
+ "BK Influence 3": {
+ "x": 231,
+ "y": 1186,
+ "w": 61,
+ "h": 63
+ },
+ "BK Influence 4": {
+ "x": 302,
+ "y": 1186,
+ "w": 61,
+ "h": 63
+ },
+ "VE Influence 0": {
+ "x": 18,
+ "y": 1292,
+ "w": 61,
+ "h": 63
+ },
+ "VE Influence 1": {
+ "x": 89,
+ "y": 1292,
+ "w": 61,
+ "h": 63
+ },
+ "VE Influence 2": {
+ "x": 160,
+ "y": 1292,
+ "w": 61,
+ "h": 63
+ },
+ "VE Influence 3": {
+ "x": 231,
+ "y": 1291,
+ "w": 61,
+ "h": 63
+ },
+ "VE Influence 4": {
+ "x": 302,
+ "y": 1292,
+ "w": 61,
+ "h": 63
+ }
+ },
+ "sequence_of_play": {
+ "Limited Command": {
+ "x": 854,
+ "y": 1305,
+ "w": 90,
+ "h": 54
+ },
+ "Eligible Factions": {
+ "x": 854,
+ "y": 1367,
+ "w": 91,
+ "h": 201
+ },
+ "Pass": {
+ "x": 854,
+ "y": 1578,
+ "w": 90,
+ "h": 56
+ },
+ "Ineligible Factions": {
+ "x": 1166,
+ "y": 1367,
+ "w": 90,
+ "h": 201
+ },
+ "Command and Decree": {
+ "x": 1016,
+ "y": 1371,
+ "w": 77,
+ "h": 77
+ },
+ "Event or Command": {
+ "x": 1016,
+ "y": 1488,
+ "w": 77,
+ "h": 77
+ }
+ }
+ }
+}
+
+/* STATE */
+
+function piece_space(p) {
+ return view.pieces[p]
+}
+
+/* BUILD */
+
+let ui = {
+ map: document.getElementById("map"),
+ favicon: document.getElementById("favicon"),
+ header: document.querySelector("header"),
+ status: document.getElementById("status"),
+ spaces: [],
+ control: [],
+ card_tip: document.getElementById("card_tip"),
+ this_card: document.getElementById("this_card"),
+ shaded_event: document.getElementById("shaded_event"),
+ unshaded_event: document.getElementById("unshaded_event"),
+ deck_outer: document.getElementById("deck_outer"),
+ deck_size: document.getElementById("deck_size"),
+ of_gods_and_kings: document.getElementById("of_gods_and_kings"),
+ dynasty_card: document.getElementById("dynasty_card"),
+ tokens: {
+ token_ds_vp: document.getElementById("token_ds_vp"),
+ token_bk_vp: document.getElementById("token_bk_vp"),
+ token_ve_vp: document.getElementById("token_ve_vp"),
+ token_bk_influence: document.getElementById("token_bk_influence"),
+ token_ve_influence: document.getElementById("token_ve_influence"),
+ token_mongol_cavalry: document.getElementById("token_mongol_cavalry"),
+ cavalry_1: document.getElementById("cavalry_1"),
+ cavalry_2: document.getElementById("cavalry_2"),
+ cavalry_3: document.getElementById("cavalry_3"),
+ cavalry_4: document.getElementById("cavalry_4"),
+ cavalry_5: document.getElementById("cavalry_5"),
+ cavalry_6: document.getElementById("cavalry_6"),
+ cavalry_7: document.getElementById("cavalry_7"),
+ cavalry_8: document.getElementById("cavalry_8"),
+ },
+ pieces: [],
+ resources: [
+ document.getElementById("ds_resources"),
+ document.getElementById("bk_resources"),
+ document.getElementById("ve_resources"),
+ ],
+ cylinder: [
+ document.getElementById("ds_cylinder"),
+ document.getElementById("bk_cylinder"),
+ document.getElementById("ve_cylinder"),
+ ],
+}
+
+function create(t, p, ...c) {
+ let e = document.createElement(t)
+ Object.assign(e, p)
+ e.append(c)
+ return e
+}
+
+function register_action(e, action, id) {
+ e.my_action = action
+ e.my_id = id
+ e.onmousedown = on_click_action
+}
+
+function is_action(action, arg) {
+ if (arg === undefined)
+ return !!(view.actions && view.actions[action] === 1)
+ return !!(view.actions && view.actions[action] && set_has(view.actions[action], arg))
+}
+
+function toggle_pieces() {
+ if (ui.map.classList.contains("hide_tokens")) {
+ ui.map.classList.remove("hide_tokens")
+ ui.map.classList.remove("hide_pieces")
+ } else if (ui.map.classList.contains("hide_pieces")) {
+ ui.map.classList.add("hide_tokens")
+ } else {
+ ui.map.classList.add("hide_pieces")
+ }
+}
+
+function on_click_action(evt) {
+ if (evt.button === 0)
+ send_action(evt.target.my_action, evt.target.my_id)
+}
+
+function init_ui() {
+ register_action(ui.this_card, "event", undefined)
+ register_action(ui.unshaded_event, "unshaded", undefined)
+ register_action(ui.shaded_event, "shaded", undefined)
+ register_action(ui.resources[DS], "resources", DS)
+ register_action(ui.resources[BK], "resources", BK)
+ register_action(ui.resources[VE], "resources", VE)
+
+ ui.this_card.onmouseenter = on_focus_this_event
+ ui.this_card.onmouseleave = on_blur_event
+ ui.shaded_event.onmouseenter = on_focus_shaded_event
+ ui.shaded_event.onmouseleave = on_focus_this_event
+ ui.unshaded_event.onmouseenter = on_focus_unshaded_event
+ ui.unshaded_event.onmouseleave = on_focus_this_event
+
+ for (let s = 0; s < 20; ++s) {
+ let e = null
+
+ // provinces
+ if (s < S_DELHI) {
+ let { cx, cy } = layout.circles.provinces[space_name[s]]
+ ui.control[s] = e = create("div", { className: "token tributary" })
+ e.style.left = (cx - 24) + "px"
+ e.style.top = (cy - 24) + "px"
+ document.getElementById("tokens").appendChild(e)
+
+ ui.spaces[s] = e = create("div", { className: "space province" })
+ e.style.left = (cx - 45) + "px"
+ e.style.top = (cy - 45) + "px"
+ e.style.width = (90 - 4) + "px"
+ e.style.height = (90 - 4) + "px"
+ register_action(e, "space", s)
+ document.getElementById("spaces").appendChild(e)
+ }
+
+ // delhi & passes
+ if (s >= S_DELHI && s <= S_PUNJAB) {
+ let { cx, cy, rx, ry } = layout.circles.mongol_invasion_regions[space_name[s]]
+ ui.spaces[s] = e = create("div", { className: "space province" })
+ e.style.left = (cx - rx) + "px"
+ e.style.top = (cy - ry) + "px"
+ e.style.width = (rx * 2 - 4) + "px"
+ e.style.height = (ry * 2 - 4) + "px"
+ register_action(e, "space", s)
+ document.getElementById("spaces").appendChild(e)
+ }
+
+ // boxes
+ if (s >= S_MONGOL_INVADERS && s <= S_VE_AVAILABLE) {
+ let { x, y, w, h } = layout.rects.available_boxes[space_name[s]]
+ ui.spaces[s] = e = create("div", { className: "space" })
+ e.style.left = x + "px"
+ e.style.top = y + "px"
+ e.style.width = (w - 4) + "px"
+ e.style.height = (h - 4) + "px"
+ register_action(e, "space", s)
+ document.getElementById("spaces").appendChild(e)
+ }
+ }
+
+ function create_piece(c, action, id, x, y) {
+ let e = create("div", {
+ className: c,
+ my_action: action,
+ my_id: id,
+ my_x_offset: x,
+ my_y_offset: y,
+ onmousedown: on_click_action
+ })
+ document.getElementById("pieces").appendChild(e)
+ return e
+ }
+
+ function create_piece_list(faction, type, c, x, y) {
+ for (let p = first_piece[faction][type]; p <= last_piece[faction][type]; ++p)
+ ui.pieces[p] = create_piece(c, "piece", p, x, y)
+ }
+
+ ui.pieces = []
+
+return
+
+ /*
+ <div class="piece ds governor" style="left:200px;top:380px"></div>
+ <div class="piece bk amir rebel" style="left:230px;top:380px"></div>
+ <div class="piece ve raja rebel" style="left:260px;top:380px"></div>
+ <div class="piece ds cube" style="left:210px;top:430px"></div>
+ <div class="piece mongol cube" style="left:245px;top:430px"></div>
+ <div class="piece ds disk" style="left:200px;top:480px"></div>
+ <div class="piece bk disk" style="left:250px;top:480px"></div>
+ <div class="piece ve disk" style="left:300px;top:480px"></div>
+ */
+
+
+ create_piece_list(DS, DISK, "piece ds disk", -4, 10)
+ create_piece_list(DS, GOVERNOR, "piece ds governor", 0, 4)
+ create_piece_list(DS, CUBE, "piece ds cube", 0, 4)
+
+ create_piece_list(BK, DISK, "piece ve disk", -4, 10)
+ create_piece_list(BK, AMIR, "piece ve amir", 2, 0)
+
+ create_piece_list(VE, DISK, "piece ve disk", -4, 10)
+ create_piece_list(VE, RAJA, "piece ve raja", 2, 0)
+
+ create_piece_list(MONGOLS, CUBE, "piece mongol cube", 2, 0)
+}
+
+/* UPDATE */
+
+function action_menu_item(action) {
+ let menu = document.getElementById(action + "_menu")
+ if (view.actions && action in view.actions) {
+ menu.classList.toggle("hide", false)
+ menu.classList.toggle("disabled", view.actions[action] === 0)
+ return 1
+ } else {
+ menu.classList.toggle("hide", true)
+ return 0
+ }
+}
+
+function action_menu(menu, action_list) {
+ let x = 0
+ for (let action of action_list)
+ x |= action_menu_item(action)
+ menu.classList.toggle("hide", !x)
+}
+
+const LAYOUT_CACHE = {
+ Center: [],
+ Govt: [],
+ FARC: [],
+ AUC: [],
+ Cartels: [],
+ COIN: [],
+ INSURGENTS: [],
+}
+
+function get_layout_xy(s, f = "Center") {
+ if (!LAYOUT_CACHE[f][s]) {
+ let id = (f !== "Center") ? data.spaces[s].id + " " + f : data.spaces[s].id
+ LAYOUT_CACHE[f][s] = LAYOUT[id]
+ }
+ return LAYOUT_CACHE[f][s]
+}
+
+function filter_piece_list(list, space, faction, type) {
+ for (let i = first_piece[faction][type]; i <= last_piece[faction][type]; ++i)
+ if (view.pieces[i] === space)
+ list.push(ui.pieces[i])
+}
+
+function layout_available(faction, type, xorig, yorig) {
+ let list = []
+ filter_piece_list(list, AVAILABLE, faction, type)
+ layout_pieces(list, xorig, yorig + 35, null, AVAILABLE)
+}
+
+function layout_pieces(list, xorig, yorig, bases, shipments, s, faction) {
+ const dx = 17
+ const dy = 11
+ let off_x = 0
+ let off_y = 0
+ let rotate = 0
+
+ if (s >= 0)
+ rotate = (data.spaces[s].type === "mountain") ? 1 : 0
+
+ function layout_piece_rowcol(nrow, ncol, row, col, e, z) {
+ // basic piece size = 29x36
+ let x = xorig - (row * dx - col * dx) - 15 + off_x
+ let y = yorig - (row * dy + col * dy) - 24 + off_y
+ let xo = e.my_x_offset
+ let yo = e.my_y_offset
+ e.style.left = (xo + x) + "px"
+ e.style.top = (yo + y) + "px"
+ e.style.zIndex = y
+ e.my_x = x + 15
+ e.my_y = y + 24
+ e.my_z = z
+ }
+
+ if (list.length > 0) {
+ let nrow = Math.round(Math.sqrt(list.length))
+ let ncol = Math.ceil(list.length / nrow)
+ let z = 50
+ let i = 0
+ if ((s >= 0 && s <= last_city) || s >= first_loc) {
+ off_x = (nrow - ncol) * 6
+ off_y = (nrow - 1) * 8
+ }
+ for (let row = 0; row < nrow; ++row)
+ for (let col = 0; col < ncol; ++col)
+ if (i < list.length)
+ layout_piece_rowcol(nrow, ncol, row, col, list[list.length-(++i)], z--)
+ }
+
+ if (bases)
+ layout_dept_bases(bases, xorig + off_x, yorig + 12 + off_y, s)
+}
+
+function place_piece(p, x, y, z) {
+ p.style.left = x + "px"
+ p.style.top = y + "px"
+ if (z)
+ p.style.zIndex = z
+ p.my_x = x
+ p.my_y = y
+ p.my_z = z
+}
+
+function layout_dept_bases(list, xc, yc, s) {
+ if (list.length === 1) {
+ place_piece(list[0], xc - 20 + 32, yc - 10, 52)
+ }
+ if (list.length === 2) {
+ place_piece(list[0], xc - 20 + 18, yc - 0, 52)
+ place_piece(list[1], xc - 20 + 18 + 32, yc - 21, 51)
+ }
+ if (list.length === 3) {
+ // TODO
+ place_piece(list[0], xc - 20 + 18, yc - 0, 52)
+ place_piece(list[1], xc - 20 + 18 + 32, yc - 21, 51)
+ place_piece(list[2], xc - 20 + 18 + 32, yc - 21, 100)
+ }
+}
+
+function layout_available_bases(list, x0, y0, cols, rows, dx, dy) {
+ let x = x0
+ let y = y0
+ for (let i = 0; i < list.length; ++i) {
+ place_piece(list[list.length-i-1], x - 44 - 6, y + 8)
+ y += dy
+ if (i % rows === rows - 1) {
+ x -= dx
+ y = y0
+ }
+ }
+}
+
+function layout_sop() {
+ let i, x, y, z
+
+ // Eligible
+ x = 1164 - 22
+ y = 480
+ z = 1
+ let order = data.card_order[view.deck[0]]
+ for (let faction of order) {
+ if (view.cylinder[faction] === ELIGIBLE) {
+ place_piece(ui.cylinder[faction], x, y, z)
+ y += 40
+ z += 1
+ }
+ }
+
+ // Ineligible
+ x = 1510 - 22
+ y = 480
+ z = 1
+ for (let faction = 0; faction < 4; ++faction) {
+ if (view.cylinder[faction] === INELIGIBLE) {
+ place_piece(ui.cylinder[faction], x, y, z)
+ y += 40
+ z += 1
+ }
+ }
+
+ // Pass
+ x = 1164 - 22 - 24
+ y = 688 - 28
+ z = 1
+ i = 0
+ for (let faction = 0; faction < 4; ++faction) {
+ if (view.cylinder[faction] === SOP_PASS) {
+ place_piece(ui.cylinder[faction], x, y, z)
+ x += 48
+ z += 1
+ if (++i === 2) { x -= 72; y += 28 }
+ }
+ }
+
+ for (let [i, x, y] of sop_xy) {
+ for (let faction = 0; faction < 4; ++faction)
+ if (view.cylinder[faction] === i)
+ place_piece(ui.cylinder[faction], x, y)
+ }
+}
+
+function layout_score_cell(list, x, y, dx, dy) {
+ let z = 1
+ if (list.length > 1) {
+ if (dy > 0) y -= 12
+ if (dy < 0) y += 12
+ if (dx > 0) x -= 12
+ if (dx < 0) x += 12
+ }
+ for (let p of list) {
+ if (p.classList.contains("token"))
+ place_piece(p, x - 24, y - 24, z)
+ else
+ place_piece(p, x - 22, y - 24, z)
+ x += dx
+ y += dy
+ z += 1
+ }
+}
+
+function layout_score() {
+ let list = []
+ let x, y
+ for (let i = 0; i <= 24; ++i) {
+ list.length = 0
+
+ if (view.vp[DS] === i) list.push(ui.tokens.token_ds_vp)
+ if (view.vp[BK] === i) list.push(ui.tokens.token_bk_vp)
+ if (view.vp[VE] === i) list.push(ui.tokens.token_ve_vp)
+
+ for (let faction = 0; faction < 3; ++faction)
+ if (view.resources[faction] === i)
+ list.push(ui.resources[faction])
+
+ // X: 15 -> 1198 (0-18)
+ // Y: 14 -> 403 (18-24)
+
+ if (i < 18) y = 20
+ else y = 20 + (i - 18) * 65
+
+ if (i < 18) x = 23 + (i * 65.6)
+ else x = 1204
+
+ x = Math.round(x) + 24
+ y = Math.round(y) + 24
+
+ if (i < 17) layout_score_cell(list, x, y, 0, 28)
+ else if (i === 17) layout_score_cell(list, x, y, -18, 25)
+ else layout_score_cell(list, x, y, -41, 0)
+ }
+}
+
+function update_rebels(faction, type, rebel) {
+ let p0 = first_piece[faction][type]
+ let p1 = last_piece[faction][type]
+ for (let i = 0, p = p0; p <= p1; ++i, ++p) {
+ if (underground & (1 << i))
+ ui.pieces[p].classList.add("rebel")
+ else
+ ui.pieces[p].classList.remove("rebel")
+ }
+}
+
+function make_card_class_name(c) {
+ return "card card_" + c
+ // TODO:
+ if (set_has([1,2,3,7,9,10,11,13], view.deck[0]))
+ return "card card_" + c + " u" + data.card_unshaded_lines[c] + " s" + data.card_shaded_lines[c] + " c"
+ else
+ return "card card_" + c + " u" + data.card_unshaded_lines[c] + " s" + data.card_shaded_lines[c]
+}
+
+function update_player_active(name, x) {
+ if (roles[name])
+ roles[name].element.classList.toggle("active", x)
+}
+
+let once = true
+function on_update() {
+ if (once) {
+ init_ui()
+ once = false
+ }
+
+ switch (player) {
+ case "DS": ui.favicon.href = "images/Flag_DS.png"; break
+ case "BK": ui.favicon.href = "images/Flag_BK.png"; break
+ case "VE": ui.favicon.href = "images/Flag_VE.png"; break
+ }
+
+ ui.header.classList.toggle("ds", view.current === DS)
+ ui.header.classList.toggle("bk", view.current === BK)
+ ui.header.classList.toggle("ve", view.current === VE)
+
+ ui.tokens.token_bk_influence.classList.toggle("action", is_action("bk_inf"))
+ ui.tokens.token_ve_influence.classList.toggle("action", is_action("ve_inf"))
+
+ ui.resources[DS].classList.toggle("action", is_action("resources", DS))
+ ui.resources[BK].classList.toggle("action", is_action("resources", BK))
+ ui.resources[VE].classList.toggle("action", is_action("resources", VE))
+
+ update_player_active(NAME_DS, view.current === DS)
+ update_player_active(NAME_VE, view.current === VE)
+ update_player_active(NAME_BK, view.current === BK)
+
+ ui.this_card.className = make_card_class_name(view.deck[0])
+ if (view.deck[1] > 0) {
+ ui.deck_outer.className = "card card_back"
+ ui.deck_size.textContent = `${view.deck[1]}`
+ } else {
+ ui.deck_outer.className = "hide"
+ }
+
+ ui.this_card.classList.toggle("action", !!(view.actions && view.actions.event === 1))
+ ui.shaded_event.classList.toggle("action", !!(view.actions && view.actions.shaded === 1))
+ ui.unshaded_event.classList.toggle("action", !!(view.actions && view.actions.unshaded === 1))
+
+ layout_score()
+
+return
+
+ layout_sop()
+
+ layout_available(GOVT, TROOPS, 114, 248)
+ layout_available(GOVT, POLICE, 114, 448)
+ layout_available(FARC, GUERRILLA, 1396, 234)
+ layout_available(AUC, GUERRILLA, 196, 2370)
+ layout_available(CARTELS, GUERRILLA, 1465, 1970)
+
+ for (let i = view.farc_zones.length; i < ui.farc_zones.length; ++i)
+ ui.farc_zones[i].className = "hide"
+
+ let tix = 0
+
+ let list = []
+ let bases = []
+ let drugs = []
+ for (let s = 0; s < data.spaces.length; ++s) {
+ let id = data.spaces[s].id
+ let xy
+
+ if (s <= last_pop) {
+ switch (view.support[s]) {
+ case -2: ui.support[s].className = "token active_opposition"; break
+ case -1: ui.support[s].className = "token passive_opposition"; break
+ case 0: ui.support[s].className = "hide"; break
+ case 1: ui.support[s].className = "token passive_support"; break
+ case 2: ui.support[s].className = "token active_support"; break
+ }
+ }
+
+ if (s >= first_loc && s <= last_loc) {
+ if (set_has(view.sabotage, s))
+ ui.sabotage[s].className = "token sabotage"
+ else
+ ui.sabotage[s].className = "hide"
+ }
+
+ if (s >= first_dept && s <= last_dept) {
+ ui.farc_zone[s].classList.toggle("hide", !(view.farc_zones & (1<<s)))
+ }
+
+ if (s <= last_dept) {
+ if (view.govt_control & (1<<s))
+ ui.control[s].className = "token govt_control"
+ else if (view.farc_control & (1<<s))
+ ui.control[s].className = "token farc_control"
+ else
+ ui.control[s].className = "hide"
+ }
+
+ tix = layout_terror(tix, s, map_get(view.terror, s, 0) * 1)
+
+ update_rebels(BK, AMIR, view.rebel[BK])
+ update_rebels(VE, RAJA, view.rebel[VE])
+
+ drugs.length = 0
+ for (let i = 0; i < 4; ++i) {
+ let shx = view.shipments[i]
+ if (shx !== 0) {
+ if ((shx & 3) === 0 && view.pieces[(shx >> 2)] === s)
+ layout_shipments_push(drugs, ui.pieces[shx>>2], ui.shipments[i], piece_faction(shx>>2))
+ else if ((shx & 3) !== 0 && (shx >> 2) === s)
+ layout_shipments_push(drugs, null, ui.shipments[i], shx & 3)
+ }
+ }
+
+ if (s <= last_city) {
+ list.length = bases.length = 0
+ filter_piece_list(list, s, FARC, GUERRILLA)
+ filter_piece_list(list, s, AUC, GUERRILLA)
+ filter_piece_list(list, s, CARTELS, GUERRILLA)
+ filter_piece_list(list, s, GOVT, TROOPS)
+ filter_piece_list(list, s, GOVT, POLICE)
+ filter_piece_list(bases, s, GOVT, BASE)
+ filter_piece_list(bases, s, FARC, BASE)
+ filter_piece_list(bases, s, AUC, BASE)
+ filter_piece_list(bases, s, CARTELS, BASE)
+ xy = get_layout_xy(s)
+ layout_pieces(list, xy[0], xy[1], null, null, s, 0)
+ layout_city_bases(bases, xy[0], xy[1] + get_layout_radius(s) - 12, s)
+
+ layout_city_shipments(s, drugs, xy[0], xy[1])
+ } else if (s <= last_dept) {
+ list.length = bases.length = 0
+ filter_piece_list(list, s, FARC, GUERRILLA)
+ filter_piece_list(bases, s, FARC, BASE)
+ xy = get_layout_xy(s, "FARC")
+ layout_pieces(list, xy[0], xy[1], bases, drugs, s, FARC)
+
+ list.length = bases.length = 0
+ filter_piece_list(list, s, AUC, GUERRILLA)
+ filter_piece_list(bases, s, AUC, BASE)
+ xy = get_layout_xy(s, "AUC")
+ layout_pieces(list, xy[0], xy[1], bases, drugs, s, AUC)
+
+ list.length = bases.length = 0
+ filter_piece_list(list, s, CARTELS, GUERRILLA)
+ filter_piece_list(bases, s, CARTELS, BASE)
+ xy = get_layout_xy(s, "Cartels")
+ layout_pieces(list, xy[0], xy[1], bases, drugs, s, CARTELS)
+
+ list.length = bases.length = 0
+ filter_piece_list(list, s, GOVT, TROOPS)
+ filter_piece_list(list, s, GOVT, POLICE)
+ filter_piece_list(bases, s, GOVT, BASE)
+ xy = get_layout_xy(s, "Govt")
+ layout_pieces(list, xy[0], xy[1], bases, null, s, GOVT)
+ } else {
+ list.length = 0
+ filter_piece_list(list, s, FARC, GUERRILLA)
+ filter_piece_list(list, s, AUC, GUERRILLA)
+ filter_piece_list(list, s, CARTELS, GUERRILLA)
+ xy = get_layout_xy(s, "INSURGENTS")
+ layout_pieces(list, xy[0], xy[1], null, s)
+
+ list.length = 0
+ filter_piece_list(list, s, GOVT, TROOPS)
+ filter_piece_list(list, s, GOVT, POLICE)
+ xy = get_layout_xy(s, "COIN")
+ layout_pieces(list, xy[0], xy[1], null, s)
+
+ xy = get_layout_xy(s)
+ layout_loc_shipments(s, drugs, xy[0], xy[1])
+ }
+
+ ui.spaces[s].classList.toggle("action", is_action("space", s))
+ ui.spaces[s].classList.toggle("selected", view.where === s)
+ }
+
+ for (; tix < 40; ++tix)
+ ui.terror[tix].className = "hide"
+
+ for (let i = first_piece[AUC][GUERRILLA]; i <= last_piece[AUC][GUERRILLA]; ++i)
+ ui.pieces[i].classList.toggle("hide", view.pieces[i] === OUT_OF_PLAY)
+
+ list.length = 0
+ for (let i = 0; i < 4; ++i) {
+ let shx = view.shipments[i]
+ let shf = shx & 3
+ if (shx === 0)
+ list.push(ui.shipments[i])
+ if (shf === 0)
+ ui.shipments[i].className = "token shipment"
+ else if (shf === FARC)
+ ui.shipments[i].className = "token shipment farc"
+ else if (shf === AUC)
+ ui.shipments[i].className = "token shipment auc"
+ else if (shf === CARTELS)
+ ui.shipments[i].className = "token shipment cartels"
+ if (view.actions && view.actions.shipment && set_has(view.actions.shipment, i))
+ ui.shipments[i].classList.add("action")
+ if (view.selected_shipment === i)
+ ui.shipments[i].classList.add("selected")
+ }
+ layout_available_bases(list, 1532, 1722, 2, 2, 89, 69)
+
+ list.length = 0
+ filter_piece_list(list, AVAILABLE, GOVT, BASE)
+ layout_available_bases(list, 287 + 177, 371, 3, 1, 61, 0)
+
+ list.length = 0
+ filter_piece_list(list, AVAILABLE, FARC, BASE)
+ layout_available_bases(list, 446 + 543, 2295, 9, 1, 61, 0)
+
+ list.length = 0
+ filter_piece_list(list, AVAILABLE, AUC, BASE)
+ layout_available_bases(list, 446 + 360, 2386, 6, 1, 61, 0)
+
+ list.length = 0
+ filter_piece_list(list, AVAILABLE, CARTELS, BASE)
+ layout_available_bases(list, 1373 + 183, 2117, 3, 5, 63, 63)
+
+ if (view.actions && view.actions.piece)
+ for (let i = 0; i < ui.pieces.length; ++i)
+ ui.pieces[i].classList.toggle("action", set_has(view.actions.piece, i))
+ else
+ for (let i = 0; i < ui.pieces.length; ++i)
+ ui.pieces[i].classList.remove("action")
+ for (let i = 0; i < ui.pieces.length; ++i)
+ ui.pieces[i].classList.toggle("selected", view.who === i)
+
+ action_menu(document.getElementById("negotiate_menu"), [
+ "remove_pieces",
+ "transfer_resources",
+ "transfer_shipment",
+ "ask_resources",
+ "ask_shipment",
+ ])
+
+ // Select Faction
+ action_button("govt", "Government")
+ action_button("farc", "FARC")
+ action_button("auc", "AUC")
+ action_button("cartels", "Cartels")
+
+ confirm_action_button("choose_govt", "Government", "Choose GOVERNMENT to execute this event?")
+ confirm_action_button("choose_farc", "FARC", "Choose FARC to execute this event?")
+ confirm_action_button("choose_auc", "AUC", "Choose AUC to execute this event?")
+ confirm_action_button("choose_cartels", "Cartels", "Choose CARTELS to execute this event?")
+
+ // Select Operation
+ action_button("train", "Train")
+ action_button("patrol", "Patrol")
+ action_button("sweep", "Sweep")
+ action_button("assault", "Assault")
+ action_button("rally", "Rally")
+ action_button("march", "March")
+ action_button("attack", "Attack")
+ action_button("terror", "Terror")
+
+ // Select Special Activity
+ action_button("air_lift", "Air Lift")
+ action_button("air_strike", "Air Strike")
+ action_button("eradicate", "Eradicate")
+ action_button("extort", "Extort")
+ action_button("ambush", "Ambush")
+ action_button("assassinate", "Assassinate")
+ action_button("kidnap", "Kidnap")
+ action_button("cultivate", "Cultivate")
+ action_button("process", "Process")
+ action_button("bribe", "Bribe")
+
+ // Train/Rally sub-actions
+ action_button("move", "Move")
+ action_button("flip", "Flip")
+ action_button("base", "Base")
+ action_button("civic", "Civic Action")
+
+ action_button("support", "Support")
+ action_button("opposition", "Opposition")
+
+ action_button("remove", "Remove")
+ action_button("roll", "Roll")
+ action_button("skip", "Skip")
+ action_button("next", "Next")
+ action_button("pass", "Pass")
+
+ action_button("end_train", "End Train")
+ action_button("end_patrol", "End Patrol")
+ action_button("end_sweep", "End Sweep")
+ action_button("end_assault", "End Assault")
+ action_button("end_rally", "End Rally")
+ action_button("end_march", "End March")
+ action_button("end_attack", "End Attack")
+ action_button("end_terror", "End Terror")
+
+ action_button("end_air_lift", "End Air Lift")
+ action_button("end_extort", "End Extort")
+ action_button("end_assassinate", "End Assassinate")
+ action_button("end_kidnap", "End Kidnap")
+ action_button("end_process", "End Process")
+ action_button("end_bribe", "End Bribe")
+
+ action_button("end_event", "End Event")
+
+ action_button("deny", "Deny")
+ action_button("done", "Done")
+ action_button("undo", "Undo")
+}
+
+/* TOOLTIPS */
+
+function register_card_tip(e, c) {
+ e.onmouseenter = () => on_focus_card_tip(c)
+ e.onmouseleave = on_blur_card_tip
+}
+
+function on_focus_this_event() {
+ let c = view.deck[0]
+ if (c > 0)
+ ui.status.textContent = data.card_title[c]
+}
+
+function on_focus_unshaded_event() {
+ let c = view.deck[0]
+ if (c > 0) {
+ let f = data.card_flavor[c]
+ if (f)
+ ui.status.textContent = data.card_title[c] + " - " + f
+ else
+ ui.status.textContent = data.card_title[c]
+ }
+}
+
+function on_focus_shaded_event() {
+ let c = view.deck[0]
+ if (c > 0) {
+ ui.status.textContent = data.card_title[c] + " - " + data.card_flavor_shaded[c]
+ }
+}
+
+function on_blur_event() {
+ ui.status.textContent = ""
+}
+
+function on_focus_card_tip(c) {
+ ui.card_tip.className = "card card_" + c
+}
+
+function on_blur_card_tip() {
+ ui.card_tip.className = "hide"
+}
+
+function on_focus_space_tip(s) {
+ ui.spaces[s].classList.add("tip")
+}
+
+function on_blur_space_tip(s) {
+ ui.spaces[s].classList.remove("tip")
+}
+
+function on_click_space_tip(s) {
+ scroll_into_view(ui.spaces[s])
+}
+
+/* LOG */
+
+function sub_card(match, p1) {
+ let x = p1 | 0
+ let n = data.card_title[x]
+ return `<span class="tip" onmouseenter="on_focus_card_tip(${x})" onmouseleave="on_blur_card_tip()">${n}</span>`
+}
+
+function sub_space(match, p1) {
+ let x = p1 | 0
+ let n = data.space_name[x]
+ return `<span class="tip" onmouseenter="on_focus_space_tip(${x})" onmouseleave="on_blur_space_tip(${x})" onmousedown="on_click_space_tip(${x})">${n}</span>`
+}
+
+function on_log(text) {
+ let p = document.createElement("div")
+
+ if (text.match(/^>/)) {
+ text = text.substring(1)
+ p.className = "indent"
+ }
+
+ text = text.replace(/&/g, "&amp;")
+ text = text.replace(/</g, "&lt;")
+ text = text.replace(/>/g, "&gt;")
+
+ if (text.match(/^\.h1/)) {
+ text = text.substring(4)
+ p.className = "h1"
+ }
+ else if (text.match(/^\.h2 Gov/)) {
+ text = text.substring(3)
+ p.className = "h2 govt"
+ }
+ else if (text.match(/^\.h2 AUC/)) {
+ text = text.substring(3)
+ p.className = "h2 auc"
+ }
+ else if (text.match(/^\.h2 Cartels/)) {
+ text = text.substring(3)
+ p.className = "h2 cartels"
+ }
+ else if (text.match(/^\.h2 FARC/)) {
+ text = text.substring(3)
+ p.className = "h2 farc"
+ }
+ else if (text.match(/^\.h2 /)) {
+ text = text.substring(3)
+ p.className = "h2"
+ }
+ else if (text.match(/^\.h3/)) {
+ text = text.substring(4)
+ p.className = "h3"
+ }
+ else if (text.match(/^\.h4/)) {
+ text = text.substring(4)
+ p.className = "h4"
+ }
+ else if (text.match(/^\.n/)) {
+ text = text.substring(3)
+ p.className = "italic"
+ }
+ else if (text.match(/^\.i/)) {
+ text = text.substring(3)
+ p.className = "indent italic"
+ }
+
+ text = text.replace(/C(\d+)/g, sub_card)
+ text = text.replace(/S(\d+)/g, sub_space)
+
+ p.innerHTML = text
+ return p
+}
diff --git a/rules.js b/rules.js
index 9c795a7..4cb186c 100644
--- a/rules.js
+++ b/rules.js
@@ -6,13 +6,71 @@ let view = null
/* DATA */
-const data = require("./data.js")
+// const data = require("./data.js")
+
+const space_name = [
+ "Andhra",
+ "Bengal",
+ "Gondwana",
+ "Gujarat",
+ "Jaunpur",
+ "Karnataka",
+ "Madhyadesh",
+ "Maharashtra",
+ "Malwa",
+ "Orissa",
+ "Rajput Kingdoms",
+ "Sindh",
+ "Tamilakam",
+ "Delhi",
+ "Mountain Passes",
+ "Punjab",
+ "Mongol Invaders",
+ "DS Available",
+ "BK Available",
+ "VE Available",
+]
+
+const S_ANDHRA = 0
+const S_BENGAL = 1
+const S_GONDWANA = 2
+const S_GUJARAT = 3
+const S_JAUNPUR = 4
+const S_KARNATAKA = 5
+const S_MADHYADESH = 6
+const S_MAHARASHTRA = 7
+const S_MALWA = 8
+const S_ORISSA = 9
+const S_RAJPUT_KINGDOMS = 10
+const S_SINDH = 11
+const S_TAMILAKAM = 12
+const S_DELHI = 13
+const S_MOUNTAIN_PASSES = 14
+const S_PUNJAB = 15
+const S_MONGOL_INVADERS = 16
+const S_DS_AVAILABLE = 17
+const S_BK_AVAILABLE = 18
+const S_VE_AVAILABLE = 19
+const S_BK_INF_2 = 20
+const S_BK_INF_4 = 21
+const S_VE_INF_1 = 22
+const S_VE_INF_2 = 23
+const S_VE_INF_3 = 24
+const S_VE_INF_4 = 25
// Factions
const DS = 0
const BK = 1
const VE = 2
+// Sequence of Play options
+const ELIGIBLE = 0
+const SOP_LIMITED_COMMAND = 1
+const SOP_COMMAND_DECREE = 2
+const SOP_EVENT_OR_COMMAND = 3
+const SOP_PASS = 4
+const INELIGIBLE = 5
+
const faction_name = [ "Delhi Sultanate", "Bahmani Kingdom", "Vijayanagara Empire" ]
// Role names
@@ -66,7 +124,11 @@ exports.view = function (state, role) {
actions: null,
log: game.log,
current: game.current,
- deck: [ this_card, deck_size ],
+ vp: [ 18, 0, 0 ],
+ resources: [ 12, 6, 7 ],
+ bk_inf: 0,
+ ve_inf: 0,
+ deck: [ this_card, deck_size, game.of_gods_and_kings ],
}
if (game.result) {
@@ -149,7 +211,14 @@ exports.setup = function (seed, scenario, _options) {
current: 0,
state: null,
- // TODO: control, pieces, and tracks
+ cylinder: [ ELIGIBLE, ELIGIBLE, ELIGIBLE ],
+ resources: [ 12, 6, 7 ],
+ bk_inf: 0,
+ ve_inf: 0,
+ tributary: 8191, // all 13 provinces
+ rebel: 0, // amir/raja rebel status
+ pieces: [], // piece locations
+
deck: [],
}
diff --git a/tools/layout.svg b/tools/layout.svg
new file mode 100644
index 0000000..1ead306
--- /dev/null
+++ b/tools/layout.svg
@@ -0,0 +1,378 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="1275"
+ height="1650"
+ version="1.1"
+ id="svg4"
+ sodipodi:docname="layout.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata10">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs8" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="640"
+ inkscape:window-height="480"
+ id="namedview6"
+ showgrid="false"
+ inkscape:zoom="1.3237657"
+ inkscape:cx="732.66925"
+ inkscape:cy="245.31771"
+ inkscape:current-layer="g74"
+ inkscape:document-rotation="0" />
+ <image
+ sodipodi:absref="/home/tor/src/rally/public/vijayanagara/map75.jpg"
+ xlink:href="../map75.jpg"
+ id="image2"
+ sodipodi:insensitive="true"
+ image-rendering="pixelated"
+ height="1650"
+ width="1275"
+ y="0"
+ x="0" />
+ <g
+ id="g58"
+ inkscape:label="provinces">
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="path12"
+ cx="65.337875"
+ cy="393.10236"
+ r="34"
+ inkscape:label="Sindh" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle837"
+ cx="328.72156"
+ cy="402.87799"
+ r="34"
+ inkscape:label="Rajput Kingdoms" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle839"
+ cx="605.31427"
+ cy="572.21539"
+ r="34"
+ inkscape:label="Malwa" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle841"
+ cx="894.45099"
+ cy="455.29669"
+ r="34"
+ inkscape:label="Jaunpur" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle843"
+ cx="1192.2776"
+ cy="535.87579"
+ r="34"
+ inkscape:label="Bengal" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle845"
+ cx="1034.0819"
+ cy="858.19214"
+ r="34"
+ inkscape:label="Orissa" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle847"
+ cx="913.01575"
+ cy="736.92853"
+ r="34"
+ inkscape:label="Gondwana" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle849"
+ cx="670.44879"
+ cy="817.22009"
+ r="34"
+ inkscape:label="Madhyadesh" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle851"
+ cx="743.06372"
+ cy="1089.9803"
+ r="34"
+ inkscape:label="Andhra" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle853"
+ cx="438.38513"
+ cy="968.98218"
+ r="34"
+ inkscape:label="Maharashtra" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle855"
+ cx="220.07103"
+ cy="678.30847"
+ r="34"
+ inkscape:label="Gujarat" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle857"
+ cx="550.05054"
+ cy="1278.4006"
+ r="34"
+ inkscape:label="Karnataka" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="circle859"
+ cx="703.82556"
+ cy="1399.2897"
+ r="34"
+ inkscape:label="Tamilakam" />
+ </g>
+ <g
+ id="g64"
+ inkscape:label="available_boxes">
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect863"
+ width="177.06958"
+ height="110.22359"
+ x="23.983149"
+ y="99.914024"
+ inkscape:label="Mongol Invaders" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect865"
+ width="387.69797"
+ height="223.10062"
+ x="796.19531"
+ y="90.733284"
+ inkscape:label="DS Available" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect867"
+ width="237.8972"
+ height="223.72247"
+ x="21.017628"
+ y="908.0174"
+ inkscape:label="BK Available" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect869"
+ width="238.52563"
+ height="224.56042"
+ x="20.668497"
+ y="1405.1785"
+ inkscape:label="VE Available" />
+ </g>
+ <g
+ id="g86"
+ inkscape:label="tracks">
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect915"
+ width="60.631813"
+ height="61.421806"
+ x="1197.824"
+ y="403.09293"
+ inkscape:label="Track 24" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect913"
+ width="61.619305"
+ height="62.804291"
+ x="1196.8364"
+ y="14.219839"
+ inkscape:label="Track 18" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect911"
+ width="61.619305"
+ height="62.804291"
+ x="15.404826"
+ y="14.219839"
+ inkscape:label="Track 0" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect871"
+ width="61.446884"
+ height="62.564102"
+ x="18.15476"
+ y="1186.2042"
+ inkscape:label="BK Influence 0" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect873"
+ width="61.446884"
+ height="62.564102"
+ x="88.81868"
+ y="1186.4835"
+ inkscape:label="BK Influence 1" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect877"
+ width="61.446884"
+ height="62.564102"
+ x="160.11537"
+ y="1186.286"
+ inkscape:label="BK Influence 2" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect879"
+ width="61.446884"
+ height="62.564102"
+ x="231.21457"
+ y="1185.891"
+ inkscape:label="BK Influence 3" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect881"
+ width="61.446884"
+ height="62.564102"
+ x="302.31375"
+ y="1186.0885"
+ inkscape:label="BK Influence 4" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect885"
+ width="61.446884"
+ height="62.564102"
+ x="18.15476"
+ y="1291.7811"
+ inkscape:label="VE Influence 0" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect887"
+ width="61.446884"
+ height="62.564102"
+ x="88.81868"
+ y="1292.0604"
+ inkscape:label="VE Influence 1" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect889"
+ width="61.446884"
+ height="62.564102"
+ x="160.11537"
+ y="1291.8629"
+ inkscape:label="VE Influence 2" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect891"
+ width="61.446884"
+ height="62.564102"
+ x="231.21457"
+ y="1291.4679"
+ inkscape:label="VE Influence 3" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect893"
+ width="61.446884"
+ height="62.564102"
+ x="302.31375"
+ y="1291.6654"
+ inkscape:label="VE Influence 4" />
+ </g>
+ <g
+ id="g97"
+ inkscape:label="sequence_of_play">
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect895"
+ width="90.453941"
+ height="53.719433"
+ x="853.98035"
+ y="1305.0652"
+ inkscape:label="Limited Command" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect897"
+ width="90.848976"
+ height="200.65773"
+ x="853.58533"
+ y="1367.4745"
+ inkscape:label="Eligible Factions" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect899"
+ width="90.453979"
+ height="56.48436"
+ x="853.58533"
+ y="1577.6122"
+ inkscape:label="Pass" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect901"
+ width="90.058937"
+ height="201.0527"
+ x="1166.4218"
+ y="1366.6846"
+ inkscape:label="Ineligible Factions" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect905"
+ width="77.024101"
+ height="77.024071"
+ x="1016.3234"
+ y="1371.0295"
+ inkscape:label="Command and Decree" />
+ <rect
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:4"
+ id="rect907"
+ width="77.024101"
+ height="77.024071"
+ x="1016.3234"
+ y="1487.5532"
+ inkscape:label="Event or Command" />
+ </g>
+ <g
+ id="g74"
+ inkscape:label="mongol_invasion_regions">
+ <ellipse
+ style="fill:#00fff2;fill-opacity:0.566802;stroke-width:4"
+ id="path66"
+ cx="301.7406"
+ cy="139.98946"
+ rx="82.824387"
+ ry="27.608128"
+ inkscape:label="Mountain Passes" />
+ <ellipse
+ style="fill:#00fff2;fill-opacity:0.566802;stroke-width:4.16953"
+ id="path68"
+ cx="478.43262"
+ cy="219.56583"
+ rx="58.139465"
+ ry="18.838488"
+ inkscape:transform-center-x="0.5963195"
+ inkscape:transform-center-y="-19.991867"
+ inkscape:label="Punjab" />
+ <circle
+ style="fill:#00fff2;fill-opacity:0.566802;stroke:none;stroke-width:5.05769"
+ id="path861"
+ cx="646.8681"
+ cy="374.82599"
+ r="148"
+ inkscape:label="Delhi" />
+ </g>
+</svg>
diff --git a/tools/parse-layout.js b/tools/parse-layout.js
new file mode 100644
index 0000000..2060e17
--- /dev/null
+++ b/tools/parse-layout.js
@@ -0,0 +1,187 @@
+"use strict"
+
+const fs = require("fs")
+
+let circles = {}
+let rects = {}
+let edges = {}
+let mode, name, subname, x, y, w, h, cx, cy, rx, ry, x2, y2
+let labels = []
+
+function add_circle(cx, cy, rx, ry) {
+ if (!(name in circles))
+ circles[name] = {}
+ circles[name][subname] = {cx,cy,rx,ry}
+}
+
+function add_rect(x, y, w, h) {
+ if (!(name in rects))
+ rects[name] = {}
+ rects[name][subname] = {x,y,w,h}
+}
+
+function add_edge(x1, y1, x2, y2) {
+ if (name in edges)
+ edges[name].push({x1,y1,x2,y2})
+ else
+ edges[name] = [ {x1,y1,x2,y2} ]
+}
+
+function flush() {
+ if (mode === 'path') {
+ add_edge(x, y, x2, y2)
+ }
+ if (mode === 'rect') {
+ add_rect(x, y, w, h)
+ }
+ if (mode === 'circle') {
+ add_circle( cx, cy, rx, ry )
+ }
+ x = y = x2 = y2 = w = h = cx = cy = rx = ry = 0
+ subname = null
+}
+
+function parse_path_data(path) {
+ let cx = 0
+ let cy = 0
+ let abs = 0
+ for (let i = 0; i < path.length;) {
+ switch (path[i]) {
+ case 'M':
+ x2 = x = cx = Number(path[i+1])
+ y2 = y = cy = Number(path[i+2])
+ i += 3
+ abs = true
+ break
+ case 'm':
+ x2 = x = cx = cx + Number(path[i+1])
+ y2 = y = cy = cy + Number(path[i+2])
+ i += 3
+ abs = false
+ break
+ case 'C':
+ x2 = cx = Number(path[i+5])
+ y2 = cy = Number(path[i+6])
+ i += 7
+ abs = true
+ break
+ case 'L':
+ i += 1
+ abs = true
+ break
+ case 'H':
+ x2 = cx = Number(path[i+1])
+ i += 2
+ abs = true
+ break
+ case 'V':
+ y2 = cy = Number(path[i+1])
+ i += 2
+ abs = true
+ break
+ case 'c':
+ x2 = cx = cx + Number(path[i+5])
+ y2 = cy = cy + Number(path[i+6])
+ i += 7
+ abs = false
+ break
+ case 'l':
+ i += 1
+ abs = false
+ break
+ case 'h':
+ x2 = cx = cx + Number(path[i+1])
+ i += 2
+ abs = false
+ break
+ case 'v':
+ y2 = cy = cy + Number(path[i+1])
+ i += 2
+ abs = false
+ break
+ default:
+ if (abs) {
+ x2 = cx = Number(path[i+0])
+ y2 = cy = Number(path[i+1])
+ } else {
+ x2 = cx = cx + Number(path[i+0])
+ y2 = cy = cy + Number(path[i+1])
+ }
+ i += 2
+ break
+ }
+ }
+}
+
+for (let line of fs.readFileSync("tools/layout.svg", "utf-8").split("\n")) {
+ line = line.trim()
+ if (line.startsWith("<rect")) {
+ flush()
+ mode = "rect"
+ x = y = w = h = 0
+ }
+ else if (line.startsWith("<ellipse") || line.startsWith("<circle")) {
+ flush()
+ mode = "circle"
+ cx = cy = rx = ry = 0
+ }
+ else if (line.startsWith("<path")) {
+ flush()
+ mode = "path"
+ }
+ else if (line.startsWith("<g")) {
+ flush()
+ mode = "g"
+ }
+ else if (line.startsWith('x="'))
+ x = (Math.round(line.split('"')[1]))
+ else if (line.startsWith('y="'))
+ y = (Math.round(line.split('"')[1]))
+ else if (line.startsWith('width="'))
+ w = (Math.round(line.split('"')[1]))
+ else if (line.startsWith('height="'))
+ h = (Math.round(line.split('"')[1]))
+ else if (line.startsWith('cx="'))
+ cx = (Math.round(line.split('"')[1]))
+ else if (line.startsWith('cy="'))
+ cy = (Math.round(line.split('"')[1]))
+ else if (line.startsWith('r="'))
+ rx = ry = (Math.round(line.split('"')[1]))
+ else if (line.startsWith('rx="'))
+ rx = (Math.round(line.split('"')[1]))
+ else if (line.startsWith('ry="'))
+ ry = (Math.round(line.split('"')[1]))
+ else if (line.startsWith('inkscape:label="') && mode === "g")
+ name = line.split('"')[1]
+ else if (line.startsWith('inkscape:label="') && mode !== "g")
+ subname = line.split('"')[1]
+ else if (line.startsWith('d="'))
+ parse_path_data(line.split('"')[1].split(/[ ,]/))
+ if (line.includes("</tspan>")) {
+ let name = line.replace(/^[^>]*>/, "").replace(/<\/tspan.*/, "")
+ labels.push({x, y, name})
+ }
+}
+
+flush()
+
+function find_closest_node(list, x, y) {
+ let nd = Infinity, nn = null
+
+ for (let n of list) {
+ let d = Math.hypot(n.x - x, n.y - y)
+ if (d < nd) {
+ nd = d
+ nn = n
+ }
+ }
+
+ if (!nn) {
+ console.log("NOT FOUND", x, y)
+ return [ null, 0 ]
+ }
+
+ return [ nn, nd ]
+}
+
+console.log("const layout = " + JSON.stringify({ circles, rects }, null, "\t"))