mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:40:46 +00:00
@@ -172,11 +172,21 @@
|
||||
* rewrite EventsDisplay DONE 11/1/2024
|
||||
* update Mena NPC locations DONE 11/1/2024
|
||||
* create build menu DONE 11/3/2024
|
||||
* add gold
|
||||
* add troop/worker slider
|
||||
* add gold DONE 11/4/2024
|
||||
* add troop/worker slider DONE 11/4/2024
|
||||
* create Unit layer DONE 11/9/2024
|
||||
* create Unit interface DONE 11/10/2024
|
||||
* add destroyer DONE 11/12/2024
|
||||
* add ports DONE 11/14/2024
|
||||
* destroyer spawn from port DONE 11/14/2024
|
||||
* create trade routes DONE 11/15/2024
|
||||
* add trade ship DONE 11/15/2024
|
||||
* trade ship gives gold when completes route DONE 11/15/2024
|
||||
* add missile silo
|
||||
* nuke spawns from missile silo
|
||||
* destroyer can capture trade ships
|
||||
* add battleship
|
||||
* NPC has relations
|
||||
* fix name rendering
|
||||
* use twitter emojis
|
||||
* private game shows how many players joined
|
||||
* optimize sendBoat function
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 136 B |
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
shape-rendering="geometricPrecision"
|
||||
text-rendering="geometricPrecision"
|
||||
image-rendering="optimizeQuality"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
viewBox="0 0 500 499.99999"
|
||||
version="1.1"
|
||||
id="svg44"
|
||||
sodipodi:docname="Destroyer.svg"
|
||||
width="500"
|
||||
height="500"
|
||||
inkscape:export-filename="warship-icon.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs48" />
|
||||
<sodipodi:namedview
|
||||
id="namedview46"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.2877175"
|
||||
inkscape:cx="86.587311"
|
||||
inkscape:cy="266.36276"
|
||||
inkscape:window-width="1536"
|
||||
inkscape:window-height="987"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg44" />
|
||||
<path
|
||||
d="M 5.8295895,247.16089 C 65.39826,256.02013 106.88513,262.31167 162.00809,266.85556 l 14.35936,-93.47237 c 5.31364,0 10.70442,-0.0538 16.15307,-0.12099 v -59.97127 h 17.56104 V 172.953 l 12.17026,-0.25542 v -34.25394 h 17.56104 v 33.94474 c 19.59584,-0.26887 39.07597,-0.2151 57.44707,1.0217 11.50485,1.3309 21.51493,6.54696 29.54807,16.89843 7.85955,10.13636 13.56858,24.99139 16.59668,45.68087 l 0.10608,0.73939 0.0193,0.79316 0.58827,39.24146 c 21.23526,0.68562 28.57406,1.27713 50.74475,1.80142 l 97.12095,2.33917 c 1.9673,0.0403 4.58072,2.67524 3.57778,4.98752 l -29.90488,68.79018 c -0.99329,2.28538 -1.64906,4.98752 -3.57778,4.98752 H 50.305987 c -2.237321,0 -43.357737,-96.22828 -48.0541823,-107.50734 -0.7811336,-1.86864 0.5496866,-5.4446 3.5777848,-5.00097 z M 108.38954,189.01793 H 98.5434 l -1.330822,-16.25314 c -0.125367,-1.41156 -0.858282,-2.58114 -1.861219,-2.58114 H 71.531605 c -1.002937,0 -1.735852,1.16958 -1.851576,2.58114 l -1.340464,16.25314 H 59.4385 c -1.40797,0 -2.218034,1.72077 -2.594136,3.60285 l -0.829351,4.18092 -43.12629,-9.90783 -1.475475,10.29769 41.438656,15.6213 -6.422654,32.73482 c 25.989569,3.45497 51.352302,6.42597 76.38715,9.00712 l -11.84237,-61.93402 c -0.35681,-1.90897 -1.20545,-3.60285 -2.58449,-3.60285 z m 283.25255,37.11739 h 9.8365 l 1.34046,-16.25314 c 0.11572,-1.41156 0.839,-2.58114 1.85158,-2.58114 h 23.81975 c 1.02222,0 1.7455,1.16958 1.86122,2.58114 l 1.34046,16.25314 h 8.90107 c 1.42726,0 2.21803,1.72077 2.58449,3.60285 l 0.82935,4.18092 36.30825,-7.34013 1.47547,10.29769 -34.62061,13.0536 4.45536,22.65222 -56.76238,-1.35779 -13.67466,-0.33608 7.85955,-41.15043 c 0.36646,-1.90897 1.16688,-3.60285 2.59414,-3.60285 z m -121.095,-21.68429 h 52.28774 c 4.69644,9.86749 8.04278,19.95009 9.04572,30.28811 h -61.33346 z m -35.60426,-0.0135 h 22.49857 v 31.0275 h -22.49857 z"
|
||||
id="path42"
|
||||
style="stroke-width:1.13861" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
shape-rendering="geometricPrecision"
|
||||
text-rendering="geometricPrecision"
|
||||
image-rendering="optimizeQuality"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
viewBox="0 0 500 499.99999"
|
||||
version="1.1"
|
||||
id="svg44"
|
||||
sodipodi:docname="DestroyerIconWhite.svg"
|
||||
width="500"
|
||||
height="500"
|
||||
inkscape:export-filename="warship-icon.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs48" />
|
||||
<sodipodi:namedview
|
||||
id="namedview46"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.2877175"
|
||||
inkscape:cx="86.587311"
|
||||
inkscape:cy="266.36276"
|
||||
inkscape:window-width="1536"
|
||||
inkscape:window-height="987"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg44" />
|
||||
<path
|
||||
d="M 5.8295895,247.16089 C 65.39826,256.02013 106.88513,262.31167 162.00809,266.85556 l 14.35936,-93.47237 c 5.31364,0 10.70442,-0.0538 16.15307,-0.12099 v -59.97127 h 17.56104 V 172.953 l 12.17026,-0.25542 v -34.25394 h 17.56104 v 33.94474 c 19.59584,-0.26887 39.07597,-0.2151 57.44707,1.0217 11.50485,1.3309 21.51493,6.54696 29.54807,16.89843 7.85955,10.13636 13.56858,24.99139 16.59668,45.68087 l 0.10608,0.73939 0.0193,0.79316 0.58827,39.24146 c 21.23526,0.68562 28.57406,1.27713 50.74475,1.80142 l 97.12095,2.33917 c 1.9673,0.0403 4.58072,2.67524 3.57778,4.98752 l -29.90488,68.79018 c -0.99329,2.28538 -1.64906,4.98752 -3.57778,4.98752 H 50.305987 c -2.237321,0 -43.357737,-96.22828 -48.0541823,-107.50734 -0.7811336,-1.86864 0.5496866,-5.4446 3.5777848,-5.00097 z M 108.38954,189.01793 H 98.5434 l -1.330822,-16.25314 c -0.125367,-1.41156 -0.858282,-2.58114 -1.861219,-2.58114 H 71.531605 c -1.002937,0 -1.735852,1.16958 -1.851576,2.58114 l -1.340464,16.25314 H 59.4385 c -1.40797,0 -2.218034,1.72077 -2.594136,3.60285 l -0.829351,4.18092 -43.12629,-9.90783 -1.475475,10.29769 41.438656,15.6213 -6.422654,32.73482 c 25.989569,3.45497 51.352302,6.42597 76.38715,9.00712 l -11.84237,-61.93402 c -0.35681,-1.90897 -1.20545,-3.60285 -2.58449,-3.60285 z m 283.25255,37.11739 h 9.8365 l 1.34046,-16.25314 c 0.11572,-1.41156 0.839,-2.58114 1.85158,-2.58114 h 23.81975 c 1.02222,0 1.7455,1.16958 1.86122,2.58114 l 1.34046,16.25314 h 8.90107 c 1.42726,0 2.21803,1.72077 2.58449,3.60285 l 0.82935,4.18092 36.30825,-7.34013 1.47547,10.29769 -34.62061,13.0536 4.45536,22.65222 -56.76238,-1.35779 -13.67466,-0.33608 7.85955,-41.15043 c 0.36646,-1.90897 1.16688,-3.60285 2.59414,-3.60285 z m -121.095,-21.68429 h 52.28774 c 4.69644,9.86749 8.04278,19.95009 9.04572,30.28811 h -61.33346 z m -35.60426,-0.0135 h 22.49857 v 31.0275 h -22.49857 z"
|
||||
id="path42"
|
||||
style="stroke-width:1.13861" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke-width:0.776568"
|
||||
d="M 46.706349,351.9808 C 42.004196,342.57569 31.558258,319.09983 15.001906,280.72927 4.2738049,255.8661 2.3933801,250.94457 3.0592429,249.47241 c 0.4346644,-0.961 1.4320088,-1.74728 2.2163209,-1.74728 0.7843122,0 15.7310272,2.08277 33.2149232,4.62839 55.538666,8.08629 122.159113,15.92117 123.550193,14.53008 0.23554,-0.23552 3.6233,-21.24116 7.52837,-46.67918 l 7.10013,-46.25095 7.95982,-0.001 7.95982,-0.001 V 143.66505 113.3789 h 8.54224 8.54225 v 29.89786 29.89786 h 6.60083 6.60082 v -17.08449 -17.08449 h 8.15396 8.15397 v 16.69621 16.6962 l 13.39579,0.0102 c 21.89937,0.0167 46.22637,1.33987 51.05934,2.77709 18.87306,5.61245 30.73872,21.45768 37.4049,49.94994 1.26524,5.40782 1.67375,10.89917 2.15857,29.0159 0.32889,12.29014 0.73487,22.48072 0.90216,22.64572 0.54568,0.53819 41.93742,1.93217 95.43373,3.21399 54.05235,1.29515 55.99853,1.44928 55.87254,4.42493 -0.0222,0.52405 -7.2771,17.55195 -16.12202,37.83978 l -16.08168,36.88697 -206.35685,0.19558 -206.356855,0.19559 -3.786166,-7.57298 z M 257.82051,220.15698 V 204.23734 H 246.172 234.52348 v 15.91964 15.91964 h 11.64852 11.64851 z m 74.1798,11.84265 c -0.98753,-6.42557 -3.63791,-15.80032 -6.09246,-21.54975 l -2.48647,-5.82426 -26.58789,-0.20434 -26.58789,-0.20434 v 15.54156 15.54155 h 31.13097 31.13098 z"
|
||||
id="path370" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke-width:0.776568"
|
||||
d="m 107.94293,252.66328 c -5.33891,-0.60164 -20.99574,-2.48304 -34.792965,-4.18087 -13.797226,-1.69784 -25.366829,-3.08698 -25.710229,-3.08698 -0.343401,0 0.8512,-7.35153 2.654669,-16.33673 2.940969,-14.65242 3.149735,-16.40042 2.02491,-16.95453 -0.689767,-0.3398 -10.071533,-3.90876 -20.848367,-7.93102 -14.89771,-5.56031 -19.520201,-7.59241 -19.285331,-8.47806 0.169902,-0.64067 0.525567,-2.76568 0.790368,-4.72225 l 0.481454,-3.5574 19.580444,4.50115 c 10.769244,2.47564 20.492947,4.70513 21.608227,4.95443 1.844631,0.41233 2.069192,0.19808 2.486266,-2.37205 0.70519,-4.34557 1.657226,-5.01642 7.119029,-5.01642 h 4.842078 l 0.550514,-8.70006 c 0.302782,-4.78504 0.722598,-8.9785 0.932924,-9.31882 0.210326,-0.34031 6.104255,-0.61875 13.097618,-0.61875 14.76188,0 13.249343,-1.15028 14.040604,10.67781 l 0.532477,7.95982 h 5.664 5.664 l 0.80464,2.52384 c 0.80804,2.53444 11.74089,59.3751 11.74089,61.04166 0,1.06925 -1.42288,1.03009 -13.97822,-0.38477 z"
|
||||
id="path372" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke-width:0.776568"
|
||||
d="m 416.88697,271.41698 c -18.65086,-0.4307 -34.15559,-1.02828 -34.45497,-1.32794 -0.53305,-0.53356 6.25145,-38.17574 7.4887,-41.54929 0.55729,-1.51951 1.26654,-1.7384 6.29062,-1.94141 l 5.66252,-0.22882 0.77657,-8.54225 c 0.42711,-4.69823 1.05929,-8.97906 1.40483,-9.51295 0.45782,-0.70738 3.87376,-0.97071 12.5919,-0.97071 10.68101,0 12.04305,0.14836 12.70426,1.38384 0.40734,0.76112 0.9915,5.12931 1.29815,9.7071 l 0.55754,8.32325 h 5.2935 5.29349 l 0.80874,2.91213 c 0.44481,1.60167 0.81069,3.35561 0.81307,3.89764 0.003,0.6267 0.64048,0.83439 1.75161,0.57047 4.27407,-1.01523 32.80247,-6.60367 33.71102,-6.60367 1.00175,0 1.31418,0.90275 2.17005,6.26999 l 0.44256,2.77543 -17.28817,6.44808 c -9.5085,3.54644 -17.34584,6.49096 -17.4163,6.54338 -0.0705,0.0524 0.89062,5.1332 2.13575,11.29063 1.24513,6.15743 2.17643,11.22747 2.06954,11.26676 -0.10688,0.0393 -15.45412,-0.28096 -34.10498,-0.71166 z"
|
||||
id="path374" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 100 125"
|
||||
enable-background="new 0 0 100 100"
|
||||
xml:space="preserve"
|
||||
id="svg12"
|
||||
sodipodi:docname="PortIcon.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs16" /><sodipodi:namedview
|
||||
id="namedview14"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="6.984"
|
||||
inkscape:cx="15.463918"
|
||||
inkscape:cy="62.142039"
|
||||
inkscape:window-width="3072"
|
||||
inkscape:window-height="1653"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg12" /><g
|
||||
id="g6"><g
|
||||
id="g4"><path
|
||||
d="M86.946,70.119l-2.417-15.098c-0.286-1.419-1.511-2.495-2.982-2.495c-0.899,0-1.687,0.385-2.268,0.997L68.104,63.997 c-0.176,0.156-0.352,0.338-0.515,0.56c-1.049,1.433-0.749,3.465,0.684,4.527c0.273,0.215,0.573,0.345,0.866,0.462l2.808,0.931 c-2.697,7.641-9.313,13.45-17.417,15.059l0.014-45.638h5.992c1.889,0,3.412-1.538,3.412-3.426c0-1.875-1.523-3.413-3.412-3.413 h-5.992v-6.78c3.979-1.687,6.721-5.634,6.721-10.206C61.264,9.95,56.314,5,50.192,5c-6.097,0-11.073,4.95-11.073,11.073 c0,4.578,2.847,8.52,6.8,10.206l0.007,6.78h-6.468c-1.882,0-3.413,1.538-3.413,3.413c0,1.889,1.531,3.426,3.413,3.426h6.468 l0.021,45.71c-8.292-1.485-15.15-7.354-17.926-15.131l2.827-0.931c0.299-0.117,0.6-0.247,0.879-0.456 c1.427-1.067,1.753-3.1,0.671-4.533c-0.155-0.222-0.313-0.404-0.508-0.554L20.706,53.524c-0.553-0.612-1.361-0.997-2.246-0.997 c-1.486,0-2.743,1.075-3.009,2.495l-2.424,15.098c-0.053,0.241-0.064,0.508-0.064,0.762c0,1.791,1.446,3.236,3.229,3.236 c0.339,0,0.658-0.032,0.959-0.137l2.624-0.853C23.956,85.816,35.901,95,49.997,95c14.094,0,26.053-9.184,30.221-21.891 l2.619,0.872c0.299,0.105,0.618,0.137,0.963,0.137c1.785,0,3.237-1.446,3.237-3.236C87.037,70.627,87.012,70.36,86.946,70.119 M45.659,16.073c0-2.521,2.044-4.553,4.533-4.553c2.527,0,4.552,2.032,4.552,4.553c0,2.514-2.024,4.559-4.552,4.559 C47.703,20.632,45.659,18.586,45.659,16.073"
|
||||
id="path2" /></g></g><path
|
||||
style="fill:#ffffff;stroke:none;stroke-width:0.143184"
|
||||
d="M 46.821306,94.770829 C 37.149181,93.798182 28.293166,88.256973 23.04965,79.896907 c -0.868866,-1.385288 -2.385574,-4.487112 -2.870545,-5.870561 -0.179443,-0.511884 -0.334071,-0.944343 -0.343618,-0.96102 -0.0095,-0.01668 -0.705224,0.192731 -1.545948,0.46535 -0.840723,0.272619 -1.804312,0.494255 -2.141309,0.492526 -1.598809,-0.0082 -3.122304,-1.547171 -3.115278,-3.146913 8.64e-4,-0.196879 0.574575,-3.918388 1.274912,-8.270022 1.419295,-8.818969 1.428076,-8.850837 2.627499,-9.536632 0.834583,-0.477189 1.483957,-0.563323 2.337029,-0.309989 0.644845,0.191499 1.336272,0.793564 6.866372,5.978939 4.549564,4.26596 6.233135,5.932201 6.471488,6.40487 0.603214,1.196211 0.325525,2.693184 -0.677921,3.654548 -0.288018,0.275937 -1.002559,0.607386 -2.183926,1.013042 -2.007091,0.68919 -1.921853,0.529349 -1.189152,2.229934 2.313522,5.369652 7.064022,9.988932 12.577448,12.230032 1.263767,0.513697 3.384288,1.159414 4.275471,1.30192 l 0.550027,0.08795 V 62.733077 39.805269 H 42.37799 c -3.958889,0 -4.236614,-0.04887 -5.126792,-0.902159 -0.76147,-0.729916 -1.085823,-1.473414 -1.081006,-2.477941 0.0071,-1.483668 0.947583,-2.699422 2.43431,-3.146861 0.49772,-0.149791 1.545965,-0.202707 4.01562,-0.202707 h 3.342077 V 29.652118 26.228633 L 44.85252,25.624122 c -0.793899,-0.432485 -1.482722,-0.979822 -2.420583,-1.923389 -1.146297,-1.15327 -1.401076,-1.505996 -2.029023,-2.809061 -0.858345,-1.781165 -1.170382,-3.086543 -1.170382,-4.896174 0,-6.4184795 5.839733,-11.6186738 12.164919,-10.8326779 2.656895,0.3301574 4.668838,1.3296881 6.615711,3.286675 1.070143,1.0757025 1.339007,1.4513238 1.957431,2.7346549 0.865772,1.796619 1.169155,3.085051 1.169155,4.965255 0,4.023175 -2.191435,7.679165 -5.755536,9.602011 l -0.969271,0.522925 -0.0024,3.389446 -0.0024,3.389446 3.472222,0.04702 c 3.946037,0.05344 4.168331,0.108713 5.177124,1.287261 1.316051,1.53751 0.926562,3.821223 -0.837885,4.912821 l -0.700444,0.433339 -3.555509,0.04279 -3.555508,0.0428 v 22.902508 c 0,12.596381 0.04833,22.902346 0.107388,22.902148 0.332248,-0.0011 2.935079,-0.780331 4.075018,-1.219944 5.752163,-2.218302 10.721311,-7.144396 13.059805,-12.946652 0.204466,-0.507318 0.322918,-0.966456 0.263226,-1.020307 -0.05969,-0.05385 -0.65621,-0.274921 -1.325597,-0.491268 -1.73484,-0.560706 -2.51001,-1.013168 -2.998016,-1.749923 -0.680235,-1.026966 -0.727607,-2.0483 -0.14813,-3.19361 0.309132,-0.610985 12.085183,-11.683542 12.854449,-12.08653 1.199252,-0.628243 2.81487,-0.181005 3.651625,1.010847 0.392869,0.559594 0.481176,1.003292 1.695561,8.519473 0.875547,5.419011 1.264717,8.190005 1.229335,8.753186 -0.06941,1.104804 -0.613524,1.897274 -1.670778,2.433394 -1.027413,0.520988 -1.635513,0.502208 -3.442467,-0.106313 -0.816705,-0.275039 -1.512424,-0.472563 -1.546044,-0.438944 -0.03362,0.03362 -0.379938,0.854657 -0.769596,1.824527 -2.13365,5.310705 -5.61865,9.924534 -10.123033,13.401993 -6.338429,4.893379 -14.56767,7.254819 -22.501513,6.456977 z m 4.86827,-74.330261 c 1.140586,-0.346728 2.230937,-1.344337 2.770928,-2.535241 0.21803,-0.480845 0.29704,-0.95557 0.301498,-1.811516 0.0055,-1.049248 -0.04177,-1.253646 -0.495106,-2.142377 -1.123467,-2.202486 -3.642798,-3.073429 -5.980881,-2.067613 -0.987511,0.424816 -2.068224,1.59611 -2.409954,2.611949 -1.248434,3.711132 2.053637,7.087771 5.813515,5.944798 z"
|
||||
id="path257" /></svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
@@ -1,5 +1,5 @@
|
||||
import { Executor } from "../core/execution/ExecutionManager";
|
||||
import { Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Player, Game, BoatEvent, Tile, PlayerType, GameMap, Difficulty } from "../core/game/Game";
|
||||
import { Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Player, Game, UnitEvent, Tile, PlayerType, GameMap, Difficulty } from "../core/game/Game";
|
||||
import { createGame } from "../core/game/GameImpl";
|
||||
import { EventBus } from "../core/EventBus";
|
||||
import { Config, getConfig } from "../core/configuration/Config";
|
||||
|
||||
+21
-2
@@ -1,7 +1,7 @@
|
||||
import { Config } from "../core/configuration/Config"
|
||||
import { EventBus, GameEvent } from "../core/EventBus"
|
||||
import { AllianceRequest, AllPlayers, Cell, Player, PlayerID, PlayerType } from "../core/game/Game"
|
||||
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema } from "../core/Schemas"
|
||||
import { AllianceRequest, AllPlayers, Cell, BuildItem, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game"
|
||||
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, BuildUnitIntentSchema, GameID, Intent, ServerMessage, ServerMessageSchema } from "../core/Schemas"
|
||||
import { LocalServer } from "./LocalServer"
|
||||
|
||||
|
||||
@@ -47,6 +47,13 @@ export class SendBoatAttackIntentEvent implements GameEvent {
|
||||
) { }
|
||||
}
|
||||
|
||||
export class BuildUnitIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly unit: UnitType,
|
||||
public readonly cell: Cell,
|
||||
) { }
|
||||
}
|
||||
|
||||
export class SendTargetPlayerIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly targetID: PlayerID,
|
||||
@@ -115,6 +122,7 @@ export class Transport {
|
||||
this.eventBus.on(SendDonateIntentEvent, (e) => this.onSendDonateIntent(e))
|
||||
this.eventBus.on(SendNukeIntentEvent, (e) => this.onSendNukeIntent(e))
|
||||
this.eventBus.on(SendSetTargetTroopRatioEvent, (e) => this.onSendSetTargetTroopRatioEvent(e))
|
||||
this.eventBus.on(BuildUnitIntentEvent, (e) => this.onCreateDestroyerIntent(e))
|
||||
}
|
||||
|
||||
connect(onconnect: () => void, onmessage: (message: ServerMessage) => void) {
|
||||
@@ -314,6 +322,17 @@ export class Transport {
|
||||
})
|
||||
}
|
||||
|
||||
private onCreateDestroyerIntent(event: BuildUnitIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "build_unit",
|
||||
clientID: this.clientID,
|
||||
player: this.playerID,
|
||||
unit: event.unit,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y,
|
||||
})
|
||||
}
|
||||
|
||||
private sendIntent(intent: Intent) {
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientIntentMessageSchema.parse({
|
||||
|
||||
@@ -14,6 +14,9 @@ import { Leaderboard } from "./layers/Leaderboard";
|
||||
import { ControlPanel } from "./layers/ControlPanel";
|
||||
import { UIState } from "./UIState";
|
||||
import { BuildMenu } from "./layers/radial/BuildMenu";
|
||||
import { UnitLayer } from "./layers/UnitLayer";
|
||||
import { BuildValidator } from "../../core/game/BuildValidator";
|
||||
import { StructureLayer } from "./layers/StructureLayer";
|
||||
|
||||
|
||||
export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus: EventBus, clientID: ClientID): GameRenderer {
|
||||
@@ -32,7 +35,7 @@ export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus:
|
||||
}
|
||||
buildMenu.game = game
|
||||
buildMenu.eventBus = eventBus
|
||||
buildMenu.init()
|
||||
buildMenu.buildValidator = new BuildValidator(game)
|
||||
|
||||
const leaderboard = document.querySelector('leader-board') as Leaderboard;
|
||||
if (!emojiTable || !(leaderboard instanceof Leaderboard)) {
|
||||
@@ -61,6 +64,8 @@ export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus:
|
||||
const layers: Layer[] = [
|
||||
new TerrainLayer(game),
|
||||
new TerritoryLayer(game, eventBus),
|
||||
new StructureLayer(game, eventBus),
|
||||
new UnitLayer(game, eventBus),
|
||||
new NameLayer(game, game.config().theme(), transformHandler, clientID),
|
||||
new UILayer(eventBus, game, clientID, transformHandler),
|
||||
eventsDisplay,
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { Colord } from "colord";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { Unit, UnitEvent, Cell, Game, Tile, UnitType } from "../../../core/game/Game";
|
||||
import { bfs, dist, euclDist } from "../../../core/Util";
|
||||
import { Layer } from "./Layer";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
|
||||
import anchorIcon from '../../../../resources/images/AnchorIcon.png';
|
||||
|
||||
export class StructureLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private imageData: ImageData;
|
||||
private anchorImage: HTMLImageElement;
|
||||
private anchorImageLoaded: boolean = false;
|
||||
|
||||
|
||||
private theme: Theme = null;
|
||||
|
||||
constructor(private game: Game, private eventBus: EventBus) {
|
||||
this.theme = game.config().theme();
|
||||
this.loadAnchorImage();
|
||||
}
|
||||
|
||||
private loadAnchorImage() {
|
||||
this.anchorImage = new Image();
|
||||
this.anchorImage.onload = () => {
|
||||
this.anchorImageLoaded = true;
|
||||
};
|
||||
this.anchorImage.src = anchorIcon;
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
tick() {
|
||||
}
|
||||
|
||||
init(game: Game) {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.context = this.canvas.getContext("2d");
|
||||
|
||||
this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height());
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
this.initImageData();
|
||||
|
||||
this.eventBus.on(UnitEvent, e => this.onUnitEvent(e));
|
||||
}
|
||||
|
||||
initImageData() {
|
||||
this.game.forEachTile((tile) => {
|
||||
const index = (tile.cell().y * this.game.width()) + tile.cell().x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset + 3] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
);
|
||||
}
|
||||
|
||||
private handlePortEvent(event: UnitEvent) {
|
||||
if (!this.anchorImageLoaded) return;
|
||||
|
||||
bfs(event.unit.tile(), euclDist(event.unit.tile(), 8))
|
||||
.forEach(t => this.clearCell(t.cell()));
|
||||
|
||||
if (!event.unit.isActive()) {
|
||||
return
|
||||
}
|
||||
// Create a temporary canvas to process the anchor icon
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempContext = tempCanvas.getContext('2d');
|
||||
tempCanvas.width = this.anchorImage.width;
|
||||
tempCanvas.height = this.anchorImage.height;
|
||||
|
||||
// Draw the anchor icon to the temporary canvas
|
||||
tempContext.drawImage(this.anchorImage, 0, 0);
|
||||
const iconData = tempContext.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||
|
||||
// Calculate position to center the icon on the port
|
||||
const cell = event.unit.tile().cell();
|
||||
const startX = cell.x - Math.floor(tempCanvas.width / 2);
|
||||
const startY = cell.y - Math.floor(tempCanvas.height / 2);
|
||||
|
||||
bfs(event.unit.tile(), euclDist(event.unit.tile(), 8))
|
||||
.forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.unit.owner().info()), 255));
|
||||
|
||||
bfs(event.unit.tile(), euclDist(event.unit.tile(), 6))
|
||||
.forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 255));
|
||||
// Process each pixel of the icon
|
||||
for (let y = 0; y < tempCanvas.height; y++) {
|
||||
for (let x = 0; x < tempCanvas.width; x++) {
|
||||
const iconIndex = (y * tempCanvas.width + x) * 4;
|
||||
const alpha = iconData.data[iconIndex + 3];
|
||||
|
||||
if (alpha > 0) { // Only process non-transparent pixels
|
||||
const targetX = startX + x;
|
||||
const targetY = startY + y;
|
||||
|
||||
// Check if the target pixel is within the game bounds
|
||||
if (targetX >= 0 && targetX < this.game.width() &&
|
||||
targetY >= 0 && targetY < this.game.height()) {
|
||||
|
||||
// Color the pixel using the unit owner's colors
|
||||
this.paintCell(
|
||||
new Cell(targetX, targetY),
|
||||
this.theme.borderColor(event.unit.owner().info()),
|
||||
alpha
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onUnitEvent(event: UnitEvent) {
|
||||
switch (event.unit.type()) {
|
||||
case UnitType.Port:
|
||||
this.handlePortEvent(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
paintCell(cell: Cell, color: Colord, alpha: number) {
|
||||
const index = (cell.y * this.game.width()) + cell.x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset] = color.rgba.r;
|
||||
this.imageData.data[offset + 1] = color.rgba.g;
|
||||
this.imageData.data[offset + 2] = color.rgba.b;
|
||||
this.imageData.data[offset + 3] = alpha;
|
||||
}
|
||||
|
||||
clearCell(cell: Cell) {
|
||||
const index = (cell.y * this.game.width()) + cell.x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,33 @@
|
||||
import {PriorityQueue} from "@datastructures-js/priority-queue";
|
||||
import {Boat, BoatEvent, Cell, Game, Player, Tile, TileEvent} from "../../../core/game/Game";
|
||||
import {PseudoRandom} from "../../../core/PseudoRandom";
|
||||
import {Colord} from "colord";
|
||||
import {bfs, dist} from "../../../core/Util";
|
||||
import {Theme} from "../../../core/configuration/Config";
|
||||
import {Layer} from "./Layer";
|
||||
import {TransformHandler} from "../TransformHandler";
|
||||
import {EventBus} from "../../../core/EventBus";
|
||||
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
||||
import { Cell, Game, Player, Tile, TileEvent } from "../../../core/game/Game";
|
||||
import { PseudoRandom } from "../../../core/PseudoRandom";
|
||||
import { Colord } from "colord";
|
||||
import { bfs, dist } from "../../../core/Util";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { Layer } from "./Layer";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
|
||||
export class TerritoryLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement
|
||||
private context: CanvasRenderingContext2D
|
||||
private imageData: ImageData
|
||||
|
||||
private tileToRenderQueue: PriorityQueue<{tileEvent: TileEvent, lastUpdate: number}> = new PriorityQueue((a, b) => {return a.lastUpdate - b.lastUpdate})
|
||||
private tileToRenderQueue: PriorityQueue<{ tileEvent: TileEvent, lastUpdate: number }> = new PriorityQueue((a, b) => { return a.lastUpdate - b.lastUpdate })
|
||||
private random = new PseudoRandom(123)
|
||||
private theme: Theme = null
|
||||
|
||||
private boatToTrail = new Map<Boat, Set<Tile>>()
|
||||
|
||||
|
||||
constructor(private game: Game, eventBus: EventBus) {
|
||||
this.theme = game.config().theme()
|
||||
eventBus.on(TileEvent, e => this.tileUpdate(e))
|
||||
eventBus.on(BoatEvent, e => this.boatEvent(e))
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
tick() {
|
||||
}
|
||||
|
||||
@@ -62,31 +62,6 @@ export class TerritoryLayer implements Layer {
|
||||
)
|
||||
}
|
||||
|
||||
boatEvent(event: BoatEvent) {
|
||||
if (!this.boatToTrail.has(event.boat)) {
|
||||
this.boatToTrail.set(event.boat, new Set<Tile>())
|
||||
}
|
||||
const trail = this.boatToTrail.get(event.boat)
|
||||
trail.add(event.oldTile)
|
||||
bfs(event.oldTile, dist(event.oldTile, 3)).forEach(t => {
|
||||
this.paintTerritory(t)
|
||||
})
|
||||
if (event.boat.isActive()) {
|
||||
bfs(event.boat.tile(), dist(event.boat.tile(), 4)).forEach(
|
||||
t => {
|
||||
if (trail.has(t)) {
|
||||
this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().info()), 150)
|
||||
}
|
||||
}
|
||||
)
|
||||
bfs(event.boat.tile(), dist(event.boat.tile(), 2)).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().info()), 255))
|
||||
bfs(event.boat.tile(), dist(event.boat.tile(), 1)).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().info()), 180))
|
||||
} else {
|
||||
trail.forEach(t => this.paintTerritory(t))
|
||||
this.boatToTrail.delete(event.boat)
|
||||
}
|
||||
}
|
||||
|
||||
renderTerritory() {
|
||||
let numToRender = Math.floor(this.tileToRenderQueue.size() / 10)
|
||||
if (numToRender == 0) {
|
||||
@@ -138,6 +113,6 @@ export class TerritoryLayer implements Layer {
|
||||
}
|
||||
|
||||
tileUpdate(event: TileEvent) {
|
||||
this.tileToRenderQueue.push({tileEvent: event, lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5)})
|
||||
this.tileToRenderQueue.push({ tileEvent: event, lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Colord } from "colord";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { Unit, UnitEvent, Cell, Game, Tile, UnitType } from "../../../core/game/Game";
|
||||
import { bfs, dist, euclDist } from "../../../core/Util";
|
||||
import { Layer } from "./Layer";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
|
||||
import anchorIcon from '../../../../resources/images/AnchorIcon.png';
|
||||
|
||||
export class UnitLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private imageData: ImageData;
|
||||
private anchorImage: HTMLImageElement;
|
||||
private anchorImageLoaded: boolean = false;
|
||||
|
||||
private boatToTrail = new Map<Unit, Set<Tile>>();
|
||||
|
||||
private theme: Theme = null;
|
||||
|
||||
constructor(private game: Game, private eventBus: EventBus) {
|
||||
this.theme = game.config().theme();
|
||||
this.loadAnchorImage();
|
||||
}
|
||||
|
||||
private loadAnchorImage() {
|
||||
this.anchorImage = new Image();
|
||||
this.anchorImage.onload = () => {
|
||||
this.anchorImageLoaded = true;
|
||||
};
|
||||
this.anchorImage.src = anchorIcon;
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
tick() {
|
||||
}
|
||||
|
||||
init(game: Game) {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.context = this.canvas.getContext("2d");
|
||||
|
||||
this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height());
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
this.initImageData();
|
||||
|
||||
this.eventBus.on(UnitEvent, e => this.onUnitEvent(e));
|
||||
}
|
||||
|
||||
initImageData() {
|
||||
this.game.forEachTile((tile) => {
|
||||
const index = (tile.cell().y * this.game.width()) + tile.cell().x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset + 3] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
);
|
||||
}
|
||||
|
||||
onUnitEvent(event: UnitEvent) {
|
||||
switch (event.unit.type()) {
|
||||
case UnitType.TransportShip:
|
||||
this.handleBoatEvent(event);
|
||||
break;
|
||||
case UnitType.Destroyer:
|
||||
this.handleDestroyerEvent(event);
|
||||
break;
|
||||
case UnitType.TradeShip:
|
||||
this.handleTradeShipEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private handleDestroyerEvent(event: UnitEvent) {
|
||||
bfs(event.oldTile, euclDist(event.oldTile, 3)).forEach(t => {
|
||||
this.clearCell(t.cell());
|
||||
});
|
||||
bfs(event.unit.tile(), euclDist(event.unit.tile(), 3))
|
||||
.forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.unit.owner().info()), 255));
|
||||
bfs(event.unit.tile(), euclDist(event.unit.tile(), 2))
|
||||
.forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 180));
|
||||
}
|
||||
|
||||
private handleTradeShipEvent(event: UnitEvent) {
|
||||
bfs(event.oldTile, euclDist(event.oldTile, 1)).forEach(t => {
|
||||
this.clearCell(t.cell());
|
||||
});
|
||||
if (event.unit.isActive()) {
|
||||
bfs(event.unit.tile(), euclDist(event.unit.tile(), 1))
|
||||
.forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.unit.owner().info()), 255));
|
||||
}
|
||||
}
|
||||
|
||||
private handleBoatEvent(event: UnitEvent) {
|
||||
if (!this.boatToTrail.has(event.unit)) {
|
||||
this.boatToTrail.set(event.unit, new Set<Tile>());
|
||||
}
|
||||
const trail = this.boatToTrail.get(event.unit);
|
||||
trail.add(event.oldTile);
|
||||
bfs(event.oldTile, dist(event.oldTile, 3)).forEach(t => {
|
||||
this.clearCell(t.cell());
|
||||
});
|
||||
if (event.unit.isActive()) {
|
||||
bfs(event.unit.tile(), dist(event.unit.tile(), 4)).forEach(
|
||||
t => {
|
||||
if (trail.has(t)) {
|
||||
this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 150);
|
||||
}
|
||||
}
|
||||
);
|
||||
bfs(event.unit.tile(), dist(event.unit.tile(), 2))
|
||||
.forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.unit.owner().info()), 255));
|
||||
bfs(event.unit.tile(), dist(event.unit.tile(), 1))
|
||||
.forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 180));
|
||||
} else {
|
||||
trail.forEach(t => this.clearCell(t.cell()));
|
||||
this.boatToTrail.delete(event.unit);
|
||||
}
|
||||
}
|
||||
|
||||
paintCell(cell: Cell, color: Colord, alpha: number) {
|
||||
const index = (cell.y * this.game.width()) + cell.x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset] = color.rgba.r;
|
||||
this.imageData.data[offset + 1] = color.rgba.g;
|
||||
this.imageData.data[offset + 2] = color.rgba.b;
|
||||
this.imageData.data[offset + 3] = alpha;
|
||||
}
|
||||
|
||||
clearCell(cell: Cell) {
|
||||
const index = (cell.y * this.game.width()) + cell.x;
|
||||
const offset = index * 4;
|
||||
this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,26 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { EventBus } from '../../../../core/EventBus';
|
||||
import { Cell, Game, Item, Items, Player } from '../../../../core/game/Game';
|
||||
import { SendNukeIntentEvent } from '../../../Transport';
|
||||
import { Cell, Game, BuildItem, BuildItems, Player, UnitType } from '../../../../core/game/Game';
|
||||
import { BuildUnitIntentEvent, SendNukeIntentEvent } from '../../../Transport';
|
||||
import nukeIcon from '../../../../../resources/images/NukeIconWhite.svg';
|
||||
import destroyerIcon from '../../../../../resources/images/DestroyerIconWhite.svg';
|
||||
import goldCoinIcon from '../../../../../resources/images/GoldCoinIcon.svg';
|
||||
import portIcon from '../../../../../resources/images/PortIcon.svg';
|
||||
import { renderNumber } from '../../Utils';
|
||||
import { BuildValidator } from '../../../../core/game/BuildValidator';
|
||||
import { ContextMenuEvent } from '../../../InputHandler';
|
||||
|
||||
interface BuildItem {
|
||||
item: Item
|
||||
interface BuildItemDisplay {
|
||||
item: BuildItem
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const buildTable: BuildItem[][] = [
|
||||
const buildTable: BuildItemDisplay[][] = [
|
||||
[
|
||||
{ item: Items.Nuke, icon: nukeIcon },
|
||||
// { id: 'battleship', name: 'Battleship', icon: '🚢', cost: 500, buildTime: 20 }
|
||||
{ item: BuildItems.Nuke, icon: nukeIcon },
|
||||
{ item: BuildItems.Destroyer, icon: destroyerIcon },
|
||||
{ item: BuildItems.Port, icon: portIcon }
|
||||
]
|
||||
];
|
||||
|
||||
@@ -24,6 +28,7 @@ const buildTable: BuildItem[][] = [
|
||||
export class BuildMenu extends LitElement {
|
||||
public game: Game;
|
||||
public eventBus: EventBus;
|
||||
public buildValidator: BuildValidator;
|
||||
|
||||
private myPlayer: Player;
|
||||
private clickedCell: Cell;
|
||||
@@ -142,12 +147,25 @@ export class BuildMenu extends LitElement {
|
||||
@state()
|
||||
private _hidden = true;
|
||||
|
||||
private canAfford(item: BuildItem): boolean {
|
||||
return this.myPlayer && this.myPlayer.gold() >= item.item.cost;
|
||||
private canBuild(item: BuildItemDisplay): boolean {
|
||||
if (this.myPlayer == null) {
|
||||
return false
|
||||
}
|
||||
return this.buildValidator.canBuild(this.myPlayer, this.game.tile(this.clickedCell), item.item)
|
||||
}
|
||||
|
||||
public onBuildSelected: (item: BuildItem) => void = () => {
|
||||
this.eventBus.emit(new SendNukeIntentEvent(this.myPlayer, this.clickedCell, null))
|
||||
public onBuildSelected = (item: BuildItemDisplay) => {
|
||||
switch (item.item) {
|
||||
case BuildItems.Nuke:
|
||||
this.eventBus.emit(new SendNukeIntentEvent(this.myPlayer, this.clickedCell, null))
|
||||
break
|
||||
case BuildItems.Destroyer:
|
||||
this.eventBus.emit(new BuildUnitIntentEvent(UnitType.Destroyer, this.clickedCell))
|
||||
break
|
||||
case BuildItems.Port:
|
||||
this.eventBus.emit(new BuildUnitIntentEvent(UnitType.Port, this.clickedCell))
|
||||
break
|
||||
}
|
||||
this.hideMenu()
|
||||
};
|
||||
|
||||
@@ -160,11 +178,11 @@ export class BuildMenu extends LitElement {
|
||||
<button
|
||||
class="build-button"
|
||||
@click=${() => this.onBuildSelected(item)}
|
||||
?disabled=${!this.canAfford(item)}
|
||||
title=${!this.canAfford(item) ? 'Not enough money' : ''}
|
||||
?disabled=${!this.canBuild(item)}
|
||||
title=${!this.canBuild(item) ? 'Not enough money' : ''}
|
||||
>
|
||||
<img src=${item.icon} alt="${item.item.name}" width="40" height="40">
|
||||
<span class="build-name">${item.item.name}</span>
|
||||
<img src=${item.icon} alt="${item.item.type}" width="40" height="40">
|
||||
<span class="build-name">${item.item.type}</span>
|
||||
<span class="build-cost">
|
||||
${renderNumber(item.item.cost)}
|
||||
<img src=${goldCoinIcon} alt="gold" width="12" height="12" style="vertical-align: middle;">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventBus } from "../../../../core/EventBus";
|
||||
import { AllPlayers, Cell, Game, Player } from "../../../../core/game/Game";
|
||||
import { AllPlayers, Cell, Game, Player, UnitType } from "../../../../core/game/Game";
|
||||
import { ClientID } from "../../../../core/Schemas";
|
||||
import { and, bfs, dist, manhattanDist, manhattanDistWrapped, sourceDstOceanShore } from "../../../../core/Util";
|
||||
import { ContextMenuEvent, MouseUpEvent } from "../../../InputHandler";
|
||||
@@ -305,7 +305,7 @@ export class RadialMenu implements Layer {
|
||||
if (!tile.isLand()) {
|
||||
return
|
||||
}
|
||||
if (myPlayer.boats().length >= this.game.config().boatMaxNumber()) {
|
||||
if (myPlayer.units(UnitType.TransportShip).length >= this.game.config().boatMaxNumber()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
||||
import { Tile } from "./game/Game";
|
||||
import { manhattanDist } from "./Util";
|
||||
import { colord } from "colord";
|
||||
export class AStar {
|
||||
private fwdOpenSet: PriorityQueue<{ tile: Tile; fScore: number; }>;
|
||||
private bwdOpenSet: PriorityQueue<{ tile: Tile; fScore: number; }>;
|
||||
private fwdCameFrom: Map<Tile, Tile>;
|
||||
private bwdCameFrom: Map<Tile, Tile>;
|
||||
private fwdGScore: Map<Tile, number>;
|
||||
private bwdGScore: Map<Tile, number>;
|
||||
private meetingPoint: Tile | null;
|
||||
public completed: boolean;
|
||||
|
||||
constructor(private src: Tile, private dst: Tile) {
|
||||
this.fwdOpenSet = new PriorityQueue<{ tile: Tile; fScore: number; }>(
|
||||
(a, b) => a.fScore - b.fScore
|
||||
);
|
||||
this.bwdOpenSet = new PriorityQueue<{ tile: Tile; fScore: number; }>(
|
||||
(a, b) => a.fScore - b.fScore
|
||||
);
|
||||
this.fwdCameFrom = new Map<Tile, Tile>();
|
||||
this.bwdCameFrom = new Map<Tile, Tile>();
|
||||
this.fwdGScore = new Map<Tile, number>();
|
||||
this.bwdGScore = new Map<Tile, number>();
|
||||
this.meetingPoint = null;
|
||||
this.completed = false;
|
||||
|
||||
// Initialize forward search
|
||||
this.fwdGScore.set(src, 0);
|
||||
this.fwdOpenSet.enqueue({ tile: src, fScore: this.heuristic(src, dst) });
|
||||
|
||||
// Initialize backward search
|
||||
this.bwdGScore.set(dst, 0);
|
||||
this.bwdOpenSet.enqueue({ tile: dst, fScore: this.heuristic(dst, src) });
|
||||
}
|
||||
|
||||
compute(iterations: number): boolean {
|
||||
if (this.completed) return true;
|
||||
|
||||
while (!this.fwdOpenSet.isEmpty() && !this.bwdOpenSet.isEmpty()) {
|
||||
iterations--;
|
||||
if (iterations <= 0) return false;
|
||||
|
||||
// Process forward search
|
||||
const fwdCurrent = this.fwdOpenSet.dequeue()!.tile;
|
||||
if (this.bwdGScore.has(fwdCurrent)) {
|
||||
// We found a meeting point!
|
||||
this.meetingPoint = fwdCurrent;
|
||||
this.completed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
this.expandNode(fwdCurrent, true);
|
||||
|
||||
// Process backward search
|
||||
const bwdCurrent = this.bwdOpenSet.dequeue()!.tile;
|
||||
if (this.fwdGScore.has(bwdCurrent)) {
|
||||
// We found a meeting point!
|
||||
this.meetingPoint = bwdCurrent;
|
||||
this.completed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
this.expandNode(bwdCurrent, false);
|
||||
}
|
||||
|
||||
return this.completed;
|
||||
}
|
||||
|
||||
private expandNode(current: Tile, isForward: boolean) {
|
||||
for (const neighbor of current.neighborsWrapped()) {
|
||||
if (neighbor !== (isForward ? this.dst : this.src) && neighbor.isLand()) continue;
|
||||
|
||||
const gScore = isForward ? this.fwdGScore : this.bwdGScore;
|
||||
const openSet = isForward ? this.fwdOpenSet : this.bwdOpenSet;
|
||||
const cameFrom = isForward ? this.fwdCameFrom : this.bwdCameFrom;
|
||||
|
||||
let tentativeGScore = gScore.get(current)! + 1;
|
||||
if (neighbor.magnitude() < 10) {
|
||||
tentativeGScore += 1;
|
||||
}
|
||||
|
||||
if (!gScore.has(neighbor) || tentativeGScore < gScore.get(neighbor)!) {
|
||||
cameFrom.set(neighbor, current);
|
||||
gScore.set(neighbor, tentativeGScore);
|
||||
const fScore = tentativeGScore + this.heuristic(
|
||||
neighbor,
|
||||
isForward ? this.dst : this.src
|
||||
);
|
||||
openSet.enqueue({ tile: neighbor, fScore: fScore });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private heuristic(a: Tile, b: Tile): number {
|
||||
return 1.1 * Math.abs(a.cell().x - b.cell().x) + Math.abs(a.cell().y - b.cell().y);
|
||||
}
|
||||
|
||||
public reconstructPath(): Tile[] {
|
||||
if (!this.meetingPoint) return [];
|
||||
|
||||
// Reconstruct path from start to meeting point
|
||||
const fwdPath: Tile[] = [this.meetingPoint];
|
||||
let current = this.meetingPoint;
|
||||
while (this.fwdCameFrom.has(current)) {
|
||||
current = this.fwdCameFrom.get(current)!;
|
||||
fwdPath.unshift(current);
|
||||
}
|
||||
|
||||
// Reconstruct path from meeting point to goal
|
||||
current = this.meetingPoint;
|
||||
while (this.bwdCameFrom.has(current)) {
|
||||
current = this.bwdCameFrom.get(current)!;
|
||||
fwdPath.push(current);
|
||||
}
|
||||
|
||||
return fwdPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class PathFinder {
|
||||
|
||||
private curr: Tile = null
|
||||
private dst: Tile = null
|
||||
private path: Tile[]
|
||||
private aStar: AStar
|
||||
private inProgress = false
|
||||
|
||||
constructor(private iterations: number) {
|
||||
|
||||
}
|
||||
|
||||
nextTile(curr: Tile, dst: Tile): Tile {
|
||||
if (this.shouldRecompute(curr, dst)) {
|
||||
if (this.inProgress) {
|
||||
if (this.aStar.compute(this.iterations)) {
|
||||
this.path = this.aStar.reconstructPath()
|
||||
this.inProgress = false
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
this.curr = curr
|
||||
this.dst = dst
|
||||
this.path = null
|
||||
this.aStar = new AStar(curr, dst)
|
||||
if (this.aStar.compute(this.iterations)) {
|
||||
this.inProgress = false
|
||||
this.path = this.aStar.reconstructPath()
|
||||
} else {
|
||||
this.inProgress = true
|
||||
return null
|
||||
}
|
||||
if (this.path.length > 0) {
|
||||
this.path.shift()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return this.path.shift()
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRecompute(curr: Tile, dst: Tile) {
|
||||
if (this.path == null || this.curr == null || this.dst == null) {
|
||||
return true
|
||||
}
|
||||
const dist = manhattanDist(curr.cell(), dst.cell())
|
||||
let tolerance = 10
|
||||
if (dist > 50) {
|
||||
tolerance = 10
|
||||
} else if (dist > 25) {
|
||||
tolerance = 5
|
||||
} else if (dist > 10) {
|
||||
tolerance = 3
|
||||
} else {
|
||||
tolerance = 0
|
||||
}
|
||||
if (manhattanDist(this.dst.cell(), dst.cell()) > tolerance) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
+16
-5
@@ -1,5 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { Difficulty, GameMap, PlayerType } from './game/Game';
|
||||
import { Difficulty, GameMap, PlayerType, UnitType } from './game/Game';
|
||||
|
||||
export type GameID = string
|
||||
export type ClientID = string
|
||||
@@ -15,6 +15,7 @@ export type Intent = SpawnIntent
|
||||
| DonateIntent
|
||||
| NukeIntent
|
||||
| TargetTroopRatioIntent
|
||||
| BuildUnitIntent
|
||||
|
||||
export type AttackIntent = z.infer<typeof AttackIntentSchema>
|
||||
export type SpawnIntent = z.infer<typeof SpawnIntentSchema>
|
||||
@@ -26,7 +27,8 @@ export type TargetPlayerIntent = z.infer<typeof TargetPlayerIntentSchema>
|
||||
export type EmojiIntent = z.infer<typeof EmojiIntentSchema>
|
||||
export type DonateIntent = z.infer<typeof DonateIntentSchema>
|
||||
export type NukeIntent = z.infer<typeof NukeIntentSchema>
|
||||
export type TargetTroopRatioIntent = z.infer<typeof TargetTroopRatioSchema>
|
||||
export type TargetTroopRatioIntent = z.infer<typeof TargetTroopRatioIntentSchema>
|
||||
export type BuildUnitIntent = z.infer<typeof BuildUnitIntentSchema>
|
||||
|
||||
export type Turn = z.infer<typeof TurnSchema>
|
||||
export type GameConfig = z.infer<typeof GameConfigSchema>
|
||||
@@ -67,7 +69,7 @@ const EmojiSchema = z.string().refine(
|
||||
);
|
||||
// Zod schemas
|
||||
const BaseIntentSchema = z.object({
|
||||
type: z.enum(['attack', 'spawn', 'boat', 'name', 'targetPlayer', 'emoji', 'nuke', 'troop_ratio']),
|
||||
type: z.enum(['attack', 'spawn', 'boat', 'name', 'targetPlayer', 'emoji', 'nuke', 'troop_ratio', 'build_unit']),
|
||||
clientID: z.string(),
|
||||
});
|
||||
|
||||
@@ -153,12 +155,20 @@ export const NukeIntentSchema = BaseIntentSchema.extend({
|
||||
magnitude: z.number().nullable(),
|
||||
})
|
||||
|
||||
export const TargetTroopRatioSchema = BaseIntentSchema.extend({
|
||||
export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('troop_ratio'),
|
||||
player: z.string(),
|
||||
ratio: z.number().min(0).max(1),
|
||||
})
|
||||
|
||||
export const BuildUnitIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal('build_unit'),
|
||||
player: z.string(),
|
||||
unit: z.nativeEnum(UnitType),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
})
|
||||
|
||||
const IntentSchema = z.union([
|
||||
AttackIntentSchema,
|
||||
SpawnIntentSchema,
|
||||
@@ -170,7 +180,8 @@ const IntentSchema = z.union([
|
||||
EmojiIntentSchema,
|
||||
DonateIntentSchema,
|
||||
NukeIntentSchema,
|
||||
TargetTroopRatioSchema,
|
||||
TargetTroopRatioIntentSchema,
|
||||
BuildUnitIntentSchema,
|
||||
]);
|
||||
|
||||
const TurnSchema = z.object({
|
||||
|
||||
+17
-1
@@ -3,7 +3,8 @@ import twemoji from 'twemoji';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
|
||||
import { Cell, Game, Player, TerraNullius, Tile } from "./game/Game";
|
||||
import { Cell, Game, Player, TerraNullius, Tile, Unit } from "./game/Game";
|
||||
import { number } from 'zod';
|
||||
|
||||
export function manhattanDist(c1: Cell, c2: Cell): number {
|
||||
return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y);
|
||||
@@ -30,10 +31,25 @@ export function within(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
export function euclDist(root: Tile, dist: number): (tile: Tile) => boolean {
|
||||
return (n: Tile) => euclideanDist(root.cell(), n.cell()) <= dist;
|
||||
}
|
||||
|
||||
export function dist(root: Tile, dist: number): (tile: Tile) => boolean {
|
||||
return (n: Tile) => manhattanDist(root.cell(), n.cell()) <= dist;
|
||||
}
|
||||
|
||||
export function distSort(target: Tile): (a: Tile, b: Tile) => number {
|
||||
return (a: Tile, b: Tile) => {
|
||||
return manhattanDist(a.cell(), target.cell()) - manhattanDist(b.cell(), target.cell());
|
||||
}
|
||||
}
|
||||
|
||||
export function distSortUnit(target: Unit): (a: Unit, b: Unit) => number {
|
||||
return (a: Unit, b: Unit) => {
|
||||
return manhattanDist(a.tile().cell(), target.tile().cell()) - manhattanDist(b.tile().cell(), target.tile().cell());
|
||||
}
|
||||
}
|
||||
export function and(x: (tile: Tile) => boolean, y: (tile: Tile) => boolean): (tile: Tile) => boolean {
|
||||
return (tile: Tile) => x(tile) && y(tile)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export const devConfig = new class extends DefaultConfig {
|
||||
return 95
|
||||
}
|
||||
numSpawnPhaseTurns(): number {
|
||||
return 20
|
||||
return 80
|
||||
}
|
||||
gameCreationRate(): number {
|
||||
return 20 * 1000
|
||||
@@ -22,4 +22,7 @@ export const devConfig = new class extends DefaultConfig {
|
||||
return 400
|
||||
}
|
||||
|
||||
boatMaxDistance(): number {
|
||||
return 2000
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
||||
import { Boat, Cell, Execution, MutableBoat, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, Tile, TileEvent } from "../game/Game";
|
||||
import { and, bfs, manhattanDistWrapped, sourceDstOceanShore } from "../Util";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay";
|
||||
|
||||
export class BoatAttackExecution implements Execution {
|
||||
|
||||
private lastMove: number
|
||||
|
||||
// TODO: make this configurable
|
||||
private ticksPerMove = 1
|
||||
|
||||
private active = true
|
||||
|
||||
private mg: MutableGame
|
||||
private attacker: MutablePlayer
|
||||
private target: MutablePlayer | TerraNullius
|
||||
|
||||
// TODO make private
|
||||
public path: Tile[]
|
||||
private src: Tile | null
|
||||
private dst: Tile | null
|
||||
|
||||
private currTileIndex: number = 0
|
||||
|
||||
private boat: MutableBoat
|
||||
|
||||
private aStarPre: AStar
|
||||
private aStarComplete: AStar
|
||||
|
||||
private finalPath = false
|
||||
|
||||
constructor(
|
||||
private attackerID: PlayerID,
|
||||
private targetID: PlayerID | null,
|
||||
private cell: Cell,
|
||||
private troops: number | null,
|
||||
) { }
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
init(mg: MutableGame, ticks: number) {
|
||||
this.lastMove = ticks
|
||||
this.mg = mg
|
||||
|
||||
this.attacker = mg.player(this.attackerID)
|
||||
|
||||
if (this.attacker.boats().length >= mg.config().boatMaxNumber()) {
|
||||
mg.displayMessage(`No boats available, max ${mg.config().boatMaxNumber()}`, MessageType.WARN, this.attackerID)
|
||||
this.active = false
|
||||
this.attacker.addTroops(this.troops)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.targetID == null || this.targetID == this.mg.terraNullius().id()) {
|
||||
this.target = mg.terraNullius()
|
||||
} else {
|
||||
this.target = mg.player(this.targetID)
|
||||
}
|
||||
|
||||
if (this.troops == null) {
|
||||
this.troops = this.mg.config().boatAttackAmount(this.attacker, this.target)
|
||||
}
|
||||
|
||||
this.troops = Math.min(this.troops, this.attacker.troops())
|
||||
this.attacker.removeTroops(this.troops)
|
||||
|
||||
const [srcTile, dstTile]: [Tile | null, Tile | null] = sourceDstOceanShore(this.mg, this.attacker, this.target, this.cell);
|
||||
this.src = srcTile
|
||||
this.dst = dstTile
|
||||
|
||||
if (this.src == null || this.dst == null) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (manhattanDistWrapped(this.src.cell(), this.dst.cell(), mg.width()) > mg.config().boatMaxDistance()) {
|
||||
mg.displayMessage(`Cannot send boat: destination is too far away`, MessageType.WARN, this.attackerID)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
this.aStarPre = new AStar(this.src, this.dst)
|
||||
this.aStarPre.compute(5)
|
||||
this.path = this.aStarPre.reconstructPath()
|
||||
if (this.path != null) {
|
||||
this.boat = this.attacker.addBoat(this.troops, this.src, this.target)
|
||||
} else {
|
||||
console.log('got null path')
|
||||
this.active = false
|
||||
}
|
||||
this.aStarComplete = new AStar(this.path[this.path.length - 1], this.dst)
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.active) {
|
||||
return
|
||||
}
|
||||
if (!this.boat.isActive()) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (ticks - this.lastMove < this.ticksPerMove) {
|
||||
return
|
||||
}
|
||||
this.lastMove = ticks
|
||||
|
||||
if (!this.finalPath && this.aStarComplete.compute(30000)) {
|
||||
this.path.push(...this.aStarComplete.reconstructPath())
|
||||
this.finalPath = true
|
||||
}
|
||||
|
||||
if (this.currTileIndex >= this.path.length) {
|
||||
if (!this.finalPath) {
|
||||
return
|
||||
}
|
||||
if (this.dst.owner() == this.attacker) {
|
||||
this.attacker.addTroops(this.troops)
|
||||
this.boat.delete()
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (this.target.isPlayer() && this.attacker.isAlliedWith(this.target)) {
|
||||
this.target.addTroops(this.troops)
|
||||
} else {
|
||||
this.attacker.conquer(this.dst)
|
||||
this.mg.addExecution(new AttackExecution(this.troops, this.attacker.id(), this.targetID, this.dst.cell(), null, false))
|
||||
}
|
||||
this.boat.delete()
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
const nextTile = this.path[this.currTileIndex]
|
||||
this.boat.move(nextTile)
|
||||
this.currTileIndex++
|
||||
}
|
||||
|
||||
owner(): MutablePlayer {
|
||||
return this.attacker
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AStar {
|
||||
private openSet: PriorityQueue<{ tile: Tile, fScore: number }>;
|
||||
private cameFrom: Map<Tile, Tile>;
|
||||
private gScore: Map<Tile, number>;
|
||||
private current: Tile | null;
|
||||
public completed: boolean;
|
||||
|
||||
constructor(private src: Tile, private dst: Tile) {
|
||||
this.openSet = new PriorityQueue<{ tile: Tile, fScore: number }>(
|
||||
(a, b) => a.fScore - b.fScore
|
||||
);
|
||||
this.cameFrom = new Map<Tile, Tile>();
|
||||
this.gScore = new Map<Tile, number>();
|
||||
this.current = null;
|
||||
this.completed = false;
|
||||
|
||||
this.gScore.set(src, 0);
|
||||
this.openSet.enqueue({ tile: src, fScore: this.heuristic(src, dst) });
|
||||
}
|
||||
|
||||
compute(iterations: number): boolean {
|
||||
if (this.completed) return true;
|
||||
|
||||
while (!this.openSet.isEmpty()) {
|
||||
iterations--
|
||||
this.current = this.openSet.dequeue()!.tile;
|
||||
if (iterations <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.current === this.dst) {
|
||||
this.completed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const neighbor of this.current.neighborsWrapped()) {
|
||||
if (neighbor != this.dst && neighbor.isLand()) continue; // Skip non-water tiles
|
||||
|
||||
const tentativeGScore = this.gScore.get(this.current)! + 100 - neighbor.magnitude();
|
||||
|
||||
if (!this.gScore.has(neighbor) || tentativeGScore < this.gScore.get(neighbor)!) {
|
||||
this.cameFrom.set(neighbor, this.current);
|
||||
this.gScore.set(neighbor, tentativeGScore);
|
||||
const fScore = tentativeGScore + this.heuristic(neighbor, this.dst);
|
||||
|
||||
this.openSet.enqueue({ tile: neighbor, fScore: fScore });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.completed;
|
||||
}
|
||||
|
||||
private heuristic(a: Tile, b: Tile): number {
|
||||
// Manhattan distance
|
||||
return Math.abs(a.cell().x - b.cell().x) + Math.abs(a.cell().y - b.cell().y);
|
||||
}
|
||||
|
||||
public reconstructPath(): Tile[] {
|
||||
const path = [this.current!];
|
||||
while (this.cameFrom.has(this.current!)) {
|
||||
this.current = this.cameFrom.get(this.current!)!;
|
||||
path.unshift(this.current);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game";
|
||||
import { AStar, PathFinder } from "../PathFinding";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { distSort, distSortUnit, manhattanDist } from "../Util";
|
||||
|
||||
export class DestroyerExecution implements Execution {
|
||||
private random: PseudoRandom
|
||||
|
||||
private _owner: MutablePlayer
|
||||
private active = true
|
||||
private destroyer: MutableUnit = null
|
||||
private mg: MutableGame = null
|
||||
|
||||
private target: MutableUnit = null
|
||||
private pathfinder = new PathFinder(5000)
|
||||
|
||||
private patrolTile: Tile;
|
||||
private patrolCenterTile: Tile
|
||||
|
||||
// TODO: put in config
|
||||
private searchRange = 100
|
||||
|
||||
constructor(
|
||||
private playerID: PlayerID,
|
||||
private cell: Cell,
|
||||
) { }
|
||||
|
||||
|
||||
init(mg: MutableGame, ticks: number): void {
|
||||
this._owner = mg.player(this.playerID)
|
||||
this.mg = mg
|
||||
this.patrolCenterTile = mg.tile(this.cell)
|
||||
this.patrolTile = this.patrolCenterTile
|
||||
this.random = new PseudoRandom(mg.ticks())
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
// TODO: remove gold from player
|
||||
if (this.destroyer == null) {
|
||||
const spawns = this._owner.units(UnitType.Port).map(u => u.tile()).sort(distSort(this.patrolTile))
|
||||
if (spawns.length == 0) {
|
||||
console.warn(`no ports found for destoryer for player ${this._owner}`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.destroyer = this._owner.addUnit(UnitType.Destroyer, 0, spawns[0])
|
||||
return
|
||||
}
|
||||
if (!this.destroyer.isActive()) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (this.target != null && !this.target.isActive()) {
|
||||
this.target = null
|
||||
}
|
||||
if (this.target == null) {
|
||||
const ships = this.mg.units(UnitType.TransportShip)
|
||||
.filter(u => manhattanDist(u.tile().cell(), this.destroyer.tile().cell()) < 100)
|
||||
.filter(u => u.owner() != this.destroyer.owner())
|
||||
.filter(u => u != this.destroyer)
|
||||
.filter(u => !u.owner().isAlliedWith(this.destroyer.owner()))
|
||||
if (ships.length == 0) {
|
||||
if (manhattanDist(this.destroyer.tile().cell(), this.patrolTile.cell()) > 5) {
|
||||
const next = this.pathfinder.nextTile(this.destroyer.tile(), this.patrolTile)
|
||||
if (next == null) {
|
||||
this.target = null
|
||||
return
|
||||
}
|
||||
this.destroyer.move(next)
|
||||
} else {
|
||||
this.patrolTile = this.randomTile()
|
||||
}
|
||||
return
|
||||
}
|
||||
this.target = ships.sort(distSortUnit(this.destroyer))[0]
|
||||
}
|
||||
if (manhattanDist(this.destroyer.tile().cell(), this.target.tile().cell()) < 5) {
|
||||
this.target.delete()
|
||||
this.target = null
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < 1 + this.mg.ticks() % 2; i++) {
|
||||
const next = this.pathfinder.nextTile(this.destroyer.tile(), this.target.tile())
|
||||
if (next == null) {
|
||||
this.target = null
|
||||
console.warn(`target not found`)
|
||||
return
|
||||
}
|
||||
this.destroyer.move(next)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
owner(): MutablePlayer {
|
||||
return null
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
randomTile(): Tile {
|
||||
while (true) {
|
||||
const x = this.patrolCenterTile.cell().x + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2)
|
||||
const y = this.patrolCenterTile.cell().y + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2)
|
||||
const cell = new Cell(x, y)
|
||||
if (!this.mg.isOnMap(cell)) {
|
||||
continue
|
||||
}
|
||||
const tile = this.mg.tile(cell)
|
||||
if (!tile.isOcean()) {
|
||||
continue
|
||||
}
|
||||
return tile
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerInfo, TerraNullius, Tile, PlayerType, Alliance, AllianceRequestReplyEvent, Difficulty } from "../game/Game";
|
||||
import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerInfo, TerraNullius, Tile, PlayerType, Alliance, AllianceRequestReplyEvent, Difficulty, UnitType } from "../game/Game";
|
||||
import { AttackIntent, BoatAttackIntentSchema, GameID, Intent, Turn } from "../Schemas";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
import { SpawnExecution } from "./SpawnExecution";
|
||||
import { BotSpawner } from "./BotSpawner";
|
||||
import { BoatAttackExecution } from "./BoatAttackExecution";
|
||||
import { TransportShipExecution } from "./TransportShipExecution";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { FakeHumanExecution } from "./FakeHumanExecution";
|
||||
import Usernames from '../../../resources/Usernames.txt'
|
||||
@@ -16,6 +16,8 @@ import { EmojiExecution } from "./EmojiExecution";
|
||||
import { DonateExecution } from "./DonateExecution";
|
||||
import { NukeExecution } from "./NukeExecution";
|
||||
import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution";
|
||||
import { DestroyerExecution } from "./DestroyerExecution";
|
||||
import { PortExecution } from "./PortExecution";
|
||||
|
||||
|
||||
|
||||
@@ -36,50 +38,64 @@ export class Executor {
|
||||
}
|
||||
|
||||
createExec(intent: Intent): Execution {
|
||||
if (intent.type == "attack") {
|
||||
const source: Cell | null = intent.sourceX != null && intent.sourceY != null ? new Cell(intent.sourceX, intent.sourceY) : null
|
||||
const target: Cell | null = intent.targetX != null && intent.targetY != null ? new Cell(intent.targetX, intent.targetY) : null
|
||||
return new AttackExecution(
|
||||
intent.troops,
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
source,
|
||||
target,
|
||||
)
|
||||
} else if (intent.type == "spawn") {
|
||||
return new SpawnExecution(
|
||||
new PlayerInfo(sanitize(intent.name), intent.playerType, intent.clientID, intent.playerID),
|
||||
new Cell(intent.x, intent.y)
|
||||
)
|
||||
} else if (intent.type == "boat") {
|
||||
return new BoatAttackExecution(
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
new Cell(intent.x, intent.y),
|
||||
intent.troops
|
||||
)
|
||||
} else if (intent.type == "allianceRequest") {
|
||||
return new AllianceRequestExecution(intent.requestor, intent.recipient)
|
||||
} else if (intent.type == "allianceRequestReply") {
|
||||
return new AllianceRequestReplyExecution(intent.requestor, intent.recipient, intent.accept)
|
||||
} else if (intent.type == "breakAlliance") {
|
||||
return new BreakAllianceExecution(intent.requestor, intent.recipient)
|
||||
} else if (intent.type == "targetPlayer") {
|
||||
return new TargetPlayerExecution(intent.requestor, intent.target)
|
||||
} else if (intent.type == "emoji") {
|
||||
return new EmojiExecution(intent.sender, intent.recipient, intent.emoji)
|
||||
} else if (intent.type == "donate") {
|
||||
return new DonateExecution(intent.sender, intent.recipient, intent.troops)
|
||||
} else if (intent.type == "nuke") {
|
||||
return new NukeExecution(intent.sender, new Cell(intent.x, intent.y), intent.magnitude)
|
||||
} else if (intent.type == "troop_ratio") {
|
||||
return new SetTargetTroopRatioExecution(intent.player, intent.ratio)
|
||||
} else {
|
||||
throw new Error(`intent type ${intent} not found`)
|
||||
switch (intent.type) {
|
||||
case "attack": {
|
||||
const source: Cell | null = intent.sourceX != null && intent.sourceY != null
|
||||
? new Cell(intent.sourceX, intent.sourceY)
|
||||
: null;
|
||||
const target: Cell | null = intent.targetX != null && intent.targetY != null
|
||||
? new Cell(intent.targetX, intent.targetY)
|
||||
: null;
|
||||
return new AttackExecution(
|
||||
intent.troops,
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
source,
|
||||
target,
|
||||
);
|
||||
}
|
||||
case "spawn":
|
||||
return new SpawnExecution(
|
||||
new PlayerInfo(sanitize(intent.name), intent.playerType, intent.clientID, intent.playerID),
|
||||
new Cell(intent.x, intent.y)
|
||||
);
|
||||
case "boat":
|
||||
return new TransportShipExecution(
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
new Cell(intent.x, intent.y),
|
||||
intent.troops
|
||||
);
|
||||
case "allianceRequest":
|
||||
return new AllianceRequestExecution(intent.requestor, intent.recipient);
|
||||
case "allianceRequestReply":
|
||||
return new AllianceRequestReplyExecution(intent.requestor, intent.recipient, intent.accept);
|
||||
case "breakAlliance":
|
||||
return new BreakAllianceExecution(intent.requestor, intent.recipient);
|
||||
case "targetPlayer":
|
||||
return new TargetPlayerExecution(intent.requestor, intent.target);
|
||||
case "emoji":
|
||||
return new EmojiExecution(intent.sender, intent.recipient, intent.emoji);
|
||||
case "donate":
|
||||
return new DonateExecution(intent.sender, intent.recipient, intent.troops);
|
||||
case "nuke":
|
||||
return new NukeExecution(intent.sender, new Cell(intent.x, intent.y), intent.magnitude);
|
||||
case "troop_ratio":
|
||||
return new SetTargetTroopRatioExecution(intent.player, intent.ratio);
|
||||
case "build_unit":
|
||||
switch (intent.unit) {
|
||||
case UnitType.Destroyer:
|
||||
return new DestroyerExecution(intent.player, new Cell(intent.x, intent.y))
|
||||
case UnitType.Port:
|
||||
return new PortExecution(intent.player, new Cell(intent.x, intent.y))
|
||||
default:
|
||||
throw Error(`unit type ${intent.unit} not supported`)
|
||||
}
|
||||
default:
|
||||
throw new Error(`intent type ${intent} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
spawnBots(numBots: number): Execution[] {
|
||||
return new BotSpawner(this.gs, this.gameID).spawnBots(numBots).map(i => this.createExec(i))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Cell, Execution, MutableGame, MutablePlayer, Player, PlayerInfo, Player
|
||||
import { PseudoRandom } from "../PseudoRandom"
|
||||
import { and, bfs, dist, simpleHash } from "../Util";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
import { BoatAttackExecution } from "./BoatAttackExecution";
|
||||
import { TransportShipExecution } from "./TransportShipExecution";
|
||||
import { SpawnExecution } from "./SpawnExecution";
|
||||
|
||||
export class FakeHumanExecution implements Execution {
|
||||
@@ -194,7 +194,7 @@ export class FakeHumanExecution implements Execution {
|
||||
continue
|
||||
}
|
||||
|
||||
this.mg.addExecution(new BoatAttackExecution(
|
||||
this.mg.addExecution(new TransportShipExecution(
|
||||
this.player.id(),
|
||||
dst.hasOwner() ? dst.owner().id() : null,
|
||||
dst.cell(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cell, Execution, Items, MutableGame, MutablePlayer, PlayerID, Tile } from "../game/Game";
|
||||
import { Cell, Execution, BuildItems, MutableGame, MutablePlayer, PlayerID, Tile } from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { bfs, dist, euclideanDist, manhattanDist } from "../Util";
|
||||
|
||||
@@ -26,12 +26,12 @@ export class NukeExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.sender.gold() < Items.Nuke.cost) {
|
||||
if (this.sender.gold() < BuildItems.Nuke.cost) {
|
||||
console.warn(`player ${this.sender} insufficient gold for nuke`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.sender.removeGold(Items.Nuke.cost)
|
||||
this.sender.removeGold(BuildItems.Nuke.cost)
|
||||
|
||||
const rand = new PseudoRandom(this.mg.ticks())
|
||||
const tile = this.mg.tile(this.cell)
|
||||
@@ -48,7 +48,9 @@ export class NukeExecution implements Execution {
|
||||
mp.removeTroops(mp.troops() / mp.numTilesOwned())
|
||||
}
|
||||
}
|
||||
this.mg.boats().filter(b => euclideanDist(this.cell, b.tile().cell()) < this.magnitude + 50).forEach(b => b.delete())
|
||||
this.mg.units()
|
||||
.filter(b => euclideanDist(this.cell, b.tile().cell()) < this.magnitude + 50)
|
||||
.forEach(b => b.delete())
|
||||
this.active = false
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { BuildValidator } from "../game/BuildValidator";
|
||||
import { AllPlayers, BuildItem, BuildItems, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, Tile, Unit, UnitType } from "../game/Game";
|
||||
import { AStar, PathFinder } from "../PathFinding";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { bfs, dist, manhattanDist } from "../Util";
|
||||
import { TradeShipExecution } from "./TradeShipExecution";
|
||||
|
||||
export class PortExecution implements Execution {
|
||||
|
||||
private active = true
|
||||
private mg: MutableGame
|
||||
private player: MutablePlayer
|
||||
private port: MutableUnit
|
||||
private random: PseudoRandom
|
||||
private portPaths = new Map<MutableUnit, Tile[]>()
|
||||
private computingPaths = new Map<MutableUnit, AStar>()
|
||||
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
private cell: Cell
|
||||
) { }
|
||||
|
||||
|
||||
init(mg: MutableGame, ticks: number): void {
|
||||
this.mg = mg
|
||||
this.player = mg.player(this._owner)
|
||||
this.random = new PseudoRandom(mg.ticks())
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.port == null) {
|
||||
const tile = this.mg.tile(this.cell)
|
||||
if (!new BuildValidator(this.mg).canBuild(this.player, tile, BuildItems.Port)) {
|
||||
console.warn(`player ${this.player} cannot build port at ${this.cell}`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
const spawns = Array.from(bfs(tile, dist(tile, 20)))
|
||||
.filter(t => t.isOceanShore() && t.owner() == this.player)
|
||||
.sort((a, b) => manhattanDist(a.cell(), tile.cell()) - manhattanDist(b.cell(), tile.cell()))
|
||||
|
||||
if (spawns.length == 0) {
|
||||
console.warn(`cannot find spawn for port`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.port = this.player.addUnit(UnitType.Port, 0, spawns[0])
|
||||
}
|
||||
|
||||
|
||||
if (!this.port.tile().hasOwner()) {
|
||||
this.port.delete()
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (this.port.tile().owner() != this.port.owner()) {
|
||||
this.port.setOwner(this.port.tile().owner() as Player)
|
||||
}
|
||||
|
||||
const allPorts = this.mg.units(UnitType.Port)
|
||||
.filter(u => u.owner() != this.player)
|
||||
if (allPorts.length == 0) {
|
||||
return
|
||||
}
|
||||
for (const port of allPorts) {
|
||||
if (this.computingPaths.has(port)) {
|
||||
const aStar = this.computingPaths.get(port)
|
||||
if (aStar.compute(10_000)) {
|
||||
this.portPaths.set(port, aStar.reconstructPath())
|
||||
this.computingPaths.delete(port)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!this.portPaths.has(port)) {
|
||||
this.computingPaths.set(port, new AStar(this.port.tile(), port.tile()))
|
||||
continue
|
||||
}
|
||||
}
|
||||
for (const port of this.portPaths.keys()) {
|
||||
if (!port.isActive()) {
|
||||
this.portPaths.delete(port)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.random.chance(50)) {
|
||||
const port = this.random.randElement(
|
||||
Array.from(this.portPaths.keys())
|
||||
.filter(p => this.port.owner().isAlliedWith(p.owner()))
|
||||
)
|
||||
const path = this.portPaths.get(port)
|
||||
this.mg.addExecution(new TradeShipExecution(this._owner, this.port, port, path))
|
||||
}
|
||||
}
|
||||
|
||||
owner(): MutablePlayer {
|
||||
return null
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { BuildValidator } from "../game/BuildValidator";
|
||||
import { AllPlayers, BuildItem, BuildItems, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, Tile, Unit, UnitType } from "../game/Game";
|
||||
import { AStar, PathFinder } from "../PathFinding";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { bfs, dist, manhattanDist } from "../Util";
|
||||
|
||||
export class TradeShipExecution implements Execution {
|
||||
|
||||
private active = true
|
||||
private mg: MutableGame
|
||||
private player: MutablePlayer
|
||||
private tradeShip: MutableUnit
|
||||
private index = 0
|
||||
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
private srcPort: MutableUnit,
|
||||
private dstPort: MutableUnit,
|
||||
// don't modify
|
||||
private path: Tile[]
|
||||
) { }
|
||||
|
||||
|
||||
init(mg: MutableGame, ticks: number): void {
|
||||
this.mg = mg
|
||||
this.player = mg.player(this._owner)
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.tradeShip == null) {
|
||||
this.tradeShip = this.player.addUnit(UnitType.TradeShip, 0, this.srcPort.tile())
|
||||
}
|
||||
if (this.index >= this.path.length) {
|
||||
this.active = false
|
||||
this.tradeShip.delete()
|
||||
this.srcPort.owner().addGold(10_000)
|
||||
this.dstPort.owner().addGold(10_000)
|
||||
return
|
||||
}
|
||||
this.tradeShip.move(this.path[this.index])
|
||||
this.index++
|
||||
}
|
||||
|
||||
owner(): MutablePlayer {
|
||||
return null
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Unit, Cell, Execution, MutableUnit, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, Tile, TileEvent, UnitType } from "../game/Game";
|
||||
import { and, bfs, manhattanDistWrapped, sourceDstOceanShore } from "../Util";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay";
|
||||
import { AStar, PathFinder } from "../PathFinding";
|
||||
|
||||
export class TransportShipExecution implements Execution {
|
||||
|
||||
private lastMove: number
|
||||
|
||||
// TODO: make this configurable
|
||||
private ticksPerMove = 1
|
||||
|
||||
private active = true
|
||||
|
||||
private mg: MutableGame
|
||||
private attacker: MutablePlayer
|
||||
private target: MutablePlayer | TerraNullius
|
||||
|
||||
// TODO make private
|
||||
public path: Tile[]
|
||||
private src: Tile | null
|
||||
private dst: Tile | null
|
||||
|
||||
|
||||
private boat: MutableUnit
|
||||
|
||||
private pathFinder: PathFinder = new PathFinder(10_000)
|
||||
|
||||
constructor(
|
||||
private attackerID: PlayerID,
|
||||
private targetID: PlayerID | null,
|
||||
private cell: Cell,
|
||||
private troops: number | null,
|
||||
) { }
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
init(mg: MutableGame, ticks: number) {
|
||||
this.lastMove = ticks
|
||||
this.mg = mg
|
||||
|
||||
this.attacker = mg.player(this.attackerID)
|
||||
|
||||
if (this.attacker.units(UnitType.TransportShip).length >= mg.config().boatMaxNumber()) {
|
||||
mg.displayMessage(`No boats available, max ${mg.config().boatMaxNumber()}`, MessageType.WARN, this.attackerID)
|
||||
this.active = false
|
||||
this.attacker.addTroops(this.troops)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.targetID == null || this.targetID == this.mg.terraNullius().id()) {
|
||||
this.target = mg.terraNullius()
|
||||
} else {
|
||||
this.target = mg.player(this.targetID)
|
||||
}
|
||||
|
||||
if (this.troops == null) {
|
||||
this.troops = this.mg.config().boatAttackAmount(this.attacker, this.target)
|
||||
}
|
||||
|
||||
this.troops = Math.min(this.troops, this.attacker.troops())
|
||||
this.attacker.removeTroops(this.troops)
|
||||
|
||||
const [srcTile, dstTile]: [Tile | null, Tile | null] = sourceDstOceanShore(this.mg, this.attacker, this.target, this.cell);
|
||||
this.src = srcTile
|
||||
this.dst = dstTile
|
||||
|
||||
if (this.src == null || this.dst == null) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (manhattanDistWrapped(this.src.cell(), this.dst.cell(), mg.width()) > mg.config().boatMaxDistance()) {
|
||||
mg.displayMessage(`Cannot send boat: destination is too far away`, MessageType.WARN, this.attackerID)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
this.boat = this.attacker.addUnit(UnitType.TransportShip, this.troops, this.src)
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
if (!this.active) {
|
||||
return
|
||||
}
|
||||
if (!this.boat.isActive()) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (ticks - this.lastMove < this.ticksPerMove) {
|
||||
return
|
||||
}
|
||||
this.lastMove = ticks
|
||||
|
||||
|
||||
if (this.boat.tile() == this.dst) {
|
||||
if (this.dst.owner() == this.attacker) {
|
||||
this.attacker.addTroops(this.troops)
|
||||
this.boat.delete()
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (this.target.isPlayer() && this.attacker.isAlliedWith(this.target)) {
|
||||
this.target.addTroops(this.troops)
|
||||
} else {
|
||||
this.attacker.conquer(this.dst)
|
||||
this.mg.addExecution(
|
||||
new AttackExecution(this.troops, this.attacker.id(), this.targetID, this.dst.cell(), null, false)
|
||||
)
|
||||
}
|
||||
this.boat.delete()
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
|
||||
const nextTile = this.pathFinder.nextTile(this.boat.tile(), this.dst)
|
||||
if (nextTile == null) {
|
||||
console.warn('boat computing')
|
||||
return
|
||||
}
|
||||
this.boat.move(nextTile)
|
||||
}
|
||||
|
||||
owner(): MutablePlayer {
|
||||
return this.attacker
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { bfs, dist, manhattanDist } from "../Util";
|
||||
import { BuildItem, BuildItems, Game, Player, Tile, UnitType } from "./Game";
|
||||
|
||||
export class BuildValidator {
|
||||
constructor(private game: Game) { }
|
||||
|
||||
canBuild(player: Player, tile: Tile, item: BuildItem): boolean {
|
||||
if (!player.isAlive() || player.gold() < item.cost) {
|
||||
return false
|
||||
}
|
||||
switch (item) {
|
||||
case BuildItems.Nuke:
|
||||
return true
|
||||
case BuildItems.Port:
|
||||
return this.canBuildPort(player, tile)
|
||||
case BuildItems.Destroyer:
|
||||
return this.canBuildDestroyer(player, tile)
|
||||
default:
|
||||
throw Error(`item ${item.type} not supported`)
|
||||
}
|
||||
}
|
||||
|
||||
canBuildPort(player: Player, tile: Tile): boolean {
|
||||
return Array.from(bfs(tile, dist(tile, 20)))
|
||||
.filter(t => t.owner() == player && t.isOceanShore()).length > 0
|
||||
|
||||
}
|
||||
|
||||
canBuildDestroyer(player: Player, tile: Tile): boolean {
|
||||
return player.units(UnitType.Port)
|
||||
.filter(u => manhattanDist(u.tile().cell(), tile.cell()) < this.game.config().boatMaxDistance()).length > 0
|
||||
}
|
||||
}
|
||||
+31
-19
@@ -1,10 +1,7 @@
|
||||
import { info } from "console"
|
||||
import { Config } from "../configuration/Config"
|
||||
import { GameEvent } from "../EventBus"
|
||||
import { ClientID, GameID } from "../Schemas"
|
||||
import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay"
|
||||
import { BreakAllianceExecution } from "../execution/alliance/BreakAllianceExecution"
|
||||
import { DonateExecution } from "../execution/DonateExecution"
|
||||
import { MessageType } from "../../client/graphics/layers/EventsDisplay"
|
||||
|
||||
export type PlayerID = string
|
||||
export type Tick = number
|
||||
@@ -25,12 +22,26 @@ export enum GameMap {
|
||||
Mena
|
||||
}
|
||||
|
||||
export class Item {
|
||||
constructor(public readonly name: string, public readonly cost: Gold) { }
|
||||
export enum UnitType {
|
||||
TransportShip = "Transport",
|
||||
Destroyer = "Destroyer",
|
||||
Port = "Port",
|
||||
Nuke = "Nuke",
|
||||
TradeShip = "Trade Ship",
|
||||
}
|
||||
|
||||
export const Items = {
|
||||
Nuke: new Item("Nuke", 1_000_000),
|
||||
export class BuildItem {
|
||||
constructor(
|
||||
public readonly type: UnitType,
|
||||
public readonly cost: Gold
|
||||
) { }
|
||||
}
|
||||
|
||||
export const BuildItems = {
|
||||
// Nuke: new BuildItem(UnitType.Nuke, 1_000_000),
|
||||
Nuke: new BuildItem(UnitType.Nuke, 10),
|
||||
Destroyer: new BuildItem(UnitType.Destroyer, 10),
|
||||
Port: new BuildItem(UnitType.Port, 0)
|
||||
} as const;
|
||||
|
||||
export class Nation {
|
||||
@@ -145,20 +156,20 @@ export interface Tile {
|
||||
onShore(): boolean
|
||||
}
|
||||
|
||||
export interface Boat {
|
||||
export interface Unit {
|
||||
type(): UnitType
|
||||
troops(): number
|
||||
tile(): Tile
|
||||
owner(): Player
|
||||
target(): Player | TerraNullius
|
||||
isActive(): boolean
|
||||
}
|
||||
|
||||
export interface MutableBoat extends Boat {
|
||||
export interface MutableUnit extends Unit {
|
||||
move(tile: Tile): void
|
||||
owner(): MutablePlayer
|
||||
target(): MutablePlayer | TerraNullius
|
||||
setTroops(troops: number): void
|
||||
delete(): void
|
||||
setOwner(newOwner: Player): void
|
||||
}
|
||||
|
||||
export interface TerraNullius {
|
||||
@@ -175,7 +186,7 @@ export interface Player {
|
||||
clientID(): ClientID
|
||||
id(): PlayerID
|
||||
type(): PlayerType
|
||||
boats(): Boat[]
|
||||
units(...types: UnitType[]): Unit[]
|
||||
ownsTile(cell: Cell): boolean
|
||||
isAlive(): boolean
|
||||
borderTiles(): ReadonlySet<Tile>
|
||||
@@ -216,14 +227,13 @@ export interface MutablePlayer extends Player {
|
||||
relinquish(tile: Tile): void
|
||||
executions(): Execution[]
|
||||
neighbors(): (MutablePlayer | TerraNullius)[]
|
||||
boats(): MutableBoat[]
|
||||
units(...types: UnitType[]): MutableUnit[]
|
||||
incomingAllianceRequests(): MutableAllianceRequest[]
|
||||
outgoingAllianceRequests(): MutableAllianceRequest[]
|
||||
alliances(): MutableAlliance[]
|
||||
allianceWith(other: Player): MutableAlliance | null
|
||||
breakAlliance(alliance: Alliance): void
|
||||
createAllianceRequest(recipient: Player): MutableAllianceRequest
|
||||
addBoat(troops: number, tile: Tile, target: Player | TerraNullius): MutableBoat
|
||||
target(other: Player): void
|
||||
targets(): MutablePlayer[]
|
||||
transitiveTargets(): MutablePlayer[]
|
||||
@@ -239,6 +249,8 @@ export interface MutablePlayer extends Player {
|
||||
setTroops(troops: number): void
|
||||
addTroops(troops: number): void
|
||||
removeTroops(troops: number): number
|
||||
|
||||
addUnit(type: UnitType, troops: number, tile: Tile): MutableUnit
|
||||
}
|
||||
|
||||
export interface Game {
|
||||
@@ -263,7 +275,7 @@ export interface Game {
|
||||
nations(): Nation[]
|
||||
config(): Config
|
||||
displayMessage(message: string, type: MessageType, playerID: PlayerID | null): void
|
||||
boats(): Boat[]
|
||||
units(...types: UnitType[]): Unit[]
|
||||
}
|
||||
|
||||
export interface MutableGame extends Game {
|
||||
@@ -272,7 +284,7 @@ export interface MutableGame extends Game {
|
||||
players(): MutablePlayer[]
|
||||
addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer
|
||||
executions(): Execution[]
|
||||
boats(): MutableBoat[]
|
||||
units(...types: UnitType[]): MutableUnit[]
|
||||
}
|
||||
|
||||
export class TileEvent implements GameEvent {
|
||||
@@ -283,8 +295,8 @@ export class PlayerEvent implements GameEvent {
|
||||
constructor(public readonly player: Player) { }
|
||||
}
|
||||
|
||||
export class BoatEvent implements GameEvent {
|
||||
constructor(public readonly boat: Boat, public oldTile: Tile) { }
|
||||
export class UnitEvent implements GameEvent {
|
||||
constructor(public readonly unit: Unit, public oldTile: Tile) { }
|
||||
}
|
||||
|
||||
export class AllianceRequestEvent implements GameEvent {
|
||||
|
||||
+17
-17
@@ -1,16 +1,16 @@
|
||||
import {info} from "console";
|
||||
import {Config} from "../configuration/Config";
|
||||
import {EventBus} from "../EventBus";
|
||||
import {Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Boat, BoatEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation} from "./Game";
|
||||
import {TerrainMap} from "./TerrainMapLoader";
|
||||
import {PlayerImpl} from "./PlayerImpl";
|
||||
import {TerraNulliusImpl} from "./TerraNulliusImpl";
|
||||
import {TileImpl} from "./TileImpl";
|
||||
import {AllianceRequestImpl} from "./AllianceRequestImpl";
|
||||
import {AllianceImpl} from "./AllianceImpl";
|
||||
import {ClientID} from "../Schemas";
|
||||
import {DisplayMessageEvent, MessageType} from "../../client/graphics/layers/EventsDisplay";
|
||||
import {BoatImpl} from "./BoatImpl";
|
||||
import { info } from "console";
|
||||
import { Config } from "../configuration/Config";
|
||||
import { EventBus } from "../EventBus";
|
||||
import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, UnitEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation, UnitType } from "./Game";
|
||||
import { TerrainMap } from "./TerrainMapLoader";
|
||||
import { PlayerImpl } from "./PlayerImpl";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
import { TileImpl } from "./TileImpl";
|
||||
import { AllianceRequestImpl } from "./AllianceRequestImpl";
|
||||
import { AllianceImpl } from "./AllianceImpl";
|
||||
import { ClientID } from "../Schemas";
|
||||
import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay";
|
||||
import { UnitImpl } from "./UnitImpl";
|
||||
|
||||
export function createGame(terrainMap: TerrainMap, eventBus: EventBus, config: Config): Game {
|
||||
return new GameImpl(terrainMap, eventBus, config)
|
||||
@@ -58,8 +58,8 @@ export class GameImpl implements MutableGame {
|
||||
n.strength
|
||||
))
|
||||
}
|
||||
boats(): BoatImpl[] {
|
||||
return Array.from(this._players.values()).flatMap(p => p._boats)
|
||||
units(...types: UnitType[]): UnitImpl[] {
|
||||
return Array.from(this._players.values()).flatMap(p => p.units(...types))
|
||||
}
|
||||
nations(): Nation[] {
|
||||
return this.nations_
|
||||
@@ -354,8 +354,8 @@ export class GameImpl implements MutableGame {
|
||||
return false
|
||||
}
|
||||
|
||||
public fireBoatUpdateEvent(boat: Boat, oldTile: Tile) {
|
||||
this.eventBus.emit(new BoatEvent(boat, oldTile))
|
||||
public fireUnitUpdateEvent(boat: Unit, oldTile: Tile) {
|
||||
this.eventBus.emit(new UnitEvent(boat, oldTile))
|
||||
}
|
||||
|
||||
public breakAlliance(breaker: Player, alliance: Alliance) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, TargetPlayerEvent, EmojiMessage, EmojiMessageEvent, AllPlayers, Gold } from "./Game";
|
||||
import { MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, TargetPlayerEvent, EmojiMessage, EmojiMessageEvent, AllPlayers, Gold, UnitType } from "./Game";
|
||||
import { ClientID } from "../Schemas";
|
||||
import { processName, simpleHash } from "../Util";
|
||||
import { CellString, GameImpl } from "./GameImpl";
|
||||
import { BoatImpl } from "./BoatImpl";
|
||||
import { UnitImpl } from "./UnitImpl";
|
||||
import { TileImpl } from "./TileImpl";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
import { MessageType } from "../../client/graphics/layers/EventsDisplay";
|
||||
@@ -29,7 +29,7 @@ export class PlayerImpl implements MutablePlayer {
|
||||
|
||||
public _borderTiles: Set<Tile> = new Set();
|
||||
|
||||
public _boats: BoatImpl[] = [];
|
||||
public _units: UnitImpl[] = [];
|
||||
public _tiles: Map<CellString, Tile> = new Map<CellString, Tile>();
|
||||
|
||||
private _name: string;
|
||||
@@ -73,15 +73,16 @@ export class PlayerImpl implements MutablePlayer {
|
||||
}
|
||||
|
||||
|
||||
addBoat(troops: number, tile: Tile, target: Player | TerraNullius): BoatImpl {
|
||||
const b = new BoatImpl(this.gs, tile, troops, this, target as PlayerImpl | TerraNulliusImpl);
|
||||
this._boats.push(b);
|
||||
this.gs.fireBoatUpdateEvent(b, b.tile());
|
||||
addUnit(type: UnitType, troops: number, tile: Tile): UnitImpl {
|
||||
const b = new UnitImpl(type, this.gs, tile, troops, this);
|
||||
this._units.push(b);
|
||||
this.gs.fireUnitUpdateEvent(b, b.tile());
|
||||
return b;
|
||||
}
|
||||
|
||||
boats(): BoatImpl[] {
|
||||
return this._boats;
|
||||
units(...types: UnitType[]): UnitImpl[] {
|
||||
const ts = new Set(types)
|
||||
return this._units.filter(u => ts.has(u.type()));
|
||||
}
|
||||
|
||||
sharesBorderWith(other: Player | TerraNullius): boolean {
|
||||
|
||||
+10
-14
@@ -1,8 +1,8 @@
|
||||
import {Tile, Cell, TerrainType, Player, TerraNullius, MutablePlayer} from "./Game";
|
||||
import {Terrain} from "./TerrainMapLoader";
|
||||
import {GameImpl} from "./GameImpl";
|
||||
import {PlayerImpl} from "./PlayerImpl";
|
||||
import {TerraNulliusImpl} from "./TerraNulliusImpl";
|
||||
import { Tile, Cell, TerrainType, Player, TerraNullius, MutablePlayer } from "./Game";
|
||||
import { Terrain } from "./TerrainMapLoader";
|
||||
import { GameImpl } from "./GameImpl";
|
||||
import { PlayerImpl } from "./PlayerImpl";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
|
||||
|
||||
export class TileImpl implements Tile {
|
||||
@@ -25,15 +25,11 @@ export class TileImpl implements Tile {
|
||||
// Check top neighbor
|
||||
if (y > 0) {
|
||||
ns.push(this.gs.map[x][y - 1]);
|
||||
} else {
|
||||
ns.push(this.gs.map[x][this.gs.height() - 1]);
|
||||
}
|
||||
|
||||
// Check bottom neighbor
|
||||
if (y < this.gs.height() - 1) {
|
||||
ns.push(this.gs.map[x][y + 1]);
|
||||
} else {
|
||||
ns.push(this.gs.map[x][0]);
|
||||
}
|
||||
|
||||
// Check left neighbor (wrap around)
|
||||
@@ -94,11 +90,11 @@ export class TileImpl implements Tile {
|
||||
.length > 0;
|
||||
}
|
||||
|
||||
hasOwner(): boolean {return this._owner != this.gs._terraNullius;}
|
||||
owner(): MutablePlayer | TerraNullius {return this._owner;}
|
||||
isBorder(): boolean {return this._isBorder;}
|
||||
isInterior(): boolean {return this.hasOwner() && !this.isBorder();}
|
||||
cell(): Cell {return this._cell;}
|
||||
hasOwner(): boolean { return this._owner != this.gs._terraNullius; }
|
||||
owner(): MutablePlayer | TerraNullius { return this._owner; }
|
||||
isBorder(): boolean { return this._isBorder; }
|
||||
isInterior(): boolean { return this.hasOwner() && !this.isBorder(); }
|
||||
cell(): Cell { return this._cell; }
|
||||
|
||||
neighbors(): Tile[] {
|
||||
if (this._neighbors == null) {
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import {MutableBoat, Tile, TerraNullius} from "./Game";
|
||||
import {GameImpl} from "./GameImpl";
|
||||
import {PlayerImpl} from "./PlayerImpl";
|
||||
import {TerraNulliusImpl} from "./TerraNulliusImpl";
|
||||
import { MutableUnit, Tile, TerraNullius, UnitType, Player } from "./Game";
|
||||
import { GameImpl } from "./GameImpl";
|
||||
import { PlayerImpl } from "./PlayerImpl";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
|
||||
|
||||
export class BoatImpl implements MutableBoat {
|
||||
export class UnitImpl implements MutableUnit {
|
||||
private _active = true;
|
||||
|
||||
constructor(
|
||||
private _type: UnitType,
|
||||
private g: GameImpl,
|
||||
private _tile: Tile,
|
||||
private _troops: number,
|
||||
private _owner: PlayerImpl,
|
||||
private _target: PlayerImpl | TerraNulliusImpl
|
||||
) { }
|
||||
|
||||
type(): UnitType {
|
||||
return this._type
|
||||
}
|
||||
|
||||
move(tile: Tile): void {
|
||||
const oldTile = this._tile;
|
||||
this._tile = tile;
|
||||
this.g.fireBoatUpdateEvent(this, oldTile);
|
||||
this.g.fireUnitUpdateEvent(this, oldTile);
|
||||
}
|
||||
setTroops(troops: number): void {
|
||||
this._troops = troops;
|
||||
@@ -32,13 +36,16 @@ export class BoatImpl implements MutableBoat {
|
||||
owner(): PlayerImpl {
|
||||
return this._owner;
|
||||
}
|
||||
target(): PlayerImpl | TerraNullius {
|
||||
return this._target;
|
||||
|
||||
setOwner(newOwner: Player): void {
|
||||
this._owner = newOwner as PlayerImpl
|
||||
this.g.fireUnitUpdateEvent(this, this.tile())
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this._owner._boats = this._owner._boats.filter(b => b != this);
|
||||
this._owner._units = this._owner._units.filter(b => b != this);
|
||||
this._active = false;
|
||||
this.g.fireBoatUpdateEvent(this, this._tile);
|
||||
this.g.fireUnitUpdateEvent(this, this._tile);
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this._active;
|
||||
Reference in New Issue
Block a user