mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 12:35:03 +00:00
Merge branch 'main' into sounds
This commit is contained in:
+8
-1
@@ -41,7 +41,7 @@ export default [
|
||||
// Disable rules that would fail. The failures should be fixed, and the entries here removed.
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-expressions": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"no-unused-vars": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -50,6 +50,13 @@ export default [
|
||||
"@typescript-eslint/prefer-nullish-coalescing": "error",
|
||||
eqeqeq: "error",
|
||||
"no-case-declarations": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
args: "none",
|
||||
caughtErrors: "none",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export default {
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 21.5,
|
||||
branches: 17.0,
|
||||
branches: 16.5,
|
||||
lines: 22.0,
|
||||
functions: 20.5,
|
||||
},
|
||||
|
||||
Generated
+10
-10
@@ -3604,13 +3604,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz",
|
||||
"integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==",
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
|
||||
"integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.15.0",
|
||||
"@eslint/core": "^0.15.2",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3618,9 +3618,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz",
|
||||
"integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==",
|
||||
"version": "0.15.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
|
||||
"integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -18916,9 +18916,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
|
||||
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 100 100"
|
||||
id="svg10"
|
||||
sodipodi:docname="TraitorIcon.svg"
|
||||
width="100"
|
||||
height="100"
|
||||
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="defs14" />
|
||||
<sodipodi:namedview
|
||||
id="namedview12"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
height="100px"
|
||||
inkscape:zoom="6.984"
|
||||
inkscape:cx="49.971363"
|
||||
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="svg10" />
|
||||
<g
|
||||
transform="translate(0,-952.36218)"
|
||||
id="g4">
|
||||
<path
|
||||
style="color:#000000;text-indent:0;text-transform:none;direction:ltr;baseline-shift:baseline;enable-background:accumulate"
|
||||
d="m 49.682164,957.35229 c -0.08371,0.007 -0.167129,0.0173 -0.249965,0.0312 l -35.995035,7.00077 C 12.072501,964.6449 10.989932,965.96376 11,967.35339 v 32.00351 c 0.0012,0.13594 0.01165,0.27179 0.03125,0.4063 1.29114,9.0104 3.853487,15.5969 8.498827,22.2837 0.990121,1.4292 3.318267,1.6525 4.561871,0.4375 4.250005,-4.1812 7.885056,-7.78 11.967099,-11.9697 0.44046,-0.4164 0.750052,-0.9694 0.87488,-1.5626 0.628358,-3.8527 1.729676,-9.31167 2.530901,-13.59583 h 15.529108 c 1.243283,0.002 2.441795,-0.85453 2.843357,-2.03147 l 9.998621,-29.00319 c 0.567687,-1.6172 -0.568544,-3.6061 -2.249689,-3.93794 l -14.997932,-3.00033 c -0.298144,-0.0562 -0.604826,-0.0667 -0.906125,-0.0312 z m 23.902953,5.00055 c -1.078435,0.15321 -2.042235,0.94196 -2.405918,1.96897 L 58.899643,998.35679 H 44.995311 c -1.406702,-0.005 -2.733797,1.11295 -2.968341,2.50031 l -1.81225,10.9699 -14.27928,13.3139 c -1.128859,1.0576 -1.217322,3.0338 -0.187474,4.188 6.269465,7.139 13.562725,12.4576 22.778108,17.627 0.892103,0.5009 2.044992,0.5009 2.937095,0 19.535202,-10.9581 34.280717,-24.7622 37.494828,-47.1927 0.06997,-10.76177 0.03124,-21.61653 0.03124,-32.40981 0.0049,-1.40704 -1.112648,-2.73446 -2.499655,-2.96908 l -11.99834,-2.00021 c -0.298144,-0.0562 -0.604826,-0.0667 -0.906125,-0.0312 z m -23.621742,1.06261 11.060975,2.18774 -8.186371,23.75262 H 36.996414 c -1.378861,0.004 -2.67845,1.08304 -2.937095,2.43777 l -2.812112,15.06422 c -2.889686,2.9098 -5.919847,5.9695 -8.686302,8.7197 -2.891489,-4.8594 -4.568269,-9.6725 -5.561733,-16.50187 v -29.25322 z m 25.996415,5.28184 7.03028,1.18763 v 29.19071 c -2.868962,19.61817 -15.058331,31.46397 -32.995449,41.78587 -7.128175,-4.1138 -12.790202,-8.2954 -17.716306,-13.4077 l 12.779487,-11.9076 c 0.479911,-0.4525 0.804009,-1.0673 0.906125,-1.719 l 1.59353,-9.4698 h 13.435647 c 1.225447,-0.01 2.403546,-0.8446 2.812112,-2.0002 z"
|
||||
fill="#FFFFFF"
|
||||
fill-opacity="1"
|
||||
fill-rule="nonzero"
|
||||
stroke="none"
|
||||
marker="none"
|
||||
visibility="visible"
|
||||
display="inline"
|
||||
overflow="visible"
|
||||
id="path2" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:#ff2d2d;fill-opacity:0;stroke-width:0.143184"
|
||||
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
|
||||
id="path944" />
|
||||
<path
|
||||
style="fill:#ff2d2d;fill-opacity:0;stroke-width:0.143184"
|
||||
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
|
||||
id="path983" />
|
||||
<path
|
||||
style="fill:#ff2d2d;fill-opacity:0;stroke-width:0.143184"
|
||||
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
|
||||
id="path1022" />
|
||||
<path
|
||||
style="fill:#ff2d2d;fill-opacity:0;stroke-width:0.143184"
|
||||
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
|
||||
id="path1061" />
|
||||
<path
|
||||
style="fill:#ff2d2d;fill-opacity:0;stroke-width:0.143184"
|
||||
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
|
||||
id="path1100" />
|
||||
<path
|
||||
style="fill:#ff2d2d;fill-opacity:0;stroke-width:0.143184"
|
||||
d="M 22.451679,62.831985 C 22.040716,62.318926 20.614744,59.414324 19.904415,57.643392 18.969118,55.311589 18.276544,53.003918 17.644871,50.114548 L 17.190978,48.038373 17.14702,32.753436 c -0.02418,-8.406715 0.006,-15.283838 0.06717,-15.282495 0.06112,0.0013 7.450905,-1.423883 16.421746,-3.16717 l 16.310618,-3.169613 5.310229,1.037928 c 2.920626,0.570861 5.383063,1.077918 5.472081,1.126793 0.108366,0.0595 -1.175715,3.968788 -3.885785,11.829987 l -4.047636,11.741123 -8.356483,0.07159 c -6.695958,0.05736 -8.427615,0.110521 -8.714445,0.267501 -0.60381,0.330461 -1.419391,1.193902 -1.576025,1.66851 -0.08176,0.247726 -0.783479,3.851808 -1.559383,8.00907 l -1.410734,7.55866 -4.287731,4.287731 c -3.320136,3.320136 -4.321858,4.245124 -4.438961,4.098932 z"
|
||||
id="path1139" />
|
||||
<path
|
||||
style="fill:#FFFFFF;fill-opacity:1;stroke-width:0.143184"
|
||||
d="m 40.914948,36.977471 c 2.185353,-0.02223 5.761384,-0.02223 7.946736,0 2.185352,0.02223 0.397337,0.04042 -3.973368,0.04042 -4.370704,0 -6.15872,-0.01819 -3.973368,-0.04042 z"
|
||||
id="path1178" />
|
||||
<path
|
||||
style="fill:#ff5a5a;fill-opacity:1;stroke-width:0.143184"
|
||||
d="M 21.229102,70.741061 C 20.081432,70.373981 19.32618,69.461642 17.270525,65.959129 14.47136,61.189795 12.54351,55.744636 11.459279,49.545428 L 11.096793,47.472871 V 30.96265 c 0,-13.527886 0.03488,-16.593449 0.193078,-16.970972 0.258937,-0.617909 0.895032,-1.354431 1.391463,-1.611145 0.222726,-0.115176 8.704097,-1.825312 18.847492,-3.8003039 l 18.442537,-3.5908935 7.910402,1.5791153 c 4.350721,0.8685133 8.143647,1.6897975 8.428725,1.8250757 0.961973,0.4564861 1.758687,1.9751714 1.568376,2.9896164 -0.047,0.250541 -2.368442,7.075757 -5.158757,15.167146 -3.44537,9.990924 -5.193245,14.868872 -5.447096,15.201688 -0.205587,0.269539 -0.643902,0.634451 -0.974034,0.810914 -0.596667,0.318933 -0.648799,0.32127 -8.759053,0.392435 l -8.158815,0.07159 -0.470445,2.434135 c -0.258746,1.338774 -0.824052,4.367125 -1.256239,6.729668 -0.432185,2.362543 -0.88633,4.5664 -1.009211,4.897461 -0.177968,0.479472 -1.505767,1.882854 -6.526761,6.898286 -3.466837,3.462998 -6.517904,6.398177 -6.78015,6.52262 -0.601484,0.285423 -1.599068,0.395138 -2.109203,0.231972 z m 5.734389,-11.793889 4.338592,-4.340341 1.438261,-7.722946 c 1.617094,-8.683222 1.602011,-8.634211 2.883301,-9.369051 l 0.745198,-0.427381 8.253581,-0.0013 c 6.783328,-0.0011 8.266754,-0.03567 8.327529,-0.194046 0.04067,-0.105988 1.891336,-5.461638 4.11259,-11.901446 2.549671,-7.391954 3.982955,-11.742017 3.8876,-11.798993 -0.08307,-0.04964 -2.590856,-0.568451 -5.572852,-1.15292 l -5.42181,-1.062727 -16.494063,3.206249 -16.494064,3.206248 v 14.900444 c 0,14.430688 0.0093,14.952195 0.295638,16.541989 0.677724,3.763097 1.813851,7.543992 3.102693,10.32539 0.832524,1.796635 2.062365,4.131162 2.176318,4.131162 0.04559,0 2.035263,-1.953154 4.421488,-4.340342 z"
|
||||
id="path9953" />
|
||||
<path
|
||||
style="fill:#ff5a5a;fill-opacity:1;stroke-width:0.143184"
|
||||
d="M 49.327033,94.804719 C 48.714427,94.60513 47.768416,94.086861 45.174685,92.529868 37.917796,88.173624 32.73106,84.14232 27.795635,79.022291 c -1.145161,-1.187994 -2.213316,-2.431436 -2.374239,-2.763859 -0.397355,-0.820826 -0.385516,-1.87334 0.02963,-2.634656 0.211883,-0.388557 2.674852,-2.777493 7.195017,-6.978744 3.780069,-3.513371 7.046744,-6.559837 7.25928,-6.769926 0.367228,-0.363001 0.433179,-0.666405 1.327443,-6.106826 1.040786,-6.331825 1.066013,-6.412508 2.250084,-7.196324 l 0.596675,-0.39498 7.446948,-0.07159 7.446947,-0.07159 6.097509,-16.895762 c 3.353629,-9.292669 6.184278,-17.104473 6.290327,-17.359565 0.271361,-0.652727 1.177566,-1.423686 1.905252,-1.620901 0.55181,-0.149551 1.267997,-0.05876 6.811404,0.863504 3.407116,0.566845 6.423919,1.093902 6.704006,1.171238 0.637026,0.175891 1.521753,0.963097 1.870312,1.664151 0.253992,0.510853 0.264529,1.240952 0.247407,17.14297 -0.01766,16.399934 -0.02182,16.631061 -0.329684,18.327606 -2.58954,14.26993 -9.981308,25.824745 -22.991493,35.940307 -3.466755,2.695441 -7.896512,5.630746 -12.608706,8.354951 -2.064486,1.193517 -2.838015,1.444605 -3.642726,1.182428 z M 52.47709,87.119888 C 59.271254,83.059324 64.365846,79.189404 68.951193,74.605944 76.565106,66.995162 80.844811,58.899716 82.755326,48.494191 l 0.363231,-1.978318 V 32.007709 17.499544 l -0.322165,-0.07398 c -0.957493,-0.219871 -6.829216,-1.157603 -6.884521,-1.099478 -0.03586,0.03769 -2.785127,7.60719 -6.109478,16.821108 -3.324352,9.213917 -6.177636,16.993316 -6.340633,17.287551 -0.353145,0.637482 -1.164147,1.232386 -1.918855,1.407559 -0.300488,0.06975 -3.5908,0.128346 -7.311805,0.130225 -3.721005,0.0019 -6.765464,0.04073 -6.765464,0.08633 0,0.474978 -1.637454,9.59135 -1.804511,10.046442 -0.187845,0.511725 -1.260378,1.573508 -6.797799,6.729668 -3.61612,3.367143 -6.574871,6.194451 -6.575002,6.282908 -3e-4,0.204934 2.512831,2.628316 4.451717,4.292733 1.895466,1.627144 5.431341,4.277542 7.645781,5.731075 2.084726,1.368391 5.558468,3.470987 5.657133,3.424166 0.03938,-0.01869 1.134737,-0.669367 2.434135,-1.44596 z"
|
||||
id="path10066" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
+38
-5
@@ -249,6 +249,7 @@
|
||||
},
|
||||
"game_starting_modal": {
|
||||
"title": "Game is Starting...",
|
||||
"code_license": "Code licensed under AGPL-3.0",
|
||||
"desc": "Preparing for the lobby to start. Please wait."
|
||||
},
|
||||
"difficulty": {
|
||||
@@ -496,6 +497,7 @@
|
||||
"other_won": "{player} has won!",
|
||||
"exit": "Exit Game",
|
||||
"keep": "Keep Playing",
|
||||
"spectate": "Spectate",
|
||||
"wishlist": "Wishlist on Steam!"
|
||||
},
|
||||
"leaderboard": {
|
||||
@@ -566,6 +568,11 @@
|
||||
"upgrade": "Upgrade",
|
||||
"level": "Level"
|
||||
},
|
||||
"player_type": {
|
||||
"player": "Player",
|
||||
"nation": "Nation",
|
||||
"bot": "Bot"
|
||||
},
|
||||
"relation": {
|
||||
"hostile": "Hostile",
|
||||
"distrustful": "Distrustful",
|
||||
@@ -581,23 +588,37 @@
|
||||
"player_panel": {
|
||||
"gold": "Gold",
|
||||
"troops": "Troops",
|
||||
"betrayals": "Number of betrayals",
|
||||
"betrayals": "Betrayals",
|
||||
"traitor": "Traitor",
|
||||
"stable": "Stable",
|
||||
"trust": "Trust",
|
||||
"trading": "Trading",
|
||||
"active": "Active",
|
||||
"stopped": "Stopped",
|
||||
"alliance_time_remaining": "Alliance Expires In",
|
||||
"embargo": "Stopped trading with you",
|
||||
"nuke": "Nukes sent by them to you",
|
||||
"start_trade": "Start trading",
|
||||
"stop_trade": "Stop trading",
|
||||
"start_trade": "Start Trading",
|
||||
"stop_trade": "Stop Trading",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"none": "None",
|
||||
"alliances": "Alliances",
|
||||
"flag": "Flag"
|
||||
"flag": "Flag",
|
||||
"chat": "Chat",
|
||||
"target": "Target",
|
||||
"break": "Break",
|
||||
"break_alliance": "Break Alliance",
|
||||
"alliance": "Alliance",
|
||||
"send_alliance": "Send Alliance",
|
||||
"send_troops": "Send Troops",
|
||||
"send_gold": "Send Gold",
|
||||
"emotes": "Emojis"
|
||||
},
|
||||
"replay_panel": {
|
||||
"replay_speed": "Replay speed",
|
||||
"game_speed": "Game speed",
|
||||
"fastest_game_speed": "max"
|
||||
"fastest_game_speed": "Max"
|
||||
},
|
||||
"error_modal": {
|
||||
"crashed": "Game crashed!",
|
||||
@@ -729,5 +750,17 @@
|
||||
"map": "Map",
|
||||
"difficulty": "Difficulty",
|
||||
"type": "Type"
|
||||
},
|
||||
"player_stats_tree": {
|
||||
"public": "Public",
|
||||
"private": "Private",
|
||||
"singleplayer": "Single Player",
|
||||
"mode": "Mode",
|
||||
"stats_wins": "Wins",
|
||||
"stats_losses": "Losses",
|
||||
"stats_wlr": "Win:Loss Ratio",
|
||||
"stats_games_played": "Games Played",
|
||||
"mode_ffa": "Free-for-All",
|
||||
"mode_team": "Team"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,13 +84,20 @@ export class GameStartingModal extends LitElement {
|
||||
.modal button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.copyright {
|
||||
font-size: 32px;
|
||||
margin-top: 20px;
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="modal ${this.isVisible ? "visible" : ""}">
|
||||
<h2>${translateText("game_starting_modal.title")}</h2>
|
||||
<p>${translateText("game_starting_modal.desc")}</p>
|
||||
<div class="copyright">© OpenFront</div>
|
||||
<h5>${translateText("game_starting_modal.code_license")}</h5>
|
||||
<p>${translateText("game_starting_modal.title")}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ export class DifficultyDisplay extends LitElement {
|
||||
></path>
|
||||
</svg>`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const kingSkull = html`<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("player-stats-grid")
|
||||
export class PlayerStatsGrid extends LitElement {
|
||||
static styles = css`
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
.stat {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.stat-title {
|
||||
color: #bbb;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ type: Array }) titles: string[] = [];
|
||||
@property({ type: Array }) values: Array<string | number> = [];
|
||||
|
||||
// Currently fixed to display 4 stats (can be changed if needed)
|
||||
private readonly VISIBLE_STATS_COUNT = 4;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="grid mb-2">
|
||||
${Array(this.VISIBLE_STATS_COUNT)
|
||||
.fill(0)
|
||||
.map(
|
||||
(_, i) => html`
|
||||
<div class="stat">
|
||||
<div class="stat-value">${this.values[i] ?? ""}</div>
|
||||
<div class="stat-title">${this.titles[i] ?? ""}</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { PlayerStatsLeaf, PlayerStatsTree } from "../../../../core/ApiSchemas";
|
||||
import {
|
||||
Difficulty,
|
||||
GameMode,
|
||||
GameType,
|
||||
isDifficulty,
|
||||
isGameMode,
|
||||
isGameType,
|
||||
} from "../../../../core/game/Game";
|
||||
import { PlayerStats } from "../../../../core/StatsSchemas";
|
||||
import { renderNumber, translateText } from "../../../Utils";
|
||||
import "./PlayerStatsGrid";
|
||||
import "./PlayerStatsTable";
|
||||
|
||||
@customElement("player-stats-tree-view")
|
||||
export class PlayerStatsTreeView extends LitElement {
|
||||
@property({ type: Object }) statsTree?: PlayerStatsTree;
|
||||
@state() selectedType: GameType = GameType.Public;
|
||||
@state() selectedMode: GameMode = GameMode.FFA;
|
||||
@state() selectedDifficulty: Difficulty = Difficulty.Medium;
|
||||
|
||||
private get availableTypes(): GameType[] {
|
||||
if (!this.statsTree) return [];
|
||||
return Object.keys(this.statsTree).filter(isGameType);
|
||||
}
|
||||
|
||||
private get availableModes(): GameMode[] {
|
||||
const typeNode = this.statsTree?.[this.selectedType];
|
||||
if (!typeNode) return [];
|
||||
return Object.keys(typeNode).filter(isGameMode);
|
||||
}
|
||||
|
||||
private get availableDifficulties(): Difficulty[] {
|
||||
const typeNode = this.statsTree?.[this.selectedType];
|
||||
const modeNode = typeNode?.[this.selectedMode];
|
||||
if (!modeNode) return [];
|
||||
return Object.keys(modeNode).filter(isDifficulty);
|
||||
}
|
||||
|
||||
private labelForMode(m: GameMode) {
|
||||
return m === GameMode.FFA
|
||||
? translateText("player_stats_tree.mode_ffa")
|
||||
: translateText("player_stats_tree.mode_team");
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private getSelectedLeaf(): PlayerStatsLeaf | null {
|
||||
const typeNode = this.statsTree?.[this.selectedType];
|
||||
if (!typeNode) return null;
|
||||
const modeNode = typeNode[this.selectedMode];
|
||||
if (!modeNode) return null;
|
||||
const diffNode = modeNode[this.selectedDifficulty];
|
||||
if (!diffNode) return null;
|
||||
return diffNode;
|
||||
}
|
||||
|
||||
private getDisplayedStats(): PlayerStats | null {
|
||||
const leaf = this.getSelectedLeaf();
|
||||
if (!leaf || !leaf.stats) return null;
|
||||
return leaf.stats;
|
||||
}
|
||||
|
||||
private setGameType(t: GameType) {
|
||||
if (this.selectedType === t) return;
|
||||
this.selectedType = t;
|
||||
const modes = this.availableModes;
|
||||
if (!modes.includes(this.selectedMode)) {
|
||||
this.selectedMode = modes[0] ?? this.selectedMode;
|
||||
}
|
||||
const diffs = this.availableDifficulties;
|
||||
if (!diffs.includes(this.selectedDifficulty)) {
|
||||
this.selectedDifficulty = diffs[0] ?? this.selectedDifficulty;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private setMode(m: GameMode) {
|
||||
if (this.selectedMode === m) return;
|
||||
this.selectedMode = m;
|
||||
const diffs = this.availableDifficulties;
|
||||
if (!diffs.includes(this.selectedDifficulty)) {
|
||||
this.selectedDifficulty = diffs[0] ?? this.selectedDifficulty;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private setDifficulty(d: Difficulty) {
|
||||
if (this.selectedDifficulty === d) return;
|
||||
this.selectedDifficulty = d;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
const types = this.availableTypes;
|
||||
if (types.length && !types.includes(this.selectedType)) {
|
||||
this.selectedType = types[0];
|
||||
}
|
||||
const modes = this.availableModes;
|
||||
if (modes.length && !modes.includes(this.selectedMode)) {
|
||||
this.selectedMode = modes[0];
|
||||
}
|
||||
const diffs = this.availableDifficulties;
|
||||
if (diffs.length && !diffs.includes(this.selectedDifficulty)) {
|
||||
this.selectedDifficulty = diffs[0];
|
||||
}
|
||||
|
||||
const leaf = this.getSelectedLeaf();
|
||||
const wlr = leaf
|
||||
? leaf.losses === 0n
|
||||
? Number(leaf.wins)
|
||||
: Number(leaf.wins) / Number(leaf.losses)
|
||||
: 0;
|
||||
|
||||
return html`
|
||||
<!-- Type selector -->
|
||||
<div class="flex gap-2 mt-2 justify-center">
|
||||
${types.map(
|
||||
(t) => html`
|
||||
<button
|
||||
class="text-xs px-2 py-0.5 rounded border ${this.selectedType ===
|
||||
t
|
||||
? "border-white/60 text-white"
|
||||
: "border-white/20 text-gray-300"}"
|
||||
@click=${() => this.setGameType(t)}
|
||||
>
|
||||
${t === GameType.Public
|
||||
? translateText("player_stats_tree.public")
|
||||
: t === GameType.Private
|
||||
? translateText("player_stats_tree.private")
|
||||
: translateText("player_stats_tree.singleplayer")}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
<!-- Mode selector -->
|
||||
${modes.length
|
||||
? html`<div class="flex gap-2 mt-2 justify-center">
|
||||
${modes.map(
|
||||
(m) => html`
|
||||
<button
|
||||
class="text-xs px-2 py-0.5 rounded border ${this
|
||||
.selectedMode === m
|
||||
? "border-white/60 text-white"
|
||||
: "border-white/20 text-gray-300"}"
|
||||
@click=${() => this.setMode(m)}
|
||||
title=${translateText("player_stats_tree.mode")}
|
||||
>
|
||||
${this.labelForMode(m)}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>`
|
||||
: html``}
|
||||
<!-- Difficulty selector -->
|
||||
${diffs.length
|
||||
? html`<div class="flex gap-2 mt-2 justify-center">
|
||||
${diffs.map(
|
||||
(d) =>
|
||||
html` <button
|
||||
class="text-xs px-2 py-0.5 rounded border ${this
|
||||
.selectedDifficulty === d
|
||||
? "border-white/60 text-white"
|
||||
: "border-white/20 text-gray-300"}"
|
||||
@click=${() => this.setDifficulty(d)}
|
||||
title=${translateText("difficulty.difficulty")}
|
||||
>
|
||||
${translateText(`difficulty.${d}`)}
|
||||
</button>`,
|
||||
)}
|
||||
</div>`
|
||||
: html``}
|
||||
${leaf
|
||||
? html`
|
||||
<hr class="w-2/3 border-gray-600 my-2" />
|
||||
<player-stats-grid
|
||||
.titles=${[
|
||||
translateText("player_stats_tree.stats_wins"),
|
||||
translateText("player_stats_tree.stats_losses"),
|
||||
translateText("player_stats_tree.stats_wlr"),
|
||||
translateText("player_stats_tree.stats_games_played"),
|
||||
]}
|
||||
.values=${[
|
||||
renderNumber(leaf.wins),
|
||||
renderNumber(leaf.losses),
|
||||
wlr.toFixed(2),
|
||||
renderNumber(leaf.total),
|
||||
]}
|
||||
></player-stats-grid>
|
||||
<hr class="w-2/3 border-gray-600 my-2" />
|
||||
<player-stats-table
|
||||
.stats=${this.getDisplayedStats()}
|
||||
></player-stats-table>
|
||||
`
|
||||
: html``}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
export type ButtonVariant =
|
||||
| "normal"
|
||||
| "red"
|
||||
| "green"
|
||||
| "indigo"
|
||||
| "yellow"
|
||||
| "sky";
|
||||
export interface ActionButtonProps {
|
||||
onClick: (e: MouseEvent) => void;
|
||||
type?: ButtonVariant;
|
||||
icon: string;
|
||||
iconAlt: string;
|
||||
title: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ICON_SIZE =
|
||||
"h-5 w-5 shrink-0 transition-transform group-hover:scale-110 text-zinc-400";
|
||||
const TEXT_SIZE =
|
||||
"text-base sm:text-[14px] leading-5 font-semibold tracking-tight";
|
||||
|
||||
const getButtonStyles = () => {
|
||||
const btnBase =
|
||||
"group w-full min-w-[50px] select-none flex flex-col items-center justify-center " +
|
||||
"gap-1 rounded-lg py-1.5 border border-white/10 bg-white/[0.04] shadow-sm " +
|
||||
"transition-all duration-150 " +
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20 " +
|
||||
"active:translate-y-[1px]";
|
||||
|
||||
return {
|
||||
normal: `${btnBase} text-white/90 hover:bg-white/10 hover:text-white`,
|
||||
red: `${btnBase} text-red-400 hover:bg-red-500/10 hover:text-red-300 focus-visible:ring-red-400/30`,
|
||||
green: `${btnBase} text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300 focus-visible:ring-emerald-400/30`,
|
||||
yellow: `${btnBase} text-[#f59e0b] hover:bg-[#f59e0b]/10 hover:text-[#fbbf24] focus-visible:ring-[#f59e0b]/30`,
|
||||
indigo: `${btnBase} text-indigo-400 hover:bg-indigo-500/10 hover:text-indigo-300 focus-visible:ring-indigo-400/30`,
|
||||
sky: `${btnBase} text-[#38bdf8] hover:bg-[#38bdf8]/10 hover:text-[#0ea5e9] focus-visible:ring-[#38bdf8]/30`,
|
||||
};
|
||||
};
|
||||
|
||||
export const actionButton = (props: ActionButtonProps): TemplateResult => {
|
||||
const {
|
||||
onClick,
|
||||
type = "normal",
|
||||
icon,
|
||||
iconAlt,
|
||||
title,
|
||||
label,
|
||||
disabled = false,
|
||||
} = props;
|
||||
const buttonStyles = getButtonStyles();
|
||||
const buttonClass = buttonStyles[type];
|
||||
|
||||
return html`
|
||||
<button
|
||||
@click=${onClick}
|
||||
class="${buttonClass}"
|
||||
title="${title}"
|
||||
type="button"
|
||||
aria-label="${title}"
|
||||
?disabled=${disabled}
|
||||
>
|
||||
<img src=${icon} alt=${iconAlt} aria-hidden="true" class="${ICON_SIZE}" />
|
||||
<span class="${TEXT_SIZE}">${label}</span>
|
||||
</button>
|
||||
`;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
export type DividerSpacing = "sm" | "md" | "lg";
|
||||
@customElement("ui-divider")
|
||||
export class Divider extends LitElement {
|
||||
@property({ type: String })
|
||||
spacing: DividerSpacing = "md";
|
||||
|
||||
@property({ type: String })
|
||||
color: string = "bg-zinc-700/80";
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
const spacingClasses: Record<DividerSpacing, string> = {
|
||||
sm: "my-0.5",
|
||||
md: "my-1",
|
||||
lg: "my-2",
|
||||
} as const;
|
||||
const spacing = spacingClasses[this.spacing] ?? spacingClasses.md;
|
||||
|
||||
const colorClass = this.color || "bg-zinc-700/80";
|
||||
|
||||
return html`<div
|
||||
role="separator"
|
||||
aria-hidden="true"
|
||||
class="${spacing} h-px ${colorClass}"
|
||||
></div>`;
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,6 @@ export class SpriteFx implements Fx {
|
||||
|
||||
if (!this.animatedSprite.isActive() && !this.waitToTheEnd) return false;
|
||||
|
||||
const t = this.elapsedTime / this.duration;
|
||||
this.animatedSprite.update(frameTime);
|
||||
this.animatedSprite.draw(ctx, this.x, this.y);
|
||||
return true;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { EventBus } from "../../../core/EventBus";
|
||||
import { AllPlayers } from "../../../core/game/Game";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { TerraNulliusImpl } from "../../../core/game/TerraNulliusImpl";
|
||||
import { emojiTable, flattenedEmojiTable } from "../../../core/Util";
|
||||
import { Emoji, emojiTable, flattenedEmojiTable } from "../../../core/Util";
|
||||
import { CloseViewEvent, ShowEmojiMenuEvent } from "../../InputHandler";
|
||||
import { SendEmojiIntentEvent } from "../../Transport";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
@@ -42,7 +42,7 @@ export class EmojiTable extends LitElement {
|
||||
eventBus.emit(
|
||||
new SendEmojiIntentEvent(
|
||||
recipient,
|
||||
flattenedEmojiTable.indexOf(emoji),
|
||||
flattenedEmojiTable.indexOf(emoji as Emoji),
|
||||
),
|
||||
);
|
||||
this.hideTable();
|
||||
|
||||
@@ -268,13 +268,13 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
let playerType = "";
|
||||
switch (player.type()) {
|
||||
case PlayerType.Bot:
|
||||
playerType = translateText("player_info_overlay.bot");
|
||||
playerType = translateText("player_type.bot");
|
||||
break;
|
||||
case PlayerType.FakeHuman:
|
||||
playerType = translateText("player_info_overlay.nation");
|
||||
playerType = translateText("player_type.nation");
|
||||
break;
|
||||
case PlayerType.Human:
|
||||
playerType = translateText("player_info_overlay.player");
|
||||
playerType = translateText("player_type.player");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ import { Config } from "../../../core/configuration/Config";
|
||||
import { AllPlayers, PlayerActions, UnitType } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { flattenedEmojiTable } from "../../../core/Util";
|
||||
import { Emoji, flattenedEmojiTable } from "../../../core/Util";
|
||||
import { renderNumber, translateText } from "../../Utils";
|
||||
import { BuildItemDisplay, BuildMenu, flattenedBuildTable } from "./BuildMenu";
|
||||
import { ChatIntegration } from "./ChatIntegration";
|
||||
@@ -106,6 +106,7 @@ export enum Slot {
|
||||
Delete = "delete",
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const infoChatElement: MenuElement = {
|
||||
id: "info_chat",
|
||||
name: "chat",
|
||||
@@ -123,6 +124,7 @@ const infoChatElement: MenuElement = {
|
||||
})),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const allyTargetElement: MenuElement = {
|
||||
id: "ally_target",
|
||||
name: "target",
|
||||
@@ -138,6 +140,7 @@ const allyTargetElement: MenuElement = {
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const allyTradeElement: MenuElement = {
|
||||
id: "ally_trade",
|
||||
name: "trade",
|
||||
@@ -153,6 +156,7 @@ const allyTradeElement: MenuElement = {
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const allyEmbargoElement: MenuElement = {
|
||||
id: "ally_embargo",
|
||||
name: "embargo",
|
||||
@@ -204,6 +208,7 @@ const allyBreakElement: MenuElement = {
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const allyDonateGoldElement: MenuElement = {
|
||||
id: "ally_donate_gold",
|
||||
name: "donate gold",
|
||||
@@ -217,6 +222,7 @@ const allyDonateGoldElement: MenuElement = {
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const allyDonateTroopsElement: MenuElement = {
|
||||
id: "ally_donate_troops",
|
||||
name: "donate troops",
|
||||
@@ -230,6 +236,7 @@ const allyDonateTroopsElement: MenuElement = {
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const infoPlayerElement: MenuElement = {
|
||||
id: "info_player",
|
||||
name: "player",
|
||||
@@ -241,6 +248,7 @@ const infoPlayerElement: MenuElement = {
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const infoEmojiElement: MenuElement = {
|
||||
id: "info_emoji",
|
||||
name: "emoji",
|
||||
@@ -263,7 +271,7 @@ const infoEmojiElement: MenuElement = {
|
||||
: params.selected;
|
||||
params.playerActionHandler.handleEmoji(
|
||||
targetPlayer!,
|
||||
flattenedEmojiTable.indexOf(emoji),
|
||||
flattenedEmojiTable.indexOf(emoji as Emoji),
|
||||
);
|
||||
params.emojiTable.hideTable();
|
||||
});
|
||||
|
||||
@@ -88,6 +88,7 @@ export class RailroadLayer implements Layer {
|
||||
this.canvas.width = this.game.width() * 2;
|
||||
this.canvas.height = this.game.height() * 2;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const [_, rail] of this.existingRailroads) {
|
||||
this.paintRail(rail.tile);
|
||||
}
|
||||
@@ -111,7 +112,9 @@ export class RailroadLayer implements Layer {
|
||||
|
||||
private handleRailroadRendering(railUpdate: RailroadUpdate) {
|
||||
for (const railRoad of railUpdate.railTiles) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const x = this.game.x(railRoad.tile);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const y = this.game.y(railRoad.tile);
|
||||
if (railUpdate.isActive) {
|
||||
this.paintRailroad(railRoad);
|
||||
|
||||
@@ -246,6 +246,7 @@ export class StructureLayer implements Layer {
|
||||
) {
|
||||
let color = unit.owner().borderColor();
|
||||
if (unit.type() === UnitType.Construction) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
color = underConstructionColor;
|
||||
}
|
||||
|
||||
|
||||
@@ -447,12 +447,14 @@ export class TerritoryLayer implements Layer {
|
||||
return;
|
||||
}
|
||||
const owner = this.game.owner(tile) as PlayerView;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const isHighlighted =
|
||||
this.highlightedTerritory &&
|
||||
this.highlightedTerritory.id() === owner.id();
|
||||
const myPlayer = this.game.myPlayer();
|
||||
|
||||
if (this.game.isBorder(tile)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const playerIsFocused = owner && this.game.focusedPlayer() === owner;
|
||||
if (myPlayer) {
|
||||
const alternativeColor = this.alternateViewColor(owner);
|
||||
|
||||
@@ -28,6 +28,9 @@ export class WinModal extends LitElement implements Layer {
|
||||
@state()
|
||||
showButtons = false;
|
||||
|
||||
@state()
|
||||
private isWin = false;
|
||||
|
||||
@state()
|
||||
private patternContent: TemplateResult | null = null;
|
||||
|
||||
@@ -68,7 +71,9 @@ export class WinModal extends LitElement implements Layer {
|
||||
@click=${this.hide}
|
||||
class="flex-1 px-3 py-3 text-base cursor-pointer bg-blue-500/60 text-white border-0 rounded transition-all duration-200 hover:bg-blue-500/80 hover:-translate-y-px active:translate-y-px"
|
||||
>
|
||||
${translateText("win_modal.keep")}
|
||||
${this.isWin
|
||||
? translateText("win_modal.keep")
|
||||
: translateText("win_modal.spectate")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,10 +235,12 @@ export class WinModal extends LitElement implements Layer {
|
||||
this.eventBus.emit(new SendWinnerEvent(wu.winner, wu.allPlayersStats));
|
||||
if (wu.winner[1] === this.game.myPlayer()?.team()) {
|
||||
this._title = translateText("win_modal.your_team");
|
||||
this.isWin = true;
|
||||
} else {
|
||||
this._title = translateText("win_modal.other_team", {
|
||||
team: wu.winner[1],
|
||||
});
|
||||
this.isWin = false;
|
||||
}
|
||||
this.show();
|
||||
} else {
|
||||
@@ -250,10 +257,12 @@ export class WinModal extends LitElement implements Layer {
|
||||
winnerClient === this.game.myPlayer()?.clientID()
|
||||
) {
|
||||
this._title = translateText("win_modal.you_won");
|
||||
this.isWin = true;
|
||||
} else {
|
||||
this._title = translateText("win_modal.other_won", {
|
||||
player: winner.name(),
|
||||
});
|
||||
this.isWin = false;
|
||||
}
|
||||
this.show();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
GameMapType,
|
||||
GameMode,
|
||||
GameType,
|
||||
PlayerType,
|
||||
Quads,
|
||||
Trios,
|
||||
UnitType,
|
||||
@@ -118,7 +117,6 @@ export type PlayerCosmeticRefs = z.infer<typeof PlayerCosmeticRefsSchema>;
|
||||
export type PlayerPattern = z.infer<typeof PlayerPatternSchema>;
|
||||
export type Flag = z.infer<typeof FlagSchema>;
|
||||
export type GameStartInfo = z.infer<typeof GameStartInfoSchema>;
|
||||
const PlayerTypeSchema = z.enum(PlayerType);
|
||||
|
||||
export interface GameInfo {
|
||||
gameID: GameID;
|
||||
|
||||
+5
-3
@@ -257,7 +257,7 @@ export function createRandomName(
|
||||
return randomName;
|
||||
}
|
||||
|
||||
export const emojiTable: string[][] = [
|
||||
export const emojiTable = [
|
||||
["😀", "😊", "🥰", "😇", "😎"],
|
||||
["😞", "🥺", "😭", "😱", "😡"],
|
||||
["😈", "🤡", "🖕", "🥱", "🤦♂️"],
|
||||
@@ -269,9 +269,11 @@ export const emojiTable: string[][] = [
|
||||
["⬅️", "🎯", "➡️", "🥈", "🥉"],
|
||||
["↙️", "⬇️", "↘️", "❤️", "💔"],
|
||||
["💰", "⚓", "⛵", "🏡", "🛡️"],
|
||||
];
|
||||
] as const;
|
||||
// 2d to 1d array
|
||||
export const flattenedEmojiTable: string[] = emojiTable.flat();
|
||||
export const flattenedEmojiTable = emojiTable.flat();
|
||||
|
||||
export type Emoji = (typeof flattenedEmojiTable)[number];
|
||||
|
||||
/**
|
||||
* JSON.stringify replacer function that converts bigint values to strings.
|
||||
|
||||
@@ -669,6 +669,10 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
if (attacker.isPlayer() && defender.isPlayer()) {
|
||||
if (defender.isDisconnected() && attacker.isOnSameTeam(defender)) {
|
||||
// No troop loss if defender is disconnected.
|
||||
mag = 0;
|
||||
}
|
||||
if (
|
||||
attacker.type() === PlayerType.Human &&
|
||||
defender.type() === PlayerType.Bot
|
||||
|
||||
@@ -65,6 +65,7 @@ export class DevConfig extends DefaultConfig {
|
||||
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
const info = super.unitInfo(type);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const oldCost = info.cost;
|
||||
// info.cost = (p: Player) => oldCost(p) / 1000000000;
|
||||
return info;
|
||||
|
||||
@@ -100,13 +100,6 @@ export class AttackExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (this._owner.isOnSameTeam(this.target)) {
|
||||
console.warn(
|
||||
`${this._owner.displayName()} cannot attack ${this.target.displayName()} because they are on the same team`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.startTroops ??= this.mg
|
||||
|
||||
@@ -20,8 +20,8 @@ export class BotExecution implements Execution {
|
||||
this.random = new PseudoRandom(simpleHash(bot.id()));
|
||||
this.attackRate = this.random.nextInt(40, 80);
|
||||
this.attackTick = this.random.nextInt(0, this.attackRate);
|
||||
this.triggerRatio = this.random.nextInt(60, 90) / 100;
|
||||
this.reserveRatio = this.random.nextInt(20, 30) / 100;
|
||||
this.triggerRatio = this.random.nextInt(50, 60) / 100;
|
||||
this.reserveRatio = this.random.nextInt(30, 40) / 100;
|
||||
this.expandRatio = this.random.nextInt(10, 20) / 100;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Cell,
|
||||
Difficulty,
|
||||
Execution,
|
||||
Game,
|
||||
Gold,
|
||||
@@ -14,17 +13,18 @@ import {
|
||||
Unit,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { euclDistFN, manhattanDistFN, TileRef } from "../game/GameMap";
|
||||
import { TileRef, euclDistFN, manhattanDistFN } from "../game/GameMap";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { GameID } from "../Schemas";
|
||||
import { calculateBoundingBox, flattenedEmojiTable, simpleHash } from "../Util";
|
||||
import { calculateBoundingBox, simpleHash } from "../Util";
|
||||
import { ConstructionExecution } from "./ConstructionExecution";
|
||||
import { EmojiExecution } from "./EmojiExecution";
|
||||
import { structureSpawnTileValue } from "./nation/structureSpawnTileValue";
|
||||
import { NukeExecution } from "./NukeExecution";
|
||||
import { SpawnExecution } from "./SpawnExecution";
|
||||
import { TransportShipExecution } from "./TransportShipExecution";
|
||||
import { closestTwoTiles } from "./Util";
|
||||
import { BotBehavior } from "./utils/BotBehavior";
|
||||
import { BotBehavior, EMOJI_HECKLE } from "./utils/BotBehavior";
|
||||
|
||||
export class FakeHumanExecution implements Execution {
|
||||
private active = true;
|
||||
@@ -39,10 +39,9 @@ export class FakeHumanExecution implements Execution {
|
||||
private reserveRatio: number;
|
||||
private expandRatio: number;
|
||||
|
||||
private lastEmojiSent = new Map<Player, Tick>();
|
||||
private lastNukeSent: [Tick, TileRef][] = [];
|
||||
private embargoMalusApplied = new Set<PlayerID>();
|
||||
private heckleEmoji: number[];
|
||||
private readonly lastEmojiSent = new Map<Player, Tick>();
|
||||
private readonly lastNukeSent: [Tick, TileRef][] = [];
|
||||
private readonly embargoMalusApplied = new Set<PlayerID>();
|
||||
|
||||
constructor(
|
||||
gameID: GameID,
|
||||
@@ -53,10 +52,9 @@ export class FakeHumanExecution implements Execution {
|
||||
);
|
||||
this.attackRate = this.random.nextInt(40, 80);
|
||||
this.attackTick = this.random.nextInt(0, this.attackRate);
|
||||
this.triggerRatio = this.random.nextInt(60, 90) / 100;
|
||||
this.reserveRatio = this.random.nextInt(30, 60) / 100;
|
||||
this.expandRatio = this.random.nextInt(15, 25) / 100;
|
||||
this.heckleEmoji = ["🤡", "😡"].map((e) => flattenedEmojiTable.indexOf(e));
|
||||
this.triggerRatio = this.random.nextInt(50, 60) / 100;
|
||||
this.reserveRatio = this.random.nextInt(30, 40) / 100;
|
||||
this.expandRatio = this.random.nextInt(10, 20) / 100;
|
||||
}
|
||||
|
||||
init(mg: Game) {
|
||||
@@ -224,23 +222,12 @@ export class FakeHumanExecution implements Execution {
|
||||
const toAlly = this.random.randElement(enemies);
|
||||
if (this.player.canSendAllianceRequest(toAlly)) {
|
||||
this.player.createAllianceRequest(toAlly);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 50-50 attack weakest player vs random player
|
||||
const toAttack = this.random.chance(2)
|
||||
? enemies[0]
|
||||
: this.random.randElement(enemies);
|
||||
|
||||
if (this.shouldAttack(toAttack)) {
|
||||
this.behavior.sendAttack(toAttack);
|
||||
return;
|
||||
}
|
||||
|
||||
this.behavior.forgetOldEnemies();
|
||||
this.behavior.assistAllies();
|
||||
const enemy = this.behavior.selectEnemy();
|
||||
const enemy = this.behavior.selectEnemy(enemies);
|
||||
if (!enemy) return;
|
||||
this.maybeSendEmoji(enemy);
|
||||
this.maybeSendNuke(enemy);
|
||||
@@ -251,53 +238,6 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private shouldAttack(other: Player): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
|
||||
if (this.player.isOnSameTeam(other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const shouldAttack = this.attackChance(other);
|
||||
|
||||
// Consider betrayal for allies
|
||||
if (shouldAttack && this.player.isAlliedWith(other)) {
|
||||
return this.maybeConsiderBetrayal(other);
|
||||
}
|
||||
|
||||
return shouldAttack;
|
||||
}
|
||||
|
||||
private attackChance(other: Player): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
|
||||
if (this.player.isAlliedWith(other)) {
|
||||
return this.shouldDiscourageAttack(other)
|
||||
? this.random.chance(200)
|
||||
: this.random.chance(50);
|
||||
} else {
|
||||
return this.shouldDiscourageAttack(other) ? this.random.chance(4) : true;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldDiscourageAttack(other: Player) {
|
||||
if (other.isTraitor()) {
|
||||
return false;
|
||||
}
|
||||
const difficulty = this.mg.config().gameConfig().difficulty;
|
||||
if (
|
||||
difficulty === Difficulty.Hard ||
|
||||
difficulty === Difficulty.Impossible
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (other.type() !== PlayerType.Human) {
|
||||
return false;
|
||||
}
|
||||
// Only discourage attacks on Humans who are not traitors on easy or medium difficulty.
|
||||
return true;
|
||||
}
|
||||
|
||||
private maybeSendEmoji(enemy: Player) {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (enemy.type() !== PlayerType.Human) return;
|
||||
@@ -308,7 +248,7 @@ export class FakeHumanExecution implements Execution {
|
||||
new EmojiExecution(
|
||||
this.player,
|
||||
enemy.id(),
|
||||
this.random.randElement(this.heckleEmoji),
|
||||
this.random.randElement(EMOJI_HECKLE),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -460,6 +400,8 @@ export class FakeHumanExecution implements Execution {
|
||||
this.maybeSpawnStructure(UnitType.Port) ||
|
||||
this.maybeSpawnWarship() ||
|
||||
this.maybeSpawnStructure(UnitType.Factory) ||
|
||||
this.maybeSpawnStructure(UnitType.DefensePost) ||
|
||||
this.maybeSpawnStructure(UnitType.SAMLauncher) ||
|
||||
this.maybeSpawnStructure(UnitType.MissileSilo)
|
||||
);
|
||||
}
|
||||
@@ -486,7 +428,8 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private structureSpawnTile(type: UnitType): TileRef | null {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (this.mg === undefined) throw new Error("Not initialized");
|
||||
if (this.player === null) throw new Error("Not initialized");
|
||||
const tiles =
|
||||
type === UnitType.Port
|
||||
? Array.from(this.player.borderTiles()).filter((t) =>
|
||||
@@ -494,7 +437,7 @@ export class FakeHumanExecution implements Execution {
|
||||
)
|
||||
: Array.from(this.player.tiles());
|
||||
if (tiles.length === 0) return null;
|
||||
const valueFunction = this.structureSpawnTileValue(type);
|
||||
const valueFunction = structureSpawnTileValue(this.mg, this.player, type);
|
||||
let bestTile: TileRef | null = null;
|
||||
let bestValue = 0;
|
||||
const sampledTiles = this.arraySampler(tiles);
|
||||
@@ -524,69 +467,6 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private structureSpawnTileValue(type: UnitType): (tile: TileRef) => number {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const borderTiles = this.player.borderTiles();
|
||||
const mg = this.mg;
|
||||
const otherUnits = this.player.units(type);
|
||||
// Prefer spacing structures out of atom bomb range
|
||||
const borderSpacing = this.mg
|
||||
.config()
|
||||
.nukeMagnitudes(UnitType.AtomBomb).outer;
|
||||
const structureSpacing = borderSpacing * 2;
|
||||
switch (type) {
|
||||
case UnitType.Port:
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer to be far away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
return w;
|
||||
};
|
||||
case UnitType.City:
|
||||
case UnitType.Factory:
|
||||
case UnitType.MissileSilo:
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer higher elevations
|
||||
w += mg.magnitude(tile);
|
||||
|
||||
// Prefer to be away from the border
|
||||
const closestBorder = closestTwoTiles(mg, borderTiles, [tile]);
|
||||
if (closestBorder !== null) {
|
||||
const d = mg.manhattanDist(closestBorder.x, tile);
|
||||
w += Math.min(d, borderSpacing);
|
||||
}
|
||||
|
||||
// Prefer to be away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
// TODO: Cities and factories should consider train range limits
|
||||
return w;
|
||||
};
|
||||
default:
|
||||
throw new Error(`Value function not implemented for ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
private maybeSpawnWarship(): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (!this.random.chance(50)) {
|
||||
|
||||
@@ -50,7 +50,8 @@ class SAMTargetingSystem {
|
||||
|
||||
private isInRange(tile: TileRef) {
|
||||
const samTile = this.sam.tile();
|
||||
const rangeSquared = this.mg.config().defaultSamRange() ** 2;
|
||||
const range = this.mg.config().defaultSamRange();
|
||||
const rangeSquared = range * range;
|
||||
return this.mg.euclideanDistSquared(samTile, tile) <= rangeSquared;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { Game, Player, Relation, UnitType } from "../../game/Game";
|
||||
import { TileRef } from "../../game/GameMap";
|
||||
import { closestTwoTiles } from "../Util";
|
||||
|
||||
export function structureSpawnTileValue(
|
||||
mg: Game,
|
||||
player: Player,
|
||||
type: UnitType,
|
||||
): (tile: TileRef) => number {
|
||||
const borderTiles = player.borderTiles();
|
||||
const otherUnits = player.units(type);
|
||||
// Prefer spacing structures out of atom bomb range
|
||||
const borderSpacing = mg.config().nukeMagnitudes(UnitType.AtomBomb).outer;
|
||||
const structureSpacing = borderSpacing * 2;
|
||||
switch (type) {
|
||||
case UnitType.City:
|
||||
case UnitType.Factory:
|
||||
case UnitType.MissileSilo: {
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer higher elevations
|
||||
w += mg.magnitude(tile);
|
||||
|
||||
// Prefer to be away from the border
|
||||
const closestBorder = closestTwoTiles(mg, borderTiles, [tile]);
|
||||
if (closestBorder !== null) {
|
||||
const d = mg.manhattanDist(closestBorder.x, tile);
|
||||
w += Math.min(d, borderSpacing);
|
||||
}
|
||||
|
||||
// Prefer to be away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
// TODO: Cities and factories should consider train range limits
|
||||
return w;
|
||||
};
|
||||
}
|
||||
case UnitType.Port: {
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer to be away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
return w;
|
||||
};
|
||||
}
|
||||
case UnitType.DefensePost: {
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer higher elevations
|
||||
w += mg.magnitude(tile);
|
||||
|
||||
const closestBorder = closestTwoTiles(mg, borderTiles, [tile]);
|
||||
if (closestBorder !== null) {
|
||||
// Prefer to be borderSpacing tiles from the border
|
||||
const d = mg.manhattanDist(closestBorder.x, tile);
|
||||
w += Math.max(0, borderSpacing - Math.abs(borderSpacing - d));
|
||||
|
||||
// Prefer adjacent players who are hostile
|
||||
const neighbors: Set<Player> = new Set();
|
||||
for (const tile of mg.neighbors(closestBorder.x)) {
|
||||
if (!mg.isLand(tile)) continue;
|
||||
const id = mg.ownerID(tile);
|
||||
if (id === player.smallID()) continue;
|
||||
const neighbor = mg.playerBySmallID(id);
|
||||
if (!neighbor.isPlayer()) continue;
|
||||
neighbors.add(neighbor);
|
||||
}
|
||||
for (const neighbor of neighbors) {
|
||||
w +=
|
||||
borderSpacing * (Relation.Friendly - player.relation(neighbor));
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer to be away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
return w;
|
||||
};
|
||||
}
|
||||
case UnitType.SAMLauncher: {
|
||||
const protectTiles: Set<TileRef> = new Set();
|
||||
for (const unit of player.units()) {
|
||||
switch (unit.type()) {
|
||||
case UnitType.City:
|
||||
case UnitType.Factory:
|
||||
case UnitType.MissileSilo:
|
||||
case UnitType.Port:
|
||||
protectTiles.add(unit.tile());
|
||||
}
|
||||
}
|
||||
const range = mg.config().defaultSamRange();
|
||||
const rangeSquared = range * range;
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer higher elevations
|
||||
w += mg.magnitude(tile);
|
||||
|
||||
// Prefer to be away from the border
|
||||
const closestBorder = closestTwoTiles(mg, borderTiles, [tile]);
|
||||
if (closestBorder !== null) {
|
||||
const d = mg.manhattanDist(closestBorder.x, tile);
|
||||
w += Math.min(d, borderSpacing);
|
||||
}
|
||||
|
||||
// Prefer to be away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
// Prefer to be in range of other structures
|
||||
for (const maybeProtected of protectTiles) {
|
||||
const distanceSquared = mg.euclideanDistSquared(tile, maybeProtected);
|
||||
if (distanceSquared > rangeSquared) continue;
|
||||
w += structureSpacing;
|
||||
}
|
||||
|
||||
return w;
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(`Value function not implemented for ${type}`);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AllianceRequest,
|
||||
Difficulty,
|
||||
Game,
|
||||
Player,
|
||||
PlayerType,
|
||||
@@ -13,11 +14,17 @@ import { AllianceExtensionExecution } from "../alliance/AllianceExtensionExecuti
|
||||
import { AttackExecution } from "../AttackExecution";
|
||||
import { EmojiExecution } from "../EmojiExecution";
|
||||
|
||||
const emojiId = (e: (typeof flattenedEmojiTable)[number]) =>
|
||||
flattenedEmojiTable.indexOf(e);
|
||||
const EMOJI_ASSIST_ACCEPT = (["👍", "⛵", "🤝", "🎯"] as const).map(emojiId);
|
||||
const EMOJI_RELATION_TOO_LOW = (["🥱", "🤦♂️"] as const).map(emojiId);
|
||||
const EMOJI_TARGET_ME = (["🥺", "💀"] as const).map(emojiId);
|
||||
const EMOJI_TARGET_ALLY = (["🕊️", "👎"] as const).map(emojiId);
|
||||
export const EMOJI_HECKLE = (["🤡", "😡"] as const).map(emojiId);
|
||||
|
||||
export class BotBehavior {
|
||||
private enemy: Player | null = null;
|
||||
private enemyUpdated: Tick;
|
||||
|
||||
private assistAcceptEmoji = flattenedEmojiTable.indexOf("👍");
|
||||
private enemyUpdated: Tick | undefined;
|
||||
|
||||
constructor(
|
||||
private random: PseudoRandom,
|
||||
@@ -65,23 +72,80 @@ export class BotBehavior {
|
||||
this.game.addExecution(new EmojiExecution(this.player, player.id(), emoji));
|
||||
}
|
||||
|
||||
private setNewEnemy(newEnemy: Player | null) {
|
||||
private setNewEnemy(newEnemy: Player | null, force = false) {
|
||||
if (newEnemy !== null && !force && !this.shouldAttack(newEnemy)) return;
|
||||
this.enemy = newEnemy;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
}
|
||||
|
||||
private shouldAttack(other: Player): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (this.player.isOnSameTeam(other)) {
|
||||
return false;
|
||||
}
|
||||
const shouldAttack = this.attackChance(other);
|
||||
if (shouldAttack && this.player.isAlliedWith(other)) {
|
||||
this.betray(other);
|
||||
return true;
|
||||
}
|
||||
return shouldAttack;
|
||||
}
|
||||
|
||||
private betray(target: Player): void {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const alliance = this.player.allianceWith(target);
|
||||
if (!alliance) return;
|
||||
this.player.breakAlliance(alliance);
|
||||
}
|
||||
|
||||
private attackChance(other: Player): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
|
||||
if (this.player.isAlliedWith(other)) {
|
||||
return this.shouldDiscourageAttack(other)
|
||||
? this.random.chance(200)
|
||||
: this.random.chance(50);
|
||||
} else {
|
||||
return this.shouldDiscourageAttack(other) ? this.random.chance(4) : true;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldDiscourageAttack(other: Player) {
|
||||
if (other.isTraitor()) {
|
||||
return false;
|
||||
}
|
||||
const { difficulty } = this.game.config().gameConfig();
|
||||
if (
|
||||
difficulty === Difficulty.Hard ||
|
||||
difficulty === Difficulty.Impossible
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (other.type() !== PlayerType.Human) {
|
||||
return false;
|
||||
}
|
||||
// Only discourage attacks on Humans who are not traitors on easy or medium difficulty.
|
||||
return true;
|
||||
}
|
||||
|
||||
private clearEnemy() {
|
||||
this.enemy = null;
|
||||
}
|
||||
|
||||
forgetOldEnemies() {
|
||||
// Forget old enemies
|
||||
if (this.game.ticks() - this.enemyUpdated > 100) {
|
||||
if (this.game.ticks() - (this.enemyUpdated ?? 0) > 100) {
|
||||
this.clearEnemy();
|
||||
}
|
||||
}
|
||||
|
||||
private hasSufficientTroops(): boolean {
|
||||
private hasReserveRatioTroops(): boolean {
|
||||
const maxTroops = this.game.config().maxTroops(this.player);
|
||||
const ratio = this.player.troops() / maxTroops;
|
||||
return ratio >= this.reserveRatio;
|
||||
}
|
||||
|
||||
private hasTriggerRatioTroops(): boolean {
|
||||
const maxTroops = this.game.config().maxTroops(this.player);
|
||||
const ratio = this.player.troops() / maxTroops;
|
||||
return ratio >= this.triggerRatio;
|
||||
@@ -98,7 +162,7 @@ export class BotBehavior {
|
||||
largestAttacker = attack.attacker();
|
||||
}
|
||||
if (largestAttacker !== undefined) {
|
||||
this.setNewEnemy(largestAttacker);
|
||||
this.setNewEnemy(largestAttacker, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,34 +174,37 @@ export class BotBehavior {
|
||||
}
|
||||
|
||||
assistAllies() {
|
||||
outer: for (const ally of this.player.allies()) {
|
||||
for (const ally of this.player.allies()) {
|
||||
if (ally.targets().length === 0) continue;
|
||||
if (this.player.relation(ally) < Relation.Friendly) {
|
||||
// this.emoji(ally, "🤦");
|
||||
this.emoji(ally, this.random.randElement(EMOJI_RELATION_TOO_LOW));
|
||||
continue;
|
||||
}
|
||||
for (const target of ally.targets()) {
|
||||
if (target === this.player) {
|
||||
// this.emoji(ally, "💀");
|
||||
this.emoji(ally, this.random.randElement(EMOJI_TARGET_ME));
|
||||
continue;
|
||||
}
|
||||
if (this.player.isAlliedWith(target)) {
|
||||
// this.emoji(ally, "👎");
|
||||
this.emoji(ally, this.random.randElement(EMOJI_TARGET_ALLY));
|
||||
continue;
|
||||
}
|
||||
// All checks passed, assist them
|
||||
this.player.updateRelation(ally, -20);
|
||||
this.setNewEnemy(target);
|
||||
this.emoji(ally, this.assistAcceptEmoji);
|
||||
break outer;
|
||||
this.emoji(ally, this.random.randElement(EMOJI_ASSIST_ACCEPT));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectEnemy(): Player | null {
|
||||
selectEnemy(enemies: Player[]): Player | null {
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the trigger ratio
|
||||
if (!this.hasSufficientTroops()) return null;
|
||||
// Save up troops until we reach the reserve ratio
|
||||
if (!this.hasReserveRatioTroops()) return null;
|
||||
|
||||
// Maybe save up troops until we reach the trigger ratio
|
||||
if (!this.hasTriggerRatioTroops() && !this.random.chance(10)) return null;
|
||||
|
||||
// Prefer neighboring bots
|
||||
const bots = this.player
|
||||
@@ -165,11 +232,13 @@ export class BotBehavior {
|
||||
|
||||
// Retaliate against incoming attacks
|
||||
if (this.enemy === null) {
|
||||
// Only after clearing bots
|
||||
this.checkIncomingAttacks();
|
||||
}
|
||||
|
||||
// Select the most hated player
|
||||
if (this.enemy === null) {
|
||||
if (this.enemy === null && this.random.chance(2)) {
|
||||
// 50% chance
|
||||
const mostHated = this.player.allRelationsSorted()[0];
|
||||
if (
|
||||
mostHated !== undefined &&
|
||||
@@ -178,6 +247,16 @@ export class BotBehavior {
|
||||
this.setNewEnemy(mostHated.player);
|
||||
}
|
||||
}
|
||||
|
||||
// Select the weakest player
|
||||
if (this.enemy === null && enemies.length > 0) {
|
||||
this.setNewEnemy(enemies[0]);
|
||||
}
|
||||
|
||||
// Select a random player
|
||||
if (this.enemy === null && enemies.length > 0) {
|
||||
this.setNewEnemy(this.random.randElement(enemies));
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check, don't attack our allies or teammates
|
||||
@@ -187,7 +266,7 @@ export class BotBehavior {
|
||||
selectRandomEnemy(): Player | TerraNullius | null {
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the trigger ratio
|
||||
if (!this.hasSufficientTroops()) return null;
|
||||
if (!this.hasTriggerRatioTroops()) return null;
|
||||
|
||||
// Choose a new enemy randomly
|
||||
const neighbors = this.player.neighbors();
|
||||
|
||||
@@ -11,6 +11,13 @@ import { RailNetwork } from "./RailNetwork";
|
||||
import { Stats } from "./Stats";
|
||||
import { UnitPredicate } from "./UnitGrid";
|
||||
|
||||
function isEnumValue<T extends Record<string, string | number>>(
|
||||
enumObj: T,
|
||||
value: unknown,
|
||||
): value is T[keyof T] {
|
||||
return Object.values(enumObj).includes(value as T[keyof T]);
|
||||
}
|
||||
|
||||
export type PlayerID = string;
|
||||
export type Tick = number;
|
||||
export type Gold = bigint;
|
||||
@@ -37,6 +44,8 @@ export enum Difficulty {
|
||||
Hard = "Hard",
|
||||
Impossible = "Impossible",
|
||||
}
|
||||
export const isDifficulty = (value: unknown): value is Difficulty =>
|
||||
isEnumValue(Difficulty, value);
|
||||
|
||||
export type Team = string;
|
||||
|
||||
@@ -134,11 +143,15 @@ export enum GameType {
|
||||
Public = "Public",
|
||||
Private = "Private",
|
||||
}
|
||||
export const isGameType = (value: unknown): value is GameType =>
|
||||
isEnumValue(GameType, value);
|
||||
|
||||
export enum GameMode {
|
||||
FFA = "Free For All",
|
||||
Team = "Team",
|
||||
}
|
||||
export const isGameMode = (value: unknown): value is GameMode =>
|
||||
isEnumValue(GameMode, value);
|
||||
|
||||
export enum GameMapSize {
|
||||
Compact = "Compact",
|
||||
|
||||
@@ -160,6 +160,7 @@ export interface PlayerUpdate {
|
||||
allies: number[];
|
||||
embargoes: Set<PlayerID>;
|
||||
isTraitor: boolean;
|
||||
traitorRemainingTicks?: number;
|
||||
targets: number[];
|
||||
outgoingEmojis: EmojiMessage[];
|
||||
outgoingAttacks: AttackUpdate[];
|
||||
|
||||
@@ -404,6 +404,9 @@ export class PlayerView {
|
||||
isTraitor(): boolean {
|
||||
return this.data.isTraitor;
|
||||
}
|
||||
getTraitorRemainingTicks(): number {
|
||||
return Math.max(0, this.data.traitorRemainingTicks ?? 0);
|
||||
}
|
||||
outgoingEmojis(): EmojiMessage[] {
|
||||
return this.data.outgoingEmojis;
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ export class PlayerImpl implements Player {
|
||||
allies: this.alliances().map((a) => a.other(this).smallID()),
|
||||
embargoes: new Set([...this.embargoes.keys()].map((p) => p.toString())),
|
||||
isTraitor: this.isTraitor(),
|
||||
traitorRemainingTicks: this.getTraitorRemainingTicks(),
|
||||
targets: this.targets().map((p) => p.smallID()),
|
||||
outgoingEmojis: this.outgoingEmojis(),
|
||||
outgoingAttacks: this._outgoingAttacks.map((a) => {
|
||||
@@ -418,11 +419,15 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
isTraitor(): boolean {
|
||||
return (
|
||||
this.markedTraitorTick >= 0 &&
|
||||
this.mg.ticks() - this.markedTraitorTick <
|
||||
this.mg.config().traitorDuration()
|
||||
);
|
||||
return this.getTraitorRemainingTicks() > 0;
|
||||
}
|
||||
|
||||
getTraitorRemainingTicks(): number {
|
||||
if (this.markedTraitorTick < 0) return 0;
|
||||
const elapsed = this.mg.ticks() - this.markedTraitorTick;
|
||||
const duration = this.mg.config().traitorDuration();
|
||||
const remaining = duration - elapsed;
|
||||
return remaining > 0 ? remaining : 0;
|
||||
}
|
||||
|
||||
markTraitor(): void {
|
||||
@@ -741,6 +746,9 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
isFriendly(other: Player): boolean {
|
||||
if (other.isDisconnected()) {
|
||||
return false;
|
||||
}
|
||||
return this.isOnSameTeam(other) || this.isAlliedWith(other);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export function assignTeams(
|
||||
);
|
||||
|
||||
// First, assign clan players
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const [_, clanPlayers] of sortedClans) {
|
||||
// Try to keep the clan together on the team with fewer players
|
||||
let team: Team | null = null;
|
||||
|
||||
@@ -18,8 +18,6 @@ export class TerrainSearchMap {
|
||||
node(x: number, y: number): SearchMapTileType {
|
||||
const packedByte = this.mapData[4 + y * this.width + x];
|
||||
const isLand = packedByte & 0b10000000;
|
||||
const shoreline = !!(packedByte & 0b01000000);
|
||||
const ocean = !!(packedByte & 0b00100000);
|
||||
const magnitude = packedByte & 0b00011111;
|
||||
if (isLand) {
|
||||
return SearchMapTileType.Land;
|
||||
|
||||
@@ -285,14 +285,13 @@ async function schedulePublicGame(playlist: MapPlaylist) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to schedule public game: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
} catch (error) {
|
||||
log.error(`Failed to schedule public game on worker ${workerPath}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ export async function verifyClientToken(
|
||||
const issuer = config.jwtIssuer();
|
||||
const audience = config.jwtAudience();
|
||||
const key = await config.jwkPublicKey();
|
||||
const { payload, protectedHeader } = await jwtVerify(token, key, {
|
||||
const { payload } = await jwtVerify(token, key, {
|
||||
algorithms: ["EdDSA"],
|
||||
issuer,
|
||||
audience,
|
||||
|
||||
@@ -85,7 +85,6 @@ describe("Shell Random Damage", () => {
|
||||
expect(damage).toBeLessThanOrEqual(maxExpectedDamage);
|
||||
});
|
||||
|
||||
const uniqueDamages = new Set(damages);
|
||||
expect(damages.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -231,16 +230,6 @@ describe("Shell Random Damage", () => {
|
||||
|
||||
expect(damages.length).toBeGreaterThan(0);
|
||||
|
||||
const baseDamage = game.config().unitInfo(UnitType.Shell).damage ?? 250;
|
||||
const expectedDamages = [
|
||||
Math.round((baseDamage / 250) * 200),
|
||||
Math.round((baseDamage / 250) * 225),
|
||||
Math.round((baseDamage / 250) * 250),
|
||||
Math.round((baseDamage / 250) * 275),
|
||||
Math.round((baseDamage / 250) * 300),
|
||||
Math.round((baseDamage / 250) * 325),
|
||||
];
|
||||
|
||||
const uniqueDamages = new Set(damages);
|
||||
expect(uniqueDamages.size).toBeGreaterThan(0);
|
||||
|
||||
@@ -265,7 +254,6 @@ describe("Shell Random Damage", () => {
|
||||
);
|
||||
const initialHealth = target.health();
|
||||
|
||||
const seed = 12345;
|
||||
const shell1 = new ShellExecution(
|
||||
game.ref(coastX, 10),
|
||||
player1,
|
||||
|
||||
@@ -83,6 +83,7 @@ describe("SAM", () => {
|
||||
game.addExecution(new SAMLauncherExecution(defender, null, sam));
|
||||
|
||||
// Sam will only target nukes it can destroy before it reaches its target
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 1), {
|
||||
targetTile: game.ref(3, 1),
|
||||
trajectory: [
|
||||
|
||||
Reference in New Issue
Block a user