diff --git a/.vscode/settings.json b/.vscode/settings.json index d7d9f0d..6ad2815 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,9 +15,12 @@ "Prifecite", "Prifetium", "rasterization", + "Reconstructors", + "Respawned", "Russanite", "Russium", "unselectable", + "websockets", "Zirathium" ], "files.associations": { diff --git a/TODO.md b/TODO.md index 793c480..0514b4a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,13 @@ +### Broken by new multi server system + +- Current player information (works except logout) +- Deployment +- Alternative background (Going to ignore for now) +- Some admin routes + ### Server stuff +- The tutorial respawn breaks if the tutorial sector is transferred while the player is dead in the tutorial (low priority) - Anti chat spam (low priority) - Killed/killed by messages?? @@ -7,6 +15,7 @@ ### User Interface +- Map css grid is being janky - I may want friend location/online notifications (idk if I should have a client subscribe model or just have the server figure it out and send it to the client) ### Effects @@ -21,7 +30,6 @@ ### WebGL optimizations that can be done (in estimated order of performance gain) -- move a bunch of the vbos into vaos to avoid setting the attributes over and over - instanced rendering for projectiles - instanced rendering for other things - use ubos for the lights and particle emitters diff --git a/resources/models/emp_mine.obj b/resources/models/emp_mine.obj new file mode 100755 index 0000000..6bde81e --- /dev/null +++ b/resources/models/emp_mine.obj @@ -0,0 +1,363 @@ +# Blender 3.3.1 +# www.blender.org +o emp_mine +v 0.747111 0.000000 0.000000 +v 0.621533 0.000000 0.260766 +v 0.339362 0.000000 0.325169 +v 0.177290 0.000000 0.226891 +v 0.177290 0.000000 -0.226891 +v 0.339362 0.000000 -0.325169 +v 0.621533 0.000000 -0.260766 +v 0.528287 0.528287 0.000000 +v 0.439490 0.439490 0.260766 +v 0.239965 0.239965 0.325169 +v 0.125363 0.125363 0.226891 +v 0.125363 0.125363 -0.226891 +v 0.239965 0.239965 -0.325169 +v 0.439490 0.439490 -0.260766 +v 0.000000 0.747111 0.000000 +v 0.000000 0.621533 0.260766 +v 0.000000 0.339362 0.325169 +v 0.000000 0.177290 0.226891 +v 0.000000 0.177290 -0.226891 +v 0.000000 0.339362 -0.325169 +v 0.000000 0.621533 -0.260766 +v -0.528287 0.528287 0.000000 +v -0.439490 0.439490 0.260766 +v -0.239965 0.239965 0.325169 +v -0.125363 0.125363 0.226891 +v -0.125363 0.125363 -0.226891 +v -0.239965 0.239965 -0.325169 +v -0.439490 0.439490 -0.260766 +v -0.747111 0.000000 0.000000 +v -0.621533 0.000000 0.260766 +v -0.339362 0.000000 0.325169 +v -0.177290 0.000000 0.226891 +v -0.177290 0.000000 -0.226891 +v -0.339362 0.000000 -0.325169 +v -0.621533 0.000000 -0.260766 +v -0.528287 -0.528287 0.000000 +v -0.439490 -0.439490 0.260766 +v -0.239965 -0.239965 0.325169 +v -0.125363 -0.125363 0.226891 +v -0.125363 -0.125363 -0.226891 +v -0.239965 -0.239965 -0.325169 +v -0.439490 -0.439490 -0.260766 +v 0.000000 -0.747111 0.000000 +v 0.000000 -0.621533 0.260766 +v 0.000000 -0.339362 0.325169 +v 0.000000 -0.177290 0.226891 +v 0.000000 -0.177290 -0.226891 +v 0.000000 -0.339362 -0.325169 +v 0.000000 -0.621533 -0.260766 +v 0.528287 -0.528288 0.000000 +v 0.439490 -0.439490 0.260766 +v 0.239965 -0.239965 0.325169 +v 0.125363 -0.125363 0.226891 +v 0.125363 -0.125363 -0.226891 +v 0.239965 -0.239965 -0.325169 +v 0.439490 -0.439490 -0.260766 +vn 0.8441 0.3496 0.4065 +vn 0.2216 0.0918 0.9708 +vn -0.5069 -0.2100 0.8360 +vn -0.5069 -0.2100 -0.8360 +vn 0.2216 0.0918 -0.9708 +vn 0.8441 0.3496 -0.4065 +vn 0.3496 0.8441 0.4065 +vn 0.0918 0.2216 0.9708 +vn -0.2100 -0.5069 0.8360 +vn -0.2100 -0.5069 -0.8360 +vn 0.0918 0.2216 -0.9708 +vn 0.3496 0.8441 -0.4065 +vn -0.3496 0.8441 0.4065 +vn -0.0918 0.2216 0.9708 +vn 0.2100 -0.5069 0.8360 +vn 0.2100 -0.5069 -0.8360 +vn -0.0918 0.2216 -0.9708 +vn -0.3496 0.8441 -0.4065 +vn -0.8441 0.3496 0.4065 +vn -0.2216 0.0918 0.9708 +vn 0.5069 -0.2100 0.8360 +vn 0.5069 -0.2100 -0.8360 +vn -0.2216 0.0918 -0.9708 +vn -0.8441 0.3496 -0.4065 +vn -0.8441 -0.3496 0.4065 +vn -0.2216 -0.0918 0.9708 +vn 0.5069 0.2100 0.8360 +vn 0.5069 0.2100 -0.8360 +vn -0.2216 -0.0918 -0.9708 +vn -0.8441 -0.3496 -0.4065 +vn -0.3496 -0.8441 0.4065 +vn -0.0918 -0.2216 0.9708 +vn 0.2100 0.5069 0.8360 +vn 0.2100 0.5069 -0.8360 +vn -0.0918 -0.2216 -0.9708 +vn -0.3496 -0.8441 -0.4065 +vn 0.3496 -0.8441 0.4065 +vn 0.0918 -0.2216 0.9708 +vn -0.2100 0.5069 0.8360 +vn -0.2100 0.5069 -0.8360 +vn 0.0918 -0.2216 -0.9708 +vn 0.3496 -0.8441 -0.4065 +vn 0.8441 -0.3496 0.4065 +vn 0.2216 -0.0918 0.9708 +vn -0.5069 0.2100 0.8360 +vn -0.0000 -0.0000 -1.0000 +vn -0.5069 0.2100 -0.8360 +vn 0.2216 -0.0918 -0.9708 +vn 0.8441 -0.3496 -0.4065 +vn -0.0000 -0.0000 1.0000 +vt 0.242188 0.735491 +vt 0.242188 0.790847 +vt 0.242188 0.846203 +vt 0.242188 0.357922 +vt 0.242188 0.901559 +vt 0.242188 0.413278 +vt 0.789062 0.839059 +vt 0.914421 0.851868 +vt 0.242188 0.569423 +vt 0.242188 0.081142 +vt 0.242188 0.136498 +vt 0.242188 0.624779 +vt 0.242188 0.680135 +vt 0.290624 0.735491 +vt 0.290624 0.790847 +vt 0.290624 0.846203 +vt 0.290624 0.357922 +vt 0.290624 0.413278 +vt 0.290624 0.901559 +vt 0.837499 0.839059 +vt 0.290624 0.569423 +vt 0.290624 0.081142 +vt 0.918568 0.820738 +vt 0.290624 0.136498 +vt 0.290624 0.624779 +vt 0.290624 0.680135 +vt 0.702777 0.381754 +vt 0.339061 0.735491 +vt 0.578831 0.237620 +vt 0.642084 0.225506 +vt 0.339061 0.790847 +vt 0.870039 0.353709 +vt 0.339061 0.846203 +vt 0.339061 0.357922 +vt 0.704069 0.198288 +vt 0.339061 0.413278 +vt 0.826723 0.369342 +vt 0.879144 0.203960 +vt 0.885935 0.839059 +vt 0.870178 0.288827 +vt 0.339061 0.081142 +vt 0.893731 0.795026 +vt 0.578831 0.342422 +vt 0.339061 0.136498 +vt 0.339061 0.624779 +vt 0.891620 0.246265 +vt 0.339061 0.680135 +vt 0.633751 0.369641 +vt 0.578831 0.093485 +vt 0.702777 0.237620 +vt 0.387497 0.735491 +vt 0.387497 0.790847 +vt 0.642084 0.105598 +vt 0.704069 0.132817 +vt 0.870039 0.419179 +vt 0.387497 0.846203 +vt 0.387497 0.357922 +vt 0.387497 0.413278 +vt 0.934372 0.839059 +vt 0.826723 0.403546 +vt 0.844941 0.203960 +vt 0.387497 0.081142 +vt 0.836274 0.284310 +vt 0.895309 0.624111 +vt 0.387497 0.624779 +vt 0.826723 0.237620 +vt 0.578831 0.276952 +vt 0.387497 0.136498 +vt 0.633751 0.249733 +vt 0.387497 0.680135 +vt 0.435933 0.735491 +vt 0.048442 0.735491 +vt 0.435933 0.790847 +vt 0.048442 0.790847 +vt 0.435933 0.846203 +vt 0.435933 0.357922 +vt 0.048442 0.846203 +vt 0.435933 0.413278 +vt 0.048442 0.901559 +vt 0.595317 0.839059 +vt 0.048442 0.569423 +vt 0.556254 0.624111 +vt 0.435933 0.081142 +vt 0.435933 0.136498 +vt 0.435933 0.624779 +vt 0.048442 0.624779 +vt 0.435933 0.680135 +vt 0.048442 0.680135 +vt 0.096878 0.735491 +vt 0.096878 0.790847 +vt 0.096878 0.357922 +vt 0.096878 0.846203 +vt 0.096878 0.901559 +vt 0.096878 0.413278 +vt 0.643753 0.839059 +vt 0.096878 0.569423 +vt 0.096878 0.081142 +vt 0.604691 0.624111 +vt 0.096878 0.136498 +vt 0.096878 0.624779 +vt 0.096878 0.680135 +vt 0.145315 0.735491 +vt 0.702777 0.381754 +vt 0.829307 0.237619 +vt 0.145315 0.790847 +vt 0.771803 0.369641 +vt 0.826723 0.342422 +vt 0.829307 0.093485 +vt 0.145315 0.846203 +vt 0.145315 0.357922 +vt 0.145315 0.413278 +vt 0.844941 0.138505 +vt 0.692190 0.839059 +vt 0.145315 0.081142 +vt 0.870039 0.403545 +vt 0.827631 0.349192 +vt 0.653127 0.624111 +vt 0.913356 0.419179 +vt 0.704069 0.198287 +vt 0.145315 0.136498 +vt 0.145315 0.624779 +vt 0.145315 0.680135 +vt 0.766054 0.225506 +vt 0.193751 0.735491 +vt 0.702777 0.237620 +vt 0.829307 0.093485 +vt 0.771803 0.249733 +vt 0.193751 0.790847 +vt 0.193751 0.357922 +vt 0.826723 0.276952 +vt 0.894777 0.093485 +vt 0.193751 0.846203 +vt 0.740626 0.839059 +vt 0.879144 0.138505 +vt 0.193751 0.413278 +vt 0.193751 0.081142 +vt 0.870039 0.369342 +vt 0.861534 0.353709 +vt 0.883719 0.870181 +vt 0.193751 0.624779 +vt 0.913356 0.353709 +vt 0.704069 0.132817 +vt 0.193751 0.136498 +vt 0.766054 0.105599 +vt 0.193751 0.680135 +s 0 +f 8/14/1 2/2/1 1/1/1 +f 2/2/2 10/16/2 3/3/2 +f 10/16/3 4/5/3 3/3/3 +f 5/9/4 13/25/4 6/12/4 +f 13/25/5 7/13/5 6/12/5 +f 14/26/6 1/1/6 7/13/6 +f 8/14/7 16/31/7 9/15/7 +f 9/15/8 17/33/8 10/16/8 +f 10/17/9 18/36/9 11/18/9 +f 12/22/10 20/44/10 13/24/10 +f 20/45/11 14/26/11 13/25/11 +f 21/47/12 8/14/12 14/26/12 +f 15/29/13 23/53/13 16/30/13 +f 23/53/14 17/35/14 16/30/14 +f 24/55/15 18/37/15 17/32/15 +f 26/63/16 20/46/16 19/40/16 +f 27/67/17 21/48/17 20/43/17 +f 21/48/18 22/50/18 15/27/18 +f 22/51/19 30/73/19 23/52/19 +f 30/73/20 24/56/20 23/52/20 +f 24/57/21 32/78/21 25/58/21 +f 33/83/22 27/68/22 26/62/22 +f 34/85/23 28/70/23 27/65/23 +f 28/70/24 29/71/24 22/51/24 +f 36/89/25 30/74/25 29/72/25 +f 37/90/26 31/77/26 30/74/26 +f 38/92/27 32/79/27 31/77/27 +f 40/96/28 34/86/28 33/81/28 +f 41/100/29 35/88/29 34/86/29 +f 42/101/30 29/72/30 35/88/30 +f 43/102/31 37/90/31 36/89/31 +f 44/105/32 38/92/32 37/90/32 +f 45/110/33 39/94/33 38/91/33 +f 40/97/34 48/120/34 41/99/34 +f 48/121/35 42/101/35 41/100/35 +f 49/122/36 36/89/36 42/101/36 +f 43/103/37 51/127/37 44/106/37 +f 44/106/38 52/130/38 45/107/38 +f 52/131/39 46/112/39 45/108/39 +f 47/115/40 55/141/40 48/118/40 +f 55/142/41 49/123/41 48/119/41 +f 56/144/42 43/104/42 49/123/42 +f 1/1/43 51/128/43 50/124/43 +f 2/2/44 52/132/44 51/128/44 +f 3/4/45 53/135/45 52/129/45 +f 54/138/46 26/63/46 19/40/46 +f 5/10/47 55/143/47 54/136/47 +f 6/12/48 56/145/48 55/140/48 +f 56/145/49 1/1/49 50/124/49 +f 18/38/50 46/112/50 53/134/50 +f 4/7/50 18/39/50 53/133/50 +f 32/80/50 46/113/50 25/59/50 +f 26/64/46 40/98/46 33/82/46 +f 54/139/46 12/23/46 5/8/46 +f 8/14/1 9/15/1 2/2/1 +f 2/2/2 9/15/2 10/16/2 +f 10/16/3 11/19/3 4/5/3 +f 5/9/4 12/21/4 13/25/4 +f 13/25/5 14/26/5 7/13/5 +f 14/26/6 8/14/6 1/1/6 +f 8/14/7 15/28/7 16/31/7 +f 9/15/8 16/31/8 17/33/8 +f 10/17/9 17/34/9 18/36/9 +f 12/22/10 19/41/10 20/44/10 +f 20/45/11 21/47/11 14/26/11 +f 21/47/12 15/28/12 8/14/12 +f 15/29/13 22/49/13 23/53/13 +f 23/53/14 24/54/14 17/35/14 +f 24/55/15 25/60/15 18/37/15 +f 26/63/16 27/66/16 20/46/16 +f 27/67/17 28/69/17 21/48/17 +f 21/48/18 28/69/18 22/50/18 +f 22/51/19 29/71/19 30/73/19 +f 30/73/20 31/75/20 24/56/20 +f 24/57/21 31/76/21 32/78/21 +f 33/83/22 34/84/22 27/68/22 +f 34/85/23 35/87/23 28/70/23 +f 28/70/24 35/87/24 29/71/24 +f 36/89/25 37/90/25 30/74/25 +f 37/90/26 38/92/26 31/77/26 +f 38/92/27 39/93/27 32/79/27 +f 40/96/28 41/100/28 34/86/28 +f 41/100/29 42/101/29 35/88/29 +f 42/101/30 36/89/30 29/72/30 +f 43/102/31 44/105/31 37/90/31 +f 44/105/32 45/109/32 38/92/32 +f 45/110/33 46/111/33 39/94/33 +f 40/97/34 47/114/34 48/120/34 +f 48/121/35 49/122/35 42/101/35 +f 49/122/36 43/102/36 36/89/36 +f 43/103/37 50/125/37 51/127/37 +f 44/106/38 51/127/38 52/130/38 +f 52/131/39 53/134/39 46/112/39 +f 47/115/40 54/137/40 55/141/40 +f 55/142/41 56/144/41 49/123/41 +f 56/144/42 50/126/42 43/104/42 +f 1/1/43 2/2/43 51/128/43 +f 2/2/44 3/3/44 52/132/44 +f 3/4/45 4/6/45 53/135/45 +f 54/138/46 47/116/46 26/63/46 +f 5/10/47 6/11/47 55/143/47 +f 6/12/48 7/13/48 56/145/48 +f 56/145/49 7/13/49 1/1/49 +f 18/38/50 25/61/50 46/112/50 +f 4/7/50 11/20/50 18/39/50 +f 32/80/50 39/95/50 46/113/50 +f 26/64/46 47/117/46 40/98/46 +f 54/139/46 19/42/46 12/23/46 diff --git a/resources/models/gun_platform.obj b/resources/models/gun_platform.obj new file mode 100755 index 0000000..ec0828f --- /dev/null +++ b/resources/models/gun_platform.obj @@ -0,0 +1,790 @@ +# Blender 3.3.1 +# www.blender.org +o gun_platform +v -0.000000 1.237018 1.326735 +v 0.399193 0.705064 1.565705 +v 0.611688 1.073117 1.326735 +v 0.685541 1.201033 0.593671 +v 0.742529 1.299740 -0.055448 +v 0.736390 1.289108 -0.752446 +v 0.526764 0.926023 -1.171731 +v 0.365038 0.645907 -1.468565 +v 0.811132 0.481949 1.701482 +v 1.479836 0.868025 1.598586 +v 2.217168 1.293724 0.779044 +v 2.387538 1.392087 -0.157919 +v 1.899885 1.110541 -0.936381 +v 1.213173 0.714067 -1.411462 +v 0.710065 0.423598 -1.577762 +v 0.936615 0.013641 1.701482 +v 1.708767 0.013641 1.598586 +v 2.560164 0.013641 0.779044 +v 2.756892 0.013641 -0.157919 +v 2.193797 0.013641 -0.936381 +v 1.400851 0.013641 -1.411462 +v 0.819912 0.013641 -1.577762 +v 0.691422 -0.385551 1.565705 +v 1.059475 -0.598047 1.326735 +v 1.187391 -0.671899 0.593671 +v 1.286098 -0.728888 -0.055448 +v 1.275466 -0.722749 -0.752446 +v 0.912381 -0.513122 -1.171731 +v 0.632265 -0.351397 -1.468565 +v 0.399193 -0.677781 1.565705 +v 0.611688 -1.045834 1.326735 +v 0.685541 -1.173750 0.593671 +v 0.742529 -1.272457 -0.055448 +v 0.736390 -1.261824 -0.752446 +v 0.526764 -0.898740 -1.171731 +v 0.365038 -0.618624 -1.468565 +v -0.000000 -0.922974 1.701482 +v -0.000000 -1.695126 1.598586 +v 0.000000 -2.546523 0.779044 +v 0.000000 -2.743250 -0.157919 +v 0.000000 -2.180156 -0.936381 +v -0.000000 -1.387210 -1.411462 +v -0.000000 -0.806271 -1.577762 +v -0.468307 -0.797491 1.701482 +v -0.854384 -1.466195 1.598586 +v -1.280082 -2.203526 0.779044 +v -1.378446 -2.373897 -0.157919 +v -1.096899 -1.886243 -0.936381 +v -0.700426 -1.199531 -1.411462 +v -0.409956 -0.696423 -1.577762 +v 0.000000 0.013641 1.755856 +v -0.691422 -0.385552 1.565705 +v -1.059475 -0.598047 1.326735 +v -1.187391 -0.671899 0.593671 +v -1.286098 -0.728888 -0.055448 +v -1.275466 -0.722749 -0.752446 +v -0.912382 -0.513122 -1.171731 +v -0.632265 -0.351397 -1.468565 +v -0.000000 0.013642 -1.780250 +v -0.798386 0.013641 1.565705 +v -1.223377 0.013642 1.326735 +v -1.371081 0.013641 0.593671 +v -1.485059 0.013641 -0.055448 +v -1.472781 0.013641 -0.752446 +v -1.053528 0.013642 -1.171731 +v -0.730077 0.013641 -1.468565 +v -0.811132 0.481949 1.701482 +v -1.479836 0.868025 1.598586 +v -2.217168 1.293724 0.779044 +v -2.387538 1.392088 -0.157919 +v -1.899884 1.110540 -0.936381 +v -1.213173 0.714067 -1.411462 +v -0.710065 0.423598 -1.577762 +v -0.468308 0.824774 1.701482 +v -0.854383 1.493478 1.598586 +v -1.280082 2.230809 0.779044 +v -1.378446 2.401180 -0.157919 +v -1.096899 1.913526 -0.936381 +v -0.700425 1.226815 -1.411462 +v -0.409956 0.723706 -1.577762 +v -0.000000 0.812027 1.565705 +v -0.000000 1.384723 0.593671 +v -0.000000 1.498700 -0.055448 +v -0.000000 1.486423 -0.752446 +v 0.000000 1.067169 -1.171731 +v -0.000000 0.743719 -1.468565 +v -0.727751 -2.702361 -0.130686 +v -0.669082 -2.483408 0.798747 +v -0.458306 -1.696779 1.675468 +v -0.247481 -0.909972 1.780015 +v -0.220099 -0.807779 -1.680988 +v -0.390740 -1.444620 -1.545736 +v -0.598140 -2.218647 -0.954071 +v 1.458261 0.404382 -1.545736 +v 2.232288 0.611781 -0.954071 +v 2.716002 0.741392 -0.130686 +v 2.497049 0.682724 0.798747 +v 1.710421 0.471947 1.675468 +v 0.923614 0.261123 1.780015 +v 0.821420 0.233740 -1.680988 +v -1.252115 1.265756 1.675468 +v -0.676132 0.689774 1.780015 +v -0.601321 0.614963 -1.680988 +v -1.067521 1.081163 -1.545736 +v -1.634148 1.647790 -0.954071 +v -1.988252 2.001894 -0.130686 +v -1.827967 1.841608 0.798747 +vn 0.1783 0.6653 -0.7250 +vn 0.2588 0.9658 -0.0170 +vn 0.2541 0.9481 0.1910 +vn 0.0620 0.2312 0.9709 +vn 0.1046 0.3905 -0.9146 +vn 0.1862 0.6948 -0.6947 +vn 0.2552 0.9523 0.1672 +vn 0.1302 0.4860 0.8642 +vn -0.1286 0.3313 0.9347 +vn 0.0007 0.4418 -0.8971 +vn 0.0041 0.7549 -0.6558 +vn -0.0499 0.9942 0.0953 +vn -0.1522 0.4920 0.8572 +vn 0.1941 0.6585 -0.7271 +vn 0.1491 0.9887 -0.0164 +vn -0.1021 0.9174 0.3847 +vn 0.4820 -0.3473 -0.8044 +vn 0.9776 0.0472 0.2053 +vn 0.1729 -0.1636 0.9713 +vn 0.2507 -0.4125 -0.8758 +vn 0.8083 -0.0693 -0.5847 +vn 0.6928 0.0442 0.7198 +vn -0.2869 0.1018 0.9525 +vn 0.2181 -0.4156 -0.8830 +vn 0.4974 -0.4732 -0.7271 +vn 0.6235 -0.7816 -0.0164 +vn 0.3703 -0.8455 0.3847 +vn 0.0543 -0.3513 0.9347 +vn 0.2216 -0.3822 -0.8971 +vn 0.3810 -0.6518 -0.6558 +vn 0.4539 -0.8859 0.0953 +vn 0.1142 -0.5022 0.8572 +vn 0.7070 -0.7070 -0.0170 +vn 0.6941 -0.6941 0.1910 +vn 0.1693 -0.1693 0.9709 +vn 0.2859 -0.2859 -0.9146 +vn 0.5086 -0.5086 -0.6947 +vn 0.6972 -0.6972 0.1672 +vn 0.3557 -0.3557 0.8642 +vn 0.4870 -0.4870 -0.7250 +vn 0.3822 -0.2216 -0.8971 +vn 0.6518 -0.3810 -0.6558 +vn 0.8859 -0.4539 0.0953 +vn 0.5022 -0.1142 0.8572 +vn 0.4732 -0.4974 -0.7271 +vn 0.7816 -0.6235 -0.0164 +vn 0.8455 -0.3703 0.3847 +vn 0.3513 -0.0543 0.9347 +vn -0.4479 -0.8702 0.2053 +vn -0.2281 -0.0679 0.9713 +vn -0.4826 -0.0109 -0.8758 +vn -0.4642 -0.6654 -0.5847 +vn -0.3082 -0.6221 0.7198 +vn -0.1976 -0.2316 0.9525 +vn -0.3967 -0.2509 -0.8830 +vn -0.5417 -0.2438 -0.8044 +vn -0.6585 -0.1941 -0.7271 +vn -0.9887 -0.1491 -0.0164 +vn -0.9174 0.1021 0.3847 +vn -0.3313 0.1286 0.9347 +vn -0.4418 -0.0007 -0.8971 +vn -0.7549 -0.0041 -0.6558 +vn -0.9942 0.0499 0.0953 +vn -0.4920 0.1522 0.8572 +vn -0.9481 -0.2541 0.1910 +vn -0.2312 -0.0620 0.9709 +vn -0.3905 -0.1046 -0.9146 +vn -0.6948 -0.1862 -0.6947 +vn -0.9523 -0.2552 0.1672 +vn -0.4860 -0.1302 0.8642 +vn -0.6653 -0.1783 -0.7250 +vn -0.9658 -0.2588 -0.0170 +vn -0.3830 -0.2203 -0.8971 +vn -0.6558 -0.3739 -0.6558 +vn -0.8361 -0.5403 0.0953 +vn -0.3500 -0.3778 0.8572 +vn -0.6673 -0.1611 -0.7271 +vn -0.9308 -0.3652 -0.0164 +vn -0.7434 -0.5471 0.3847 +vn -0.2226 -0.2771 0.9347 +vn 0.0552 0.2315 0.9713 +vn 0.2319 0.4233 -0.8758 +vn -0.3441 0.7347 -0.5847 +vn -0.3847 0.5779 0.7198 +vn 0.2316 0.1976 0.9525 +vn 0.2509 0.3967 -0.8830 +vn 0.0597 0.5910 -0.8044 +vn -0.5297 0.8230 0.2053 +vn 0.1611 0.6673 -0.7271 +vn 0.3652 0.9308 -0.0164 +vn 0.5471 0.7434 0.3847 +vn 0.2771 0.2226 0.9347 +vn 0.2203 0.3830 -0.8971 +vn 0.3739 0.6558 -0.6558 +vn 0.5403 0.8361 0.0953 +vn 0.3778 0.3500 0.8572 +vn 0.3473 -0.4820 -0.8044 +vn -0.0442 -0.6928 0.7198 +vn 0.0693 -0.8083 -0.5847 +vn 0.4125 -0.2507 -0.8758 +vn 0.1636 -0.1729 0.9713 +vn -0.0472 -0.9776 0.2053 +vn 0.6221 0.3082 0.7198 +vn 0.6654 0.4642 -0.5847 +vn 0.0109 0.4826 -0.8758 +vn 0.0679 0.2281 0.9713 +vn 0.8702 0.4479 0.2053 +vn 0.2438 0.5417 -0.8044 +vn -0.8230 0.5297 0.2053 +vn -0.5910 -0.0597 -0.8044 +vn -0.5779 0.3847 0.7198 +vn -0.7347 0.3441 -0.5847 +vn -0.4234 -0.2319 -0.8758 +vn -0.2315 -0.0552 0.9713 +vn 0.2994 -0.0553 0.9525 +vn -0.1018 0.2869 0.9525 +vn 0.0553 -0.2994 0.9525 +vn -0.0190 0.4690 -0.8830 +vn 0.4156 -0.2181 -0.8830 +vn -0.4690 0.0190 -0.8830 +vn 0.0133 0.7582 -0.6519 +vn -0.0771 0.9867 0.1433 +vn 0.0142 0.5387 0.8423 +vn -0.0888 0.6063 -0.7903 +vn -0.0720 0.9515 -0.2990 +vn 0.1704 0.9677 0.1860 +vn 0.6167 -0.0629 -0.7847 +vn 0.9687 0.0845 0.2335 +vn 0.1263 -0.2940 0.9474 +vn 0.2832 -0.3486 -0.8935 +vn 0.8556 0.0673 -0.5132 +vn 0.7529 -0.1115 0.6487 +vn 0.2262 -0.5694 -0.7903 +vn 0.4134 -0.8601 -0.2990 +vn 0.6314 -0.7528 0.1860 +vn 0.3906 -0.6499 -0.6519 +vn 0.4266 -0.8930 0.1433 +vn 0.2816 -0.4595 0.8423 +vn 0.6499 -0.3906 -0.6519 +vn 0.8930 -0.4266 0.1433 +vn 0.4595 -0.2816 0.8423 +vn 0.5694 -0.2262 -0.7903 +vn 0.8601 -0.4134 -0.2990 +vn 0.7528 -0.6314 0.1860 +vn -0.4112 -0.8811 0.2335 +vn -0.3177 0.0377 0.9474 +vn -0.4435 -0.0709 -0.8935 +vn -0.3695 -0.7746 -0.5132 +vn -0.4730 -0.5962 0.6487 +vn -0.3628 -0.5026 -0.7847 +vn -0.6063 0.0888 -0.7903 +vn -0.9515 0.0720 -0.2990 +vn -0.9677 -0.1704 0.1860 +vn -0.7582 -0.0133 -0.6519 +vn -0.9867 0.0771 0.1433 +vn -0.5387 -0.0142 0.8423 +vn -0.6632 -0.3676 -0.6519 +vn -0.8160 -0.5601 0.1433 +vn -0.4736 -0.2571 0.8423 +vn -0.4806 -0.3800 -0.7903 +vn -0.7880 -0.5382 -0.2990 +vn -0.9232 -0.3363 0.1860 +vn 0.1915 0.2563 0.9474 +vn 0.1603 0.4195 -0.8935 +vn -0.4861 0.7073 -0.5132 +vn -0.2798 0.7078 0.6487 +vn -0.2539 0.5655 -0.7847 +vn -0.5575 0.7966 0.2335 +vn 0.3800 0.4806 -0.7903 +vn 0.5382 0.7880 -0.2990 +vn 0.3363 0.9232 0.1860 +vn 0.3676 0.6632 -0.6519 +vn 0.5601 0.8160 0.1433 +vn 0.2571 0.4736 0.8423 +vn 0.0629 -0.6167 -0.7847 +vn 0.1115 -0.7529 0.6487 +vn -0.0673 -0.8556 -0.5132 +vn 0.3486 -0.2832 -0.8935 +vn 0.2940 -0.1263 0.9474 +vn -0.0845 -0.9687 0.2335 +vn 0.5962 0.4730 0.6487 +vn 0.7746 0.3695 -0.5132 +vn 0.0709 0.4435 -0.8935 +vn -0.0377 0.3177 0.9474 +vn 0.8811 0.4112 0.2335 +vn 0.5026 0.3628 -0.7847 +vn -0.7966 0.5575 0.2335 +vn -0.5655 0.2539 -0.7847 +vn -0.7078 0.2798 0.6487 +vn -0.7073 0.4861 -0.5132 +vn -0.4195 -0.1603 -0.8935 +vn -0.2563 -0.1915 0.9474 +vt 0.801885 0.640663 +vt 0.623697 0.208166 +vt 0.168225 0.628737 +vt 0.641035 0.203983 +vt 0.640072 0.217575 +vt 0.802752 0.659976 +vt 0.252347 0.237069 +vt 0.779414 0.661823 +vt 0.231744 0.238688 +vt 0.758811 0.663442 +vt 0.737191 0.665041 +vt 0.256630 0.635588 +vt 0.203516 0.683333 +vt 0.206117 0.698451 +vt 0.717973 0.211375 +vt 0.245163 0.636129 +vt 0.678927 0.273697 +vt 0.207572 0.709892 +vt 0.680382 0.285138 +vt 0.709955 0.211731 +vt 0.180314 0.632136 +vt 0.653124 0.207382 +vt 0.812312 0.686351 +vt 0.667842 0.685014 +vt 0.660091 0.230807 +vt 0.785113 0.702325 +vt 0.682386 0.712787 +vt 0.258046 0.277571 +vt 0.228523 0.282992 +vt 0.755589 0.707746 +vt 0.233981 0.686444 +vt 0.732532 0.700236 +vt 0.272752 0.714810 +vt 0.256527 0.737234 +vt 0.226462 0.704592 +vt 0.699271 0.279839 +vt 0.217193 0.716367 +vt 0.690003 0.291614 +vt 0.192852 0.624180 +vt 0.665661 0.199426 +vt 0.682965 0.216292 +vt 0.690716 0.670499 +vt 0.716656 0.691040 +vt 0.263814 0.305943 +vt 0.152836 0.372468 +vt 0.234734 0.313544 +vt 0.178507 0.364518 +vt 0.802073 0.754939 +vt 0.776804 0.764761 +vt 0.296243 0.735307 +vt 0.744337 0.325569 +vt 0.271527 0.750323 +vt 0.256599 0.761670 +vt 0.729409 0.336917 +vt 0.197423 0.610209 +vt 0.670232 0.185455 +vt 0.692563 0.643392 +vt 0.684812 0.189185 +vt 0.703947 0.646478 +vt 0.160034 0.343194 +vt 0.176880 0.221724 +vt 0.178507 0.337417 +vt 0.785226 0.802538 +vt 0.186475 0.224335 +vt 0.772713 0.801274 +vt 0.292153 0.771820 +vt 0.273978 0.773266 +vt 0.746788 0.348512 +vt 0.260422 0.774432 +vt 0.733232 0.349678 +vt 0.197423 0.597287 +vt 0.670232 0.172533 +vt 0.684812 0.169384 +vt 0.692563 0.623591 +vt 0.176880 0.199533 +vt 0.264155 0.401788 +vt 0.703947 0.624286 +vt 0.186475 0.200299 +vt 0.255543 0.401781 +vt 0.782610 0.826535 +vt 0.770118 0.825072 +vt 0.289557 0.795618 +vt 0.272121 0.790289 +vt 0.744931 0.365536 +vt 0.259136 0.786229 +vt 0.731946 0.361475 +vt 0.192852 0.582764 +vt 0.665661 0.158010 +vt 0.690716 0.594940 +vt 0.888922 0.773251 +vt 0.682965 0.140732 +vt 0.716656 0.577833 +vt 0.862686 0.749699 +vt 0.275007 0.448945 +vt 0.335620 0.324946 +vt 0.261735 0.451892 +vt 0.308327 0.313544 +vt 0.788802 0.876646 +vt 0.766243 0.861609 +vt 0.285682 0.832155 +vt 0.264783 0.812165 +vt 0.737593 0.387411 +vt 0.252652 0.797867 +vt 0.725462 0.373113 +vt 0.180314 0.575560 +vt 0.653124 0.150806 +vt 0.660091 0.127590 +vt 0.878662 0.797365 +vt 0.847314 0.785828 +vt 0.320247 0.361075 +vt 0.152836 0.306087 +vt 0.291773 0.352449 +vt 0.137779 0.291149 +vt 0.818840 0.777203 +vt 0.664846 0.715903 +vt 0.802073 0.774306 +vt 0.639773 0.722404 +vt 0.159213 0.692951 +vt 0.608953 0.283861 +vt 0.136143 0.708614 +vt 0.122822 0.721995 +vt 0.595632 0.297241 +vt 0.172547 0.603915 +vt 0.168225 0.580511 +vt 0.641035 0.155757 +vt 0.871308 0.817241 +vt 0.640072 0.143678 +vt 0.848578 0.812846 +vt 0.137301 0.349542 +vt 0.321512 0.388092 +vt 0.301602 0.384035 +vt 0.654272 0.765728 +vt 0.127205 0.340974 +vt 0.828669 0.808789 +vt 0.640251 0.758615 +vt 0.809034 0.803112 +vt 0.159690 0.729162 +vt 0.141528 0.731266 +vt 0.614338 0.306512 +vt 0.128040 0.733168 +vt 0.600850 0.308414 +vt 0.331751 0.581324 +vt 0.203444 0.732717 +vt 0.236095 0.777748 +vt 0.104656 0.741216 +vt 0.157538 0.587293 +vt 0.630348 0.162539 +vt 0.875851 0.371194 +vt 0.623697 0.154070 +vt 0.863565 0.379008 +vt 0.871316 0.833215 +vt 0.321520 0.405995 +vt 0.848586 0.830749 +vt 0.301611 0.403427 +vt 0.828678 0.828181 +vt 0.809043 0.822344 +vt 0.159690 0.750792 +vt 0.141528 0.746739 +vt 0.614338 0.321985 +vt 0.128040 0.743891 +vt 0.600850 0.319137 +vt 0.146062 0.597296 +vt 0.881171 0.381929 +vt 0.618871 0.172542 +vt 0.870939 0.404118 +vt 0.878690 0.858325 +vt 0.605351 0.621452 +vt 0.597600 0.167245 +vt 0.847355 0.877162 +vt 0.588759 0.617555 +vt 0.320289 0.452408 +vt 0.152836 0.408814 +vt 0.291818 0.450801 +vt 0.137779 0.401770 +vt 0.818885 0.875555 +vt 0.664846 0.826523 +vt 0.639773 0.810431 +vt 0.802109 0.852569 +vt 0.159213 0.780977 +vt 0.136143 0.764824 +vt 0.608953 0.340070 +vt 0.122822 0.754894 +vt 0.595632 0.330140 +vt 0.146062 0.612455 +vt 0.618871 0.187701 +vt 0.597600 0.194902 +vt 0.809000 0.612652 +vt 0.780151 0.591906 +vt 0.253084 0.167152 +vt 0.223180 0.164088 +vt 0.634625 0.857200 +vt 0.750247 0.588841 +vt 0.615725 0.834842 +vt 0.135164 0.805388 +vt 0.728281 0.605618 +vt 0.247720 0.576164 +vt 0.120787 0.780412 +vt 0.825202 0.133613 +vt 0.236095 0.598351 +vt 0.343914 0.567667 +vt 0.816724 0.142913 +vt 0.157538 0.622597 +vt 0.630348 0.197843 +vt 0.778441 0.640178 +vt 0.251375 0.215425 +vt 0.757757 0.639997 +vt 0.230691 0.215244 +vt 0.736146 0.641791 +vt 0.255585 0.612338 +vt 0.244416 0.619498 +vt 0.717226 0.194744 +vt 0.829506 0.156571 +vt 0.709437 0.200206 +vt 0.199342 0.708141 +vt 0.822519 0.154755 +vt 0.349710 0.579508 +vt 0.826891 0.755974 +vt 0.773574 0.890327 +vt 0.299825 0.331221 +vt 0.854807 0.766419 +vt 0.327740 0.341665 +vt 0.672576 0.130640 +vt 0.885169 0.784311 +vt 0.186589 0.577528 +vt 0.659398 0.152774 +vt 0.246317 0.802126 +vt 0.117009 0.717772 +vt 0.589818 0.293018 +vt 0.719127 0.377372 +vt 0.600544 0.275452 +vt 0.127734 0.700206 +vt 0.255262 0.821137 +vt 0.728072 0.396383 +vt 0.806869 0.756115 +vt 0.149187 0.683333 +vt 0.273610 0.845058 +vt 0.754171 0.874512 +vt 0.736973 0.314756 +vt 0.706791 0.289480 +vt 0.264163 0.739510 +vt 0.767796 0.749559 +vt 0.734275 0.716820 +vt 0.287236 0.720105 +vt 0.790153 0.738298 +vt 0.759403 0.726361 +vt 0.232337 0.301607 +vt 0.701005 0.704847 +vt 0.261438 0.294276 +vt 0.672576 0.226539 +vt 0.680327 0.680747 +vt 0.186589 0.629313 +vt 0.659398 0.204559 +vt 0.251331 0.756146 +vt 0.221325 0.722580 +vt 0.724141 0.331392 +vt 0.694135 0.297826 +vt 0.593264 0.180969 +vt 0.812312 0.604852 +vt 0.601015 0.635177 +vt 0.143761 0.604706 +vt 0.616571 0.179952 +vt 0.117009 0.759563 +vt 0.338823 0.562225 +vt 0.811633 0.137471 +vt 0.589818 0.334809 +vt 0.816760 0.123625 +vt 0.127734 0.774398 +vt 0.600544 0.349644 +vt 0.149187 0.796905 +vt 0.629748 0.826358 +vt 0.225663 0.153079 +vt 0.125025 0.420423 +vt 0.652092 0.845177 +vt 0.782369 0.582475 +vt 0.255302 0.157722 +vt 0.140564 0.425548 +vt 0.585217 0.638320 +s 0 +f 85/211/1 8/20/1 86/213/1 +f 83/206/2 6/11/2 84/208/2 +f 1/1/3 4/8/3 82/204/3 +f 81/202/4 51/123/4 2/3/4 +f 59/143/5 86/214/5 8/18/5 +f 84/209/6 7/16/6 85/210/6 +f 82/205/7 5/9/7 83/207/7 +f 81/203/8 3/5/8 1/2/8 +f 2/3/9 51/123/9 9/21/9 +f 59/143/10 8/18/10 15/37/10 +f 6/13/11 14/35/11 7/14/11 +f 5/9/12 11/28/12 12/29/12 +f 3/5/13 9/22/13 10/25/13 +f 7/17/14 15/38/14 8/19/14 +f 5/10/15 13/32/15 6/11/15 +f 4/8/16 10/23/16 11/26/16 +f 94/240/17 20/50/17 21/52/17 +f 96/246/18 18/44/18 19/46/18 +f 99/252/19 17/41/19 98/249/19 +f 100/255/20 21/51/20 22/54/20 +f 95/241/21 19/48/21 20/49/21 +f 97/247/22 17/42/22 18/43/22 +f 44/105/23 90/224/23 51/123/23 +f 59/144/24 100/253/24 22/53/24 +f 22/54/25 28/68/25 29/70/25 +f 20/49/26 26/63/26 27/65/26 +f 17/42/27 25/59/27 18/43/27 +f 16/39/28 51/123/28 23/55/28 +f 59/144/29 22/53/29 29/69/29 +f 21/52/30 27/66/30 28/67/30 +f 18/45/31 26/62/31 19/47/31 +f 16/40/32 24/58/32 17/41/32 +f 26/63/33 34/81/33 27/65/33 +f 24/57/34 32/77/34 25/59/34 +f 23/55/35 51/123/35 30/71/35 +f 59/144/36 29/69/36 36/85/36 +f 28/67/37 34/82/37 35/83/37 +f 26/64/38 32/75/38 33/78/38 +f 24/58/39 30/72/39 31/73/39 +f 28/68/40 36/86/40 29/70/40 +f 59/144/41 36/85/41 43/103/41 +f 34/82/42 42/101/42 35/83/42 +f 33/79/43 39/94/43 40/96/43 +f 31/73/44 37/88/44 38/91/44 +f 35/84/45 43/104/45 36/86/45 +f 33/80/46 41/99/46 34/81/46 +f 32/77/47 38/89/47 39/92/47 +f 30/71/48 51/123/48 37/87/48 +f 87/219/49 46/110/49 47/112/49 +f 90/225/50 45/107/50 89/222/50 +f 91/228/51 49/119/51 50/122/51 +f 93/234/52 47/114/52 48/116/52 +f 88/220/53 45/108/53 46/109/53 +f 67/162/54 51/123/54 102/260/54 +f 103/262/55 59/145/55 73/182/55 +f 92/231/56 48/118/56 49/120/56 +f 50/122/57 57/139/57 58/141/57 +f 48/117/58 55/132/58 56/135/58 +f 45/108/59 54/128/59 46/109/59 +f 44/105/60 51/123/60 52/124/60 +f 59/145/61 50/121/61 58/140/61 +f 49/120/62 56/137/62 57/138/62 +f 46/111/63 55/133/63 47/113/63 +f 44/106/64 53/127/64 45/107/64 +f 53/126/65 62/153/65 54/128/65 +f 52/124/66 51/123/66 60/146/66 +f 59/145/67 58/140/67 66/160/67 +f 57/138/68 64/157/68 65/158/68 +f 54/130/69 63/154/69 55/131/69 +f 53/127/70 60/147/70 61/149/70 +f 57/139/71 66/161/71 58/141/71 +f 55/134/72 64/156/72 56/136/72 +f 59/145/73 66/160/73 73/182/73 +f 64/157/74 72/180/74 65/158/74 +f 63/154/75 69/171/75 70/173/75 +f 61/150/76 67/163/76 68/165/76 +f 65/159/77 73/183/77 66/161/77 +f 63/155/78 71/178/78 64/156/78 +f 62/153/79 68/166/79 69/169/79 +f 60/146/80 51/123/80 67/162/80 +f 102/261/81 75/186/81 101/257/81 +f 103/264/82 79/198/82 80/201/82 +f 105/270/83 77/191/83 78/193/83 +f 107/274/84 75/187/84 76/188/84 +f 102/260/85 51/123/85 74/184/85 +f 59/142/86 103/263/86 80/200/86 +f 104/267/87 78/194/87 79/197/87 +f 106/271/88 76/189/88 77/190/88 +f 80/201/89 85/212/89 86/215/89 +f 78/195/90 83/206/90 84/208/90 +f 75/187/91 82/204/91 76/188/91 +f 74/184/92 51/123/92 81/202/92 +f 59/142/93 80/200/93 86/216/93 +f 79/199/94 84/209/94 85/210/94 +f 76/189/95 83/207/95 77/190/95 +f 74/185/96 1/2/96 75/186/96 +f 41/100/97 92/232/97 42/101/97 +f 38/90/98 88/220/98 39/93/98 +f 40/98/99 93/237/99 41/99/99 +f 42/102/100 91/229/100 43/104/100 +f 38/91/101 90/225/101 89/222/101 +f 39/95/102 87/219/102 40/97/102 +f 10/24/103 97/247/103 11/27/103 +f 12/30/104 95/242/104 13/32/104 +f 14/36/105 100/256/105 15/38/105 +f 10/25/106 99/252/106 98/249/106 +f 11/28/107 96/246/107 12/29/107 +f 13/33/108 94/240/108 14/34/108 +f 69/172/109 106/272/109 70/174/109 +f 71/179/110 104/267/110 72/180/110 +f 68/167/111 107/277/111 69/170/111 +f 70/176/112 105/270/112 71/177/112 +f 72/181/113 103/265/113 73/183/113 +f 68/168/114 102/261/114 101/257/114 +f 51/123/115 90/224/115 37/87/115 +f 9/21/116 51/123/116 99/251/116 +f 99/251/117 51/123/117 16/39/117 +f 100/254/118 59/143/118 15/37/118 +f 43/103/119 91/226/119 59/144/119 +f 59/145/120 91/227/120 50/121/120 +f 85/211/1 7/15/1 8/20/1 +f 83/206/2 5/10/2 6/11/2 +f 1/1/3 3/6/3 4/8/3 +f 84/209/6 6/12/6 7/16/6 +f 82/205/7 4/7/7 5/9/7 +f 81/203/8 2/4/8 3/5/8 +f 6/13/121 13/31/121 14/35/121 +f 5/9/122 4/7/122 11/28/122 +f 3/5/123 2/4/123 9/22/123 +f 7/17/124 14/36/124 15/38/124 +f 5/10/125 12/30/125 13/32/125 +f 4/8/126 3/6/126 10/23/126 +f 94/240/127 95/243/127 20/50/127 +f 96/246/128 97/248/128 18/44/128 +f 99/252/129 16/40/129 17/41/129 +f 100/255/130 94/238/130 21/51/130 +f 95/241/131 96/244/131 19/48/131 +f 97/247/132 98/250/132 17/42/132 +f 22/54/133 21/51/133 28/68/133 +f 20/49/134 19/48/134 26/63/134 +f 17/42/135 24/57/135 25/59/135 +f 21/52/136 20/50/136 27/66/136 +f 18/45/137 25/60/137 26/62/137 +f 16/40/138 23/56/138 24/58/138 +f 26/63/33 33/80/33 34/81/33 +f 24/57/34 31/74/34 32/77/34 +f 28/67/37 27/66/37 34/82/37 +f 26/64/38 25/61/38 32/75/38 +f 24/58/39 23/56/39 30/72/39 +f 28/68/40 35/84/40 36/86/40 +f 34/82/139 41/100/139 42/101/139 +f 33/79/140 32/76/140 39/94/140 +f 31/73/141 30/72/141 37/88/141 +f 35/84/142 42/102/142 43/104/142 +f 33/80/143 40/98/143 41/99/143 +f 32/77/144 31/74/144 38/89/144 +f 87/219/145 88/221/145 46/110/145 +f 90/225/146 44/106/146 45/107/146 +f 91/228/147 92/230/147 49/119/147 +f 93/234/148 87/217/148 47/114/148 +f 88/220/149 89/223/149 45/108/149 +f 92/231/150 93/235/150 48/118/150 +f 50/122/151 49/119/151 57/139/151 +f 48/117/152 47/115/152 55/132/152 +f 45/108/153 53/126/153 54/128/153 +f 49/120/154 48/118/154 56/137/154 +f 46/111/155 54/129/155 55/133/155 +f 44/106/156 52/125/156 53/127/156 +f 53/126/65 61/151/65 62/153/65 +f 57/138/68 56/137/68 64/157/68 +f 54/130/69 62/152/69 63/154/69 +f 53/127/70 52/125/70 60/147/70 +f 57/139/71 65/159/71 66/161/71 +f 55/134/72 63/155/72 64/156/72 +f 64/157/157 71/179/157 72/180/157 +f 63/154/158 62/152/158 69/171/158 +f 61/150/159 60/148/159 67/163/159 +f 65/159/160 72/181/160 73/183/160 +f 63/155/161 70/175/161 71/178/161 +f 62/153/162 61/151/162 68/166/162 +f 102/261/163 74/185/163 75/186/163 +f 103/264/164 104/266/164 79/198/164 +f 105/270/165 106/273/165 77/191/165 +f 107/274/166 101/258/166 75/187/166 +f 104/267/167 105/269/167 78/194/167 +f 106/271/168 107/275/168 76/189/168 +f 80/201/169 79/198/169 85/212/169 +f 78/195/170 77/192/170 83/206/170 +f 75/187/171 1/1/171 82/204/171 +f 79/199/172 78/196/172 84/209/172 +f 76/189/173 82/205/173 83/207/173 +f 74/185/174 81/203/174 1/2/174 +f 41/100/175 93/236/175 92/232/175 +f 38/90/176 89/223/176 88/220/176 +f 40/98/177 87/218/177 93/237/177 +f 42/102/178 92/233/178 91/229/178 +f 38/91/179 37/88/179 90/225/179 +f 39/95/180 88/221/180 87/219/180 +f 10/24/181 98/250/181 97/247/181 +f 12/30/182 96/245/182 95/242/182 +f 14/36/183 94/239/183 100/256/183 +f 10/25/184 9/22/184 99/252/184 +f 11/28/185 97/248/185 96/246/185 +f 13/33/186 95/243/186 94/240/186 +f 69/172/187 107/276/187 106/272/187 +f 71/179/188 105/269/188 104/267/188 +f 68/167/189 101/259/189 107/277/189 +f 70/176/190 106/273/190 105/270/190 +f 72/181/191 104/268/191 103/265/191 +f 68/168/192 67/164/192 102/261/192 diff --git a/resources/textures/emp_mine.png b/resources/textures/emp_mine.png new file mode 100755 index 0000000..e7bd5f1 Binary files /dev/null and b/resources/textures/emp_mine.png differ diff --git a/resources/textures/gun_platform.png b/resources/textures/gun_platform.png new file mode 100755 index 0000000..0fe4cac Binary files /dev/null and b/resources/textures/gun_platform.png differ diff --git a/server/friends.ts b/server/friends.ts index 43040b0..42d2a06 100644 --- a/server/friends.ts +++ b/server/friends.ts @@ -5,6 +5,7 @@ import { findPlayer, flashServerMessage, setMissionTargetForId } from "./stateHe import { idToWebsocket } from "./state"; import { MissionType, Player, SectorKind } from "../src/game"; import { Mission } from "./missions"; +import { playerSectors } from "./peers"; const Schema = mongoose.Schema; @@ -226,25 +227,17 @@ const friendWarp = async (ws: WebSocket, player: Player, friend: number) => { ws.send({ type: "error", payload: { message: "Failed to warp to friend (not friends)" } }); throw new Error("Failed to warp to friend (not friends)"); } - const where = findPlayer(friend); + const where = playerSectors.get(friend); if (!where) { flashServerMessage(player.id, "Failed to warp to friend (not online)", [1.0, 0.0, 0.0, 1.0]); return; } - if (where === "respawning") { - flashServerMessage(player.id, "Failed to warp to friend (respawning)", [1.0, 0.0, 0.0, 1.0]); - return; - } - if ((where as any).sectorKind === SectorKind.Tutorial) { - flashServerMessage(player.id, "Failed to warp to friend (cannot warp into tutorial)", [1.0, 0.0, 0.0, 1.0]); - return; - } player.warping = 1; - player.warpTo = where.sectorNumber; + player.warpTo = where; // Add the player to the mission if ((where as any).sectorKind === SectorKind.Mission) { const mission = await Mission.findOneAndUpdate( - { sectorNumber: where.sectorNumber, inProgress: true, assignee: { $ne: player.id }, forFaction: player.team }, + { sectorNumber: where, inProgress: true, assignee: { $ne: player.id }, forFaction: player.team }, { $addToSet: { coAssignees: player.id } } ); if (!mission) { diff --git a/server/initMap.ts b/server/initMap.ts new file mode 100644 index 0000000..5bf5b61 --- /dev/null +++ b/server/initMap.ts @@ -0,0 +1,67 @@ +// Standalone tool for initializing the map and the stations in the database + +import mongoose from "mongoose"; +import { Faction } from "../src/defs"; +import { asteroidDefMap, initAsteroids } from "../src/defs/asteroids"; +import { mapHeight, mapWidth, ResourceDensity } from "../src/mapLayout"; +import { Sector } from "./sector"; + +initAsteroids(); + +const resourceKinds = Array.from(asteroidDefMap.keys()); + +const randomResources = () => { + const acm: ResourceDensity[] = []; + const kinds = [...resourceKinds]; + while (kinds.length > 0) { + const kind = kinds.pop()!; + if (Math.random() < 0.5) { + continue; + } + const density = Math.random() * 2 + 1; + acm.push({ resource: kind, density }); + } + if (acm.length === 0) { + return randomResources(); + } + return acm; +}; + +const sectorCount = mapWidth * mapHeight; + +const factionLookup = new Array(sectorCount).fill(Faction.Alliance); + +factionLookup[0] = Faction.Scourge; +factionLookup[5] = Faction.Scourge; +factionLookup[12] = Faction.Scourge; +factionLookup[17] = Faction.Scourge; + +factionLookup[2] = Faction.Rogue; +factionLookup[3] = Faction.Rogue; +factionLookup[14] = Faction.Rogue; +factionLookup[15] = Faction.Rogue; + +factionLookup[4] = Faction.Confederation; +factionLookup[9] = Faction.Confederation; +factionLookup[10] = Faction.Confederation; +factionLookup[11] = Faction.Confederation; +factionLookup[16] = Faction.Confederation; + +mongoose + .connect("mongodb://127.0.0.1:27017/SpaceGame", {}) + .catch((err) => { + console.log("Error connecting to database: " + err); + }) + .then(async () => { + for (let i = 0; i < sectorCount; i++) { + const sector = new Sector({ + id: i, + resources: randomResources(), + asteroidCount: Math.floor(Math.random() * 30) + 5, + faction: factionLookup[i], + guardianCount: 5, + }); + await sector.save(); + } + process.exit(0); + }); diff --git a/server/killPeers.sh b/server/killPeers.sh new file mode 100755 index 0000000..7fb33e1 --- /dev/null +++ b/server/killPeers.sh @@ -0,0 +1,3 @@ +#! /bin/bash + +ps -e | grep node | awk '{ print $1 }' | xargs kill -9 diff --git a/server/misc.ts b/server/misc.ts new file mode 100644 index 0000000..455a1c1 --- /dev/null +++ b/server/misc.ts @@ -0,0 +1,154 @@ +import { clientUid, defMap, defs, emptyLoadout, Faction } from "../src/defs"; +import { effectiveInfinity, Player, SectorInfo, SectorKind, TargetKind, TutorialStage } from "../src/game"; +import { clients, knownRecipes, secondaries, secondariesToActivate, sectorAsteroidResources, sectors, targets } from "./state"; +import { WebSocket } from "ws"; +import { sendInventory } from "./inventory"; +import { sendTutorialStage } from "./tutorial"; +import { Station } from "./dataModels"; +import { makeNetworkAware, removeNetworkAwareness, setPlayerSector } from "./peers"; +import { createIsolatedSector, removeContiguousSubgraph } from "../src/sectorGraph"; +import { mapGraph } from "../src/mapLayout"; + +const setupPlayer = (id: number, ws: WebSocket, name: string, faction: Faction) => { + let defIndex: number; + if (faction === Faction.Alliance) { + // defIndex = defMap.get("Fighter")!.index; + defIndex = defMap.get("Advanced Fighter")!.index; + } else if (faction === Faction.Confederation) { + // defIndex = defMap.get("Drone")!.index; + defIndex = defMap.get("Seeker")!.index; + } else { + console.log(`Invalid faction ${faction}`); + return; + } + + const sectorToWarpTo = faction === Faction.Alliance ? 12 : 15; + + // let tutorialSector = clientUid(); + // while (sectors.has(tutorialSector)) { + // tutorialSector = clientUid(); + // } + let tutorialSector = 111222; + + clients.set(ws, { + id: id, + name, + input: { up: false, down: false, primary: false, secondary: false, right: false, left: false }, + angle: 0, + currentSector: tutorialSector, + lastMessage: "", + lastMessageTime: Date.now(), + sectorsVisited: new Set(), + inTutorial: TutorialStage.Move, + }); + + let player = { + position: { x: 0, y: 0 }, + radius: defs[defIndex].radius, + speed: 0, + heading: 0, + health: defs[defIndex].health, + id: id, + sinceLastShot: [effectiveInfinity], + energy: defs[defIndex].energy, + defIndex: defIndex, + arms: emptyLoadout(defIndex), + slotData: new Array(defs[defIndex].slots.length).fill({}), + cargo: [], + credits: 500, + team: faction, + side: 0, + isPC: true, + v: { x: 0, y: 0 }, + iv: { x: 0, y: 0 }, + ir: 0, + }; + + // player = equip(player, 0, "Basic Mining Laser", true); + // player = equip(player, 1, "Laser Beam", true); + // player = equip(player, 1, "Tomahawk Missile", true); + + const state = { + players: new Map(), + projectiles: new Map(), + asteroids: new Map(), + missiles: new Map(), + collectables: new Map(), + asteroidsDirty: false, + mines: new Map(), + projectileId: 1, + delayedActions: [], + sectorKind: SectorKind.Tutorial, + sectorChecks: [], + dynamic: true, + creationTime: Date.now(), + }; + + makeNetworkAware(tutorialSector, SectorKind.Tutorial); + // I don't need to add topology for single isolated sectors (will want to though if I go to torus wrapping single sectors) + // createIsolatedSector(mapGraph, tutorialSector); + sectors.set(tutorialSector, state); + state.players.set(id, player); + setPlayerSector(id, tutorialSector); + + targets.set(id, [TargetKind.None, 0]); + secondaries.set(id, 0); + secondariesToActivate.set(id, []); + knownRecipes.set(id, new Set()); + + const sectorInfos: SectorInfo[] = []; + sectorInfos.push({ + sector: sectorToWarpTo, + resources: sectorAsteroidResources[sectorToWarpTo].map((value) => value.resource), + }); + + ws.send( + JSON.stringify({ + type: "init", + payload: { + id: id, + sector: tutorialSector, + faction, + asteroids: Array.from(state.asteroids.values()), + collectables: Array.from(state.collectables.values()), + mines: Array.from(state.mines.values()), + sectorInfos, + recipes: [], + }, + }) + ); + sendInventory(ws, id); + sendTutorialStage(ws, TutorialStage.Move); +}; + +// This is for loading the stations from the database on server startup +const initFromDatabase = async () => { + const stations = await Station.find({}); + for (const station of stations) { + const def = defs[station.definitionIndex]; + const player: Player = { + position: station.position, + radius: def.radius, + speed: 0, + heading: 0, + health: def.health, + id: station.id, + sinceLastShot: [effectiveInfinity, effectiveInfinity, effectiveInfinity, effectiveInfinity], + energy: def.energy, + defIndex: station.definitionIndex, + arms: [], + slotData: [], + team: station.team, + side: 0, + v: { x: 0, y: 0 }, + iv: { x: 0, y: 0 }, + ir: 0, + }; + const sector = sectors.get(station.sector); + if (sector) { + sector.players.set(station.id, player); + } + } +}; + +export { setupPlayer, initFromDatabase }; diff --git a/server/missions.ts b/server/missions.ts index 3acdcd9..a59e85e 100644 --- a/server/missions.ts +++ b/server/missions.ts @@ -7,19 +7,21 @@ Some words on how missions work: - Once the player wants to start a mission the sector for the mission is created and added to the sector list for running the game update loop. - At that point the mission is marked as in progress in the database. - Upon mission completion the reward is given out and the mission is marked as completed in the database. - - There is a cleanup timeout that periodically checks to see if the mission sector can be removed from the sector list. - - If the mission sector is no longer accessible then it is removed from the sector list and the mission is marked as failed in the database if it was not completed. */ import { clientUid, Faction, randomDifferentFaction } from "../src/defs"; import { GlobalState, MissionType, Player, SectorKind } from "../src/game"; import mongoose, { HydratedDocument } from "mongoose"; -import { getPlayerFromId, sectors, sectorTriggers, uid } from "./state"; +import { getPlayerFromId, sectors, uid } from "./state"; import { WebSocket } from "ws"; import { enemyCountState, flashServerMessage, sendMissionComplete, setMissionTargetForId } from "./stateHelpers"; import { clearanceNPCsRewards, randomClearanceShip, spawnClearanceNPCs } from "./npcs/clearance"; import { spawnAssassinationNPC } from "./npcs/assassination"; +import { awareSectors, makeNetworkAware, removeNetworkAwareness } from "./peers"; +import { createIsolatedSector, removeContiguousSubgraph } from "../src/sectorGraph"; +import { mapGraph } from "../src/mapLayout"; +import { transferableActionsMap } from "./transferableActions"; const Schema = mongoose.Schema; @@ -124,32 +126,29 @@ const genMissions = async (assignee: number, forFaction: Faction, count: number, return missions; }; -const missionSectorCleanupInterval = 1000 * 60 * 60 * 3; // 3 hours +// const removeMissionSector = (sectorId: number, missionId: number) => { +// const sectorNonNPCCount = Array.from(sectors.get(sectorId)?.players.values() || []).filter((p) => p.isPC).length; +// if (sectorNonNPCCount === 0) { +// sectors.delete(sectorId); +// removeNetworkAwareness(sectorId); +// // removeContiguousSubgraph(mapGraph, sectorId); +// failMissionIfIncomplete(missionId); +// } else { +// setTimeout(() => { +// removeMissionSector(sectorId, missionId); +// }, missionSectorCleanupInterval); +// } +// }; -const removeMissionSector = (sectorId: number, missionId: number) => { - const sectorNonNPCCount = Array.from(sectors.get(sectorId)?.players.values() || []).filter((p) => p.isPC).length; - if (sectorNonNPCCount === 0) { - sectors.delete(sectorId); - sectorTriggers.delete(sectorId); - failMissionIfIncomplete(missionId); - } else { - setTimeout(() => { - removeMissionSector(sectorId, missionId); - }, missionSectorCleanupInterval); - } -}; +// const setupMissionSectorCleanup = (missionId: number, missionSector: number) => { +// setTimeout(() => { +// removeMissionSector(missionSector, missionId); +// }, missionSectorCleanupInterval); -const setupMissionSectorCleanup = (missionId: number, missionSector: number) => { - setTimeout(() => { - removeMissionSector(missionSector, missionId); - }, missionSectorCleanupInterval); - - return missionSector; -}; +// return missionSector; +// }; const startMissionGameState = (player: Player, mission: HydratedDocument, missionSectorId: number) => { - const missionSector = setupMissionSectorCleanup(mission.id, missionSectorId); - const state = { players: new Map(), projectiles: new Map(), @@ -161,17 +160,19 @@ const startMissionGameState = (player: Player, mission: HydratedDocument { - if (enemyCountState(mission.forFaction, state) === 0) { - completeMission(mission.id); - sectorTriggers.delete(missionSector); - } - }); + state.sectorChecks!.push({ index: transferableActionsMap.get("clearance")!, data: { missionId: mission.id, forFaction: mission.forFaction } }); } else if (mission.type === MissionType.Assassination) { if (!mission.targetId) { console.log("Target ID missing for assassination mission"); @@ -180,19 +181,14 @@ const startMissionGameState = (player: Player, mission: HydratedDocument { - if (!state.players.has(mission.targetId!)) { - completeMission(mission.id); - sectorTriggers.delete(missionSector); - } - }); + state.sectorChecks!.push({ index: transferableActionsMap.get("assassination")!, data: { missionId: mission.id, targetId: mission.targetId } }); } else { console.log("Unsupported mission type: " + mission.type); return; } player.warping = 1; - player.warpTo = missionSector; + player.warpTo = missionSectorId; }; const startPlayerInMission = (ws: WebSocket, player: Player, id: number) => { @@ -331,4 +327,4 @@ const failMissionIfIncomplete = (id: number) => { ); }; -export { Mission, genMissions, MissionType, startPlayerInMission, selectMission }; +export { Mission, genMissions, MissionType, startPlayerInMission, selectMission, completeMission }; diff --git a/server/npcs/assassination.ts b/server/npcs/assassination.ts index 1d58084..f700abb 100644 --- a/server/npcs/assassination.ts +++ b/server/npcs/assassination.ts @@ -7,6 +7,7 @@ import { idleState, LootTable, NPC, + npcReconstructors, passiveGoToRandomPointInSector, randomCombatManeuver, runAway, @@ -14,7 +15,8 @@ import { State, strafingSwarmCombat, stupidSwarmCombat, -} from "../../src/npc"; +} from "./npc"; +import { sfc32 } from "../../src/prng"; import { uid } from "../state"; const makeStateGraph = ( @@ -141,64 +143,84 @@ class CloakyAnnoying implements NPC { public targetId: number; - constructor(ship: string, team: Faction, id: number) { + constructor(ship: string | Player, team?: Faction, id?: number) { this.lootTable = new LootTable(); - const { def, index } = defMap.get(ship)!; - - assert(!!def.isCloaky); - - this.player = { - position: randomNearbyPointInSector({ x: 0, y: 0 }, 6000), - radius: def.radius, - speed: 0, - heading: Math.random() * 2 * Math.PI, - health: def.health, - id, - sinceLastShot: [effectiveInfinity], - energy: def.energy, - defIndex: index, - arms: emptyLoadout(index), - slotData: emptySlotData(def), - cargo: [], - credits: 500, - npc: this, - team, - side: 0, - v: { x: 0, y: 0 }, - iv: { x: 0, y: 0 }, - ir: 0, - cloak: 1, - }; + let noEquip = false; + if (typeof ship === "string") { + const { def, index } = defMap.get(ship)!; + + assert(!!def.isCloaky); + + this.player = { + position: randomNearbyPointInSector({ x: 0, y: 0 }, 6000), + radius: def.radius, + speed: 0, + heading: Math.random() * 2 * Math.PI, + health: def.health, + id: id!, + sinceLastShot: [effectiveInfinity], + energy: def.energy, + defIndex: index, + arms: emptyLoadout(index), + slotData: emptySlotData(def), + cargo: [], + credits: 500, + npc: this, + team: team!, + side: 0, + v: { x: 0, y: 0 }, + iv: { x: 0, y: 0 }, + ir: 0, + cloak: 1, + }; + } else { + this.player = ship; + noEquip = true; + } + + const def = defs[this.player.defIndex]; // The striker does not have a mine slot, but I am leaving this check here anyways (I want this to work with other cloaky ships in the future) let mineSlot: number | null = def.slots.indexOf(SlotKind.Mine); if (mineSlot === -1) { mineSlot = null; } else { - this.player = equip(this.player, mineSlot, "Proximity Mine", true); + if (!noEquip) { + this.player = equip(this.player, mineSlot, "Proximity Mine", true); + } } const utilityIndices = def.slots.map((slot, i) => (slot === SlotKind.Utility ? i : -1)).filter((i) => i !== -1); let impulseSlot: null | number = null; if (utilityIndices.length > 0) { - this.player = equip(this.player, utilityIndices[0], "Cloaking Generator", true); + if (!noEquip) { + this.player = equip(this.player, utilityIndices[0], "Cloaking Generator", true); + } } if (utilityIndices.length > 1) { - this.player = equip(this.player, utilityIndices[1], "Impulse Missile", true); + if (!noEquip) { + this.player = equip(this.player, utilityIndices[1], "Impulse Missile", true); + } impulseSlot = utilityIndices[1]; } - switch (Math.floor(Math.random() * 4)) { + const prng = sfc32(this.player.id % 10000, 4398, this.player.defIndex, 6987); + + switch (Math.floor(prng() * 4)) { case 0: case 1: - this.player = equip(this.player, 1, "Javelin Missile", true); + if (!noEquip) { + this.player = equip(this.player, 1, "Javelin Missile", true); + } this.currentState = makeStateGraph(estimateEffectivePrimaryRange(def), true, 3000, mineSlot, impulseSlot); break; case 2: case 3: - this.player = equip(this.player, 1, "Tomahawk Missile", true); + if (!noEquip) { + this.player = equip(this.player, 1, "Tomahawk Missile", true); + } this.currentState = makeStateGraph(estimateEffectivePrimaryRange(def), true, 2500, mineSlot, impulseSlot); break; } @@ -217,10 +239,12 @@ class CloakyAnnoying implements NPC { if (!target && this.targetId) { target = state.players.get(this.targetId); } - this.currentState = this.currentState.process(state, this, sector, target); + this.currentState = this.currentState.process(state, this, sector, target) ?? this.currentState; } } +npcReconstructors.set("CloakyAnnoying", (player: Player) => new CloakyAnnoying(player)); + const spawnAssassinationNPC = (state: GlobalState, npcFaction: Faction, targetId: number) => { const npc = new CloakyAnnoying("Striker", npcFaction, targetId); state.players.set(npc.player.id, npc.player); diff --git a/server/npcs/clearance.ts b/server/npcs/clearance.ts index 759eaca..432fca8 100644 --- a/server/npcs/clearance.ts +++ b/server/npcs/clearance.ts @@ -1,4 +1,4 @@ -import { defMap, defs, emptyLoadout, emptySlotData, Faction, SlotKind } from "../../src/defs"; +import { defMap, defs, emptyLoadout, emptySlotData, Faction, SlotKind, UnitDefinition } from "../../src/defs"; import { estimateEffectivePrimaryRange, projectileDefs } from "../../src/defs/projectiles"; import { effectiveInfinity, equip, findClosestTarget, GlobalState, Input, Player, randomNearbyPointInSector } from "../../src/game"; import { l2Norm } from "../../src/geometry"; @@ -6,6 +6,7 @@ import { idleState, LootTable, NPC, + npcReconstructors, passiveGoToRandomPointInSector, randomCombatManeuver, runAway, @@ -13,7 +14,8 @@ import { State, strafingSwarmCombat, stupidSwarmCombat, -} from "../../src/npc"; +} from "./npc"; +import { sfc32 } from "../../src/prng"; import { uid } from "../state"; const makeStateGraph = ( @@ -103,65 +105,86 @@ class BasicSwarmer implements NPC { public targetId: number; - constructor(ship: string, team: Faction) { + constructor(ship: string | Player, team?: Faction) { this.lootTable = new LootTable(); - const { def, index } = defMap.get(ship)!; - const bounds = { x: -3000, y: -3000, width: 6000, height: 6000 }; - this.player = { - position: randomNearbyPointInSector({ x: 0, y: 0 }, 6000), - radius: def.radius, - speed: 0, - heading: Math.random() * 2 * Math.PI, - health: def.health, - id: uid(), - sinceLastShot: [effectiveInfinity], - energy: def.energy, - defIndex: index, - arms: emptyLoadout(index), - slotData: emptySlotData(def), - cargo: [], - credits: 500, - npc: this, - team, - side: 0, - v: { x: 0, y: 0 }, - iv: { x: 0, y: 0 }, - ir: 0, - }; - + let noEquip = false; + if (typeof ship === "string") { + const { def, index } = defMap.get(ship)!; + + this.player = { + position: randomNearbyPointInSector({ x: 0, y: 0 }, 6000), + radius: def.radius, + speed: 0, + heading: Math.random() * 2 * Math.PI, + health: def.health, + id: uid(), + sinceLastShot: [effectiveInfinity], + energy: def.energy, + defIndex: index, + arms: emptyLoadout(index), + slotData: emptySlotData(def), + cargo: [], + credits: 500, + npc: this, + team: team!, + side: 0, + v: { x: 0, y: 0 }, + iv: { x: 0, y: 0 }, + ir: 0, + }; + } else { + this.player = ship; + noEquip = true; + } + + const def = defs[this.player.defIndex]; let mineSlot: number | null = def.slots.indexOf(SlotKind.Mine); if (mineSlot === -1) { mineSlot = null; } else { - this.player = equip(this.player, mineSlot, "Proximity Mine", true); + if (!noEquip) { + this.player = equip(this.player, mineSlot, "Proximity Mine", true); + } } const isStrafer = def.name === "Strafer"; - switch (Math.floor(Math.random() * 7)) { + const prng = sfc32(this.player.id % 10000, 4398, this.player.defIndex, 6987); + + switch (Math.floor(prng() * 7)) { case 0: case 1: case 2: - this.player = equip(this.player, 1, "Javelin Missile", true); + if (!noEquip) { + this.player = equip(this.player, 1, "Javelin Missile", true); + } this.currentState = makeStateGraph(estimateEffectivePrimaryRange(def), true, 3000, 3, mineSlot, isStrafer); break; case 3: - this.player = equip(this.player, 1, "Tomahawk Missile", true); + if (!noEquip) { + this.player = equip(this.player, 1, "Tomahawk Missile", true); + } this.currentState = makeStateGraph(estimateEffectivePrimaryRange(def), true, 2500, 3, mineSlot, isStrafer); break; case 4: - this.player = equip(this.player, 1, "Laser Beam", true); + if (!noEquip) { + this.player = equip(this.player, 1, "Laser Beam", true); + } this.currentState = makeStateGraph(estimateEffectivePrimaryRange(def), true, 3000, 38, mineSlot, isStrafer); break; case 5: - this.player = equip(this.player, 1, "Disruptor Cannon", true); + if (!noEquip) { + this.player = equip(this.player, 1, "Disruptor Cannon", true); + } this.currentState = makeStateGraph(estimateEffectivePrimaryRange(def), false, 350, 10, mineSlot, isStrafer); break; case 6: - this.player = equip(this.player, 1, "Shotgun", true); + if (!noEquip) { + this.player = equip(this.player, 1, "Shotgun", true); + } this.currentState = makeStateGraph(estimateEffectivePrimaryRange(def), false, 1300, 13, mineSlot, isStrafer); break; } @@ -180,10 +203,12 @@ class BasicSwarmer implements NPC { if (!target && this.targetId) { target = state.players.get(this.targetId); } - this.currentState = this.currentState.process(state, this, sector, target); + this.currentState = this.currentState.process(state, this, sector, target) ?? this.currentState; } } +npcReconstructors.set("BasicSwarmer", (player: Player) => new BasicSwarmer(player)); + const spawnClearanceNPCs = (state: GlobalState, npcFaction: Faction, shipList: string[]) => { for (const ship of shipList) { if (!defMap.has(ship)) { diff --git a/src/npc.ts b/server/npcs/npc.ts similarity index 75% rename from src/npc.ts rename to server/npcs/npc.ts index 24e137a..b34ce5b 100644 --- a/src/npc.ts +++ b/server/npcs/npc.ts @@ -1,6 +1,6 @@ -import { armDefs, collectableDefMap, defMap, defs, emptyLoadout, emptySlotData, Faction, SlotKind, UnitDefinition, UnitKind } from "./defs"; -import { defaultLootTable, LootTable } from "./defs/collectables"; -import { estimateEffectivePrimaryRange, projectileDefs } from "./defs/projectiles"; +import { armDefs, collectableDefMap, defMap, defs, emptyLoadout, emptySlotData, Faction, SlotKind, UnitDefinition, UnitKind } from "../../src/defs"; +import { defaultLootTable, LootTable } from "../../src/defs/collectables"; +import { estimateEffectivePrimaryRange, projectileDefs } from "../../src/defs/projectiles"; import { applyInputs, effectiveInfinity, @@ -9,28 +9,20 @@ import { findHeadingBetween, GlobalState, Input, - isValidSectorInDirection, - mapSize, Player, randomNearbyPointInSector, sectorBounds, sectorDelta, -} from "./game"; -import { findInterceptAimingHeading, findSmallAngleBetween, l2Norm, pointOutsideRectangle, Position, Rectangle } from "./geometry"; -import { seekPosition, currentlyFacing, stopPlayer, arrivePosition, arrivePositionUsingAngle, seekPositionUsingAngle } from "./pathing"; -import { recipeMap } from "./recipes"; - -interface NPC { - player: Player; - input: Input; - angle: number | undefined; - selectedSecondary: number; - secondariesToFire: number[]; - lootTable: LootTable; - targetId: number; - process: (state: GlobalState, sector: number) => void; - killed?: () => void; -} +} from "../../src/game"; +import { findInterceptAimingHeading, findSmallAngleBetween, l2Norm, pointOutsideRectangle, Position, Rectangle } from "../../src/geometry"; +import { mapHeight, mapWidth } from "../../src/mapLayout"; +import { seekPosition, currentlyFacing, stopPlayer, arrivePosition, arrivePositionUsingAngle, seekPositionUsingAngle } from "../../src/pathing"; +import { sfc32 } from "../../src/prng"; +import { recipeMap } from "../../src/recipes"; +import { NPC } from "../../src/game"; +import { factionSectors } from "../state"; + +const npcReconstructors: Map NPC> = new Map(); type Completed = { completed?: boolean; @@ -55,7 +47,7 @@ abstract class State { return null; } - abstract process: (state: GlobalState, npc: NPC, sector: number, target: Player | undefined) => State; + abstract process: (state: GlobalState, npc: NPC, sector: number, target: Player | undefined) => State | undefined; abstract onEnter: (npc: NPC) => void; } @@ -102,6 +94,7 @@ const passiveGoToRandomValidNeighboringSector = () => { process = (state: GlobalState, npc: NPC, sector: number, target: Player | undefined) => { if (this.memory.startSector === undefined) { this.memory.startSector = sector; + // ???? Brain tired, fix later let valid = false; while (!valid) { if (Math.random() < 0.5) { @@ -113,7 +106,7 @@ const passiveGoToRandomValidNeighboringSector = () => { if (direction === null) { continue; } - valid = isValidSectorInDirection(sector, direction); + valid = true; } } const newState = this.checkTransitions(state, npc, target); @@ -414,8 +407,8 @@ const warpTo = (sectorList: number[]) => { return new (class extends State { process = (state: GlobalState, npc: NPC, sector: number, target) => { if (this.memory.needWarp) { - const sectors = sectorList.filter((sector) => sector !== sector); - if (npc.player.warping < 1) { + const sectors = sectorList.filter((sec) => sec !== sector); + if (npc.player.warping! < 1) { npc.player.warping = 1; } npc.player.warpTo = sectors[Math.floor(Math.random() * sectors.length)]; @@ -448,24 +441,24 @@ const makeBasicStateGraph = ( ? runAwayWithStrafing(primaryRange, secondaryGuided, secondaryRange, energyThreshold, mineSlot) : runAway(primaryRange, secondaryGuided, secondaryRange, energyThreshold, mineSlot); const warpAway = warpTo(friendlySectors); - const randomWarp = warpTo(new Array(mapSize * mapSize).fill(0).map((_, i) => i)); + const randomWarp = warpTo(new Array(mapWidth * mapHeight).fill(0).map((_, i) => i)); idle.transitions.push({ trigger: (_, __, ___, target) => !!target, state: swarm }); idle.transitions.push({ trigger: () => Math.random() < 0.01, state: passiveGoTo }); idle.transitions.push({ trigger: () => Math.random() < 0.01, state: passiveGoToSector }); idle.transitions.push({ trigger: () => Math.random() < 0.02, state: randomWarp }); passiveGoTo.transitions.push({ trigger: (_, __, ___, target) => !!target, state: swarm }); - passiveGoTo.transitions.push({ trigger: (_, __, memory) => memory.completed, state: idle }); + passiveGoTo.transitions.push({ trigger: (_, __, memory) => !!memory.completed, state: idle }); passiveGoToSector.transitions.push({ trigger: (_, __, ___, target) => !!target, state: swarm }); - passiveGoToSector.transitions.push({ trigger: (_, __, memory) => memory.completed, state: passiveGoTo }); + passiveGoToSector.transitions.push({ trigger: (_, __, memory) => !!memory.completed, state: passiveGoTo }); swarm.transitions.push({ trigger: (_, __, ___, target) => !target, state: idle }); if (!isStrafer) { const randomManeuver = randomCombatManeuver(primaryRange, secondaryGuided, secondaryRange, energyThreshold, mineSlot); swarm.transitions.push({ - trigger: (_, npc, ___, target) => Math.random() < 0.008 && l2Norm(npc.player.position, target.position) < 500, + trigger: (_, npc, ___, target) => Math.random() < 0.008 && l2Norm(npc.player.position, target!.position) < 500, state: randomManeuver, }); randomManeuver.transitions.push({ trigger: (_, __, ___, target) => !target, state: idle }); - randomManeuver.transitions.push({ trigger: (_, __, memory) => memory.completed, state: swarm }); + randomManeuver.transitions.push({ trigger: (_, __, memory) => !!memory.completed, state: swarm }); randomManeuver.transitions.push({ trigger: () => Math.random() < 0.005, state: swarm }); } else { const strafeSwarm = strafingSwarmCombat( @@ -478,7 +471,7 @@ const makeBasicStateGraph = ( projectileDefs[0].range ); strafeSwarm.transitions.push({ trigger: (_, __, ___, target) => !target, state: idle }); - strafeSwarm.transitions.push({ trigger: (_, __, memory) => memory.completed, state: swarm }); + strafeSwarm.transitions.push({ trigger: (_, __, memory) => !!memory.completed, state: swarm }); swarm.transitions.push({ trigger: (_, npc, ___, target) => Math.random() < 0.05 && !!target && l2Norm(target.position, npc.player.position) < primaryRange, state: strafeSwarm, @@ -517,7 +510,7 @@ class ActiveSwarmer implements NPC { primary: false, secondary: false, }; - public angle: number = undefined; + public angle: number | undefined = undefined; public selectedSecondary = 1; @@ -529,93 +522,118 @@ class ActiveSwarmer implements NPC { public targetId: number; - public friendlySectors: number[] = []; - - constructor(what: string | number, team: number | Faction, id: number, friendlySectors: number[]) { - let defIndex: number; + constructor(what: string | number | Player, team?: number | Faction, id?: number) { + let noEquip = false; let def: UnitDefinition; - if (typeof what === "string") { - const value = defMap.get(what); - if (value) { - defIndex = value.index; - def = value.def; + if (typeof what === "object") { + this.player = what; + noEquip = true; + def = defs[what.defIndex]; + } else { + let defIndex: number; + if (typeof what === "string") { + const value = defMap.get(what); + if (value) { + defIndex = value.index; + def = value.def; + } else { + throw new Error(`Unknown NPC type: ${what}`); + } } else { - throw new Error(`Unknown NPC type: ${what}`); + defIndex = what; + def = defs[defIndex]; } - } else { - defIndex = what; - def = defs[defIndex]; + this.player = { + position: { x: Math.random() * 5000 - 2500, y: Math.random() * 5000 - 2500 }, + radius: defs[defIndex].radius, + speed: 0, + heading: Math.random() * 2 * Math.PI, + health: defs[defIndex].health, + id: id!, + sinceLastShot: [effectiveInfinity], + energy: defs[defIndex].energy, + defIndex: defIndex, + arms: emptyLoadout(defIndex), + slotData: emptySlotData(def), + cargo: [], + credits: 500, + npc: this, + warping: -defs[defIndex].warpTime!, + team: team!, + v: { x: 0, y: 0 }, + iv: { x: 0, y: 0 }, + ir: 0, + side: 0, + }; } - this.player = { - position: { x: Math.random() * 5000 - 2500, y: Math.random() * 5000 - 2500 }, - radius: defs[defIndex].radius, - speed: 0, - heading: Math.random() * 2 * Math.PI, - health: defs[defIndex].health, - id: id, - sinceLastShot: [effectiveInfinity], - energy: defs[defIndex].energy, - defIndex: defIndex, - arms: emptyLoadout(defIndex), - slotData: emptySlotData(def), - cargo: [], - credits: 500, - npc: this, - warping: -defs[defIndex].warpTime, - team, - v: { x: 0, y: 0 }, - iv: { x: 0, y: 0 }, - ir: 0, - side: 0, - }; - let mineSlot = def.slots.indexOf(SlotKind.Mine); + const prng = sfc32(id! % 10000, 4398, this.player.defIndex, 6987); + + let mineSlot: number | null = def.slots.indexOf(SlotKind.Mine); if (mineSlot === -1) { mineSlot = null; } else { - this.player = equip(this.player, mineSlot, "Proximity Mine", true); + if (!noEquip) { + this.player = equip(this.player, mineSlot, "Proximity Mine", true); + } } const isStrafer = def.name === "Strafer"; - switch (Math.floor(Math.random() * 12)) { + switch (Math.floor(prng() * 12)) { case 0: case 1: case 2: - this.player = equip(this.player, 1, "Javelin Missile", true); - this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), true, 3000, 3, mineSlot, friendlySectors, isStrafer); + if (!noEquip) { + this.player = equip(this.player, 1, "Javelin Missile", true); + } + this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), true, 3000, 3, mineSlot, factionSectors[this.player.team], isStrafer); break; case 3: case 4: - this.player = equip(this.player, 1, "Tomahawk Missile", true); - this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), true, 2500, 3, mineSlot, friendlySectors, isStrafer); + if (!noEquip) { + this.player = equip(this.player, 1, "Tomahawk Missile", true); + } + this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), true, 2500, 3, mineSlot, factionSectors[this.player.team], isStrafer); break; case 5: - this.player = equip(this.player, 1, "Laser Beam", true); - this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), true, 3000, 38, mineSlot, friendlySectors, isStrafer); + if (!noEquip) { + this.player = equip(this.player, 1, "Laser Beam", true); + } + this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), true, 3000, 38, mineSlot, factionSectors[this.player.team], isStrafer); break; case 6: - this.player = equip(this.player, 1, "Heavy Javelin Missile", true); - this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), false, 700, 3, mineSlot, friendlySectors, isStrafer); + if (!noEquip) { + this.player = equip(this.player, 1, "Heavy Javelin Missile", true); + } + this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), false, 700, 3, mineSlot, factionSectors[this.player.team], isStrafer); break; case 7: case 8: if (isStrafer) { - this.player = equip(this.player, 1, "Plasma Cannon", true); - this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), false, 800, 3, mineSlot, friendlySectors, isStrafer); + if (!noEquip) { + this.player = equip(this.player, 1, "Plasma Cannon", true); + } + this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), false, 800, 3, mineSlot, factionSectors[this.player.team], isStrafer); } else { - this.player = equip(this.player, 1, "Disruptor Cannon", true); - this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), false, 350, 3, mineSlot, friendlySectors, isStrafer); + if (!noEquip) { + this.player = equip(this.player, 1, "Disruptor Cannon", true); + } + this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), false, 350, 3, mineSlot, factionSectors[this.player.team], isStrafer); } break; case 9: case 10: - this.player = equip(this.player, 1, "Plasma Cannon", true); - this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), false, 800, 3, mineSlot, friendlySectors, isStrafer); + if (!noEquip) { + this.player = equip(this.player, 1, "Plasma Cannon", true); + } + this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), false, 800, 3, mineSlot, factionSectors[this.player.team], isStrafer); break; case 11: - this.player = equip(this.player, 1, "EMP Missile", true); - this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), true, 3000, 3, mineSlot, friendlySectors, isStrafer); + if (!noEquip) { + this.player = equip(this.player, 1, "EMP Missile", true); + } + this.currentState = makeBasicStateGraph(estimateEffectivePrimaryRange(def), true, 3000, 3, mineSlot, factionSectors[this.player.team], isStrafer); break; } @@ -627,18 +645,19 @@ class ActiveSwarmer implements NPC { public process(state: GlobalState, sector: number) { let target: Player | undefined = undefined; const def = defs[this.player.defIndex]; - const newTarget = findClosestTarget(this.player, state, def.scanRange, true); + const newTarget = findClosestTarget(this.player, state, def.scanRange!, true); this.targetId = newTarget?.id ?? 0; target = newTarget; if (!target && this.targetId) { target = state.players.get(this.targetId); } - this.currentState = this.currentState.process(state, this, sector, target); + this.currentState = this.currentState.process(state, this, sector, target) as State; } } -const addNpc = (state: GlobalState, what: string | number, team: Faction, id: number, friendlySectors: number[]) => { - const npc = new ActiveSwarmer(what, team, id, friendlySectors); +const addNpc = (state: GlobalState, what: string | number, team: Faction, id: number) => { + // const npc = new ActiveSwarmer(what, team, id, friendlySectors); + const npc = new ActiveSwarmer(what, team, id); state.players.set(npc.player.id, npc.player); }; @@ -647,7 +666,7 @@ const addNpc = (state: GlobalState, what: string | number, team: Faction, id: nu const aimlessPassiveRoaming = (bounds: Rectangle) => { const roam = passiveGoToRandomPointInSector(bounds); roam.transitions.push({ trigger: () => Math.random() < 0.05, state: roam }); - roam.transitions.push({ trigger: (_, __, memory) => memory.completed, state: roam }); + roam.transitions.push({ trigger: (_, __, memory) => !!memory.completed, state: roam }); return roam; }; @@ -662,7 +681,7 @@ class TutorialRoamingVenture implements NPC { primary: false, secondary: false, }; - public angle: number = undefined; + public angle: number | undefined = undefined; public selectedSecondary = 1; @@ -674,34 +693,41 @@ class TutorialRoamingVenture implements NPC { public targetId: number; - constructor(id: number, where: Position) { - this.lootTable = new LootTable(); - - const { def, index } = defMap.get("Venture"); + constructor(idOrPlayer: number | Player, where?: Position) { + let id: number; + if (typeof idOrPlayer === "number") { + id = idOrPlayer; + const { def, index } = defMap.get("Venture")!; + + this.player = { + position: randomNearbyPointInSector(where!, 1500), + radius: def.radius, + speed: 0, + heading: Math.random() * 2 * Math.PI, + health: def.health, + id: id, + sinceLastShot: [effectiveInfinity], + energy: def.energy, + defIndex: index, + arms: emptyLoadout(index), + slotData: emptySlotData(def), + cargo: [], + credits: 500, + npc: this, + team: Faction.Rogue, + side: 0, + v: { x: 0, y: 0 }, + iv: { x: 0, y: 0 }, + ir: 0, + }; + } else { + id = idOrPlayer.id; + this.player = idOrPlayer; + } const bounds = { x: -3000, y: -3000, width: 6000, height: 6000 }; - this.player = { - position: randomNearbyPointInSector(where, 1500), - radius: def.radius, - speed: 0, - heading: Math.random() * 2 * Math.PI, - health: def.health, - id: id, - sinceLastShot: [effectiveInfinity], - energy: def.energy, - defIndex: index, - arms: emptyLoadout(index), - slotData: emptySlotData(def), - cargo: [], - credits: 500, - npc: this, - team: Faction.Rogue, - side: 0, - v: { x: 0, y: 0 }, - iv: { x: 0, y: 0 }, - ir: 0, - }; + this.lootTable = new LootTable(); this.currentState = aimlessPassiveRoaming(bounds); @@ -711,7 +737,7 @@ class TutorialRoamingVenture implements NPC { private currentState: State; public process(state: GlobalState, sector: number) { - this.currentState = this.currentState.process(state, this, sector, undefined); + this.currentState = this.currentState.process(state, this, sector, undefined) as State; } } @@ -732,7 +758,7 @@ class TutorialStrafer implements NPC { primary: false, secondary: false, }; - public angle: number = undefined; + public angle: number | undefined = undefined; public selectedSecondary = 1; @@ -743,38 +769,49 @@ class TutorialStrafer implements NPC { private guidedSecondary: boolean; private usesAmmo: boolean; - public doNotShootYet: boolean = true; + constructor(idOrPlayer: number | Player, where?: Position) { + let noEquip = false; + let id: number; + if (typeof idOrPlayer === "number") { + id = idOrPlayer; + + const { def, index } = defMap.get("Strafer")!; + + this.player = { + position: randomNearbyPointInSector(where!, 4000), + radius: def.radius, + speed: 0, + heading: Math.random() * 2 * Math.PI, + health: def.health, + id: id, + sinceLastShot: [effectiveInfinity], + energy: def.energy, + defIndex: index, + arms: emptyLoadout(index), + slotData: emptySlotData(def), + cargo: [], + credits: 500, + npc: this, + team: Faction.Rogue, + side: 0, + v: { x: 0, y: 0 }, + iv: { x: 0, y: 0 }, + ir: 0, + doNotShootYet: true, + }; + } else { + id = idOrPlayer.id; + this.player = idOrPlayer; + noEquip = true; + } - constructor(id: number, where: Position) { this.lootTable = new LootTable(); - const { def, index } = defMap.get("Strafer"); - - this.player = { - position: randomNearbyPointInSector(where, 4000), - radius: def.radius, - speed: 0, - heading: Math.random() * 2 * Math.PI, - health: def.health, - id: id, - sinceLastShot: [effectiveInfinity], - energy: def.energy, - defIndex: index, - arms: emptyLoadout(index), - slotData: emptySlotData(def), - cargo: [], - credits: 500, - npc: this, - team: Faction.Rogue, - side: 0, - v: { x: 0, y: 0 }, - iv: { x: 0, y: 0 }, - ir: 0, - }; - this.usesAmmo = true; this.guidedSecondary = false; - this.player = equip(this.player, 1, "Javelin Missile", true); + if (!noEquip) { + this.player = equip(this.player, 1, "Javelin Missile", true); + } } public targetId = 0; @@ -787,7 +824,7 @@ class TutorialStrafer implements NPC { let target: Player | undefined = undefined; const def = defs[this.player.defIndex]; if (this.frame % 60 === 0) { - const newTarget = findClosestTarget(this.player, state, def.scanRange, true, true); + const newTarget = findClosestTarget(this.player, state, def.scanRange!, true, true); this.targetId = newTarget?.id ?? 0; target = newTarget; } @@ -860,7 +897,7 @@ class TutorialStrafer implements NPC { stopPlayer(this.player, this.input); } - if (this.doNotShootYet) { + if (this.player.doNotShootYet) { this.input.primary = false; this.input.secondary = false; } @@ -876,6 +913,10 @@ const addTutorialStrafer = (state: GlobalState, id: number, where: Position) => return npc; }; +npcReconstructors.set("ActiveSwarmer", (player: Player) => new ActiveSwarmer(player)); +npcReconstructors.set("TutorialRoamingVenture", (player: Player) => new TutorialRoamingVenture(player)); +npcReconstructors.set("TutorialStrafer", (player: Player) => new TutorialStrafer(player)); + export { NPC, LootTable, @@ -890,4 +931,5 @@ export { runAway, randomCombatManeuver, strafingSwarmCombat, + npcReconstructors, }; diff --git a/server/package-lock.json b/server/package-lock.json index baad48e..f730666 100755 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -5,16 +5,26 @@ "packages": { "": { "dependencies": { + "@types/axon": "^2.0.2", "@types/cors": "^2.8.13", "@types/express": "^4.17.14", "@types/node": "^18.7.23", "@types/ws": "^8.5.3", + "axon": "^2.0.3", "cors": "^2.8.5", "express": "^4.18.2", "mongoose": "^6.6.5", "ws": "^8.9.0" } }, + "node_modules/@types/axon": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/axon/-/axon-2.0.2.tgz", + "integrity": "sha512-5aKG+CBuwganKQJTjuFwhZd0ekQnIxRBoQpIvvyLWeIY3JvxKXySt+UmG8aLt1WgiU6X+5AVmF5D7PUyWKUnBw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -124,11 +134,39 @@ "node": ">= 0.6" } }, + "node_modules/amp": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", + "integrity": "sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==" + }, + "node_modules/amp-message": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz", + "integrity": "sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==", + "dependencies": { + "amp": "0.3.1" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/axon": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/axon/-/axon-2.0.3.tgz", + "integrity": "sha512-zGM15ltKLOMk+/tS6v4ttbCmhNm5uV09FalXc0/cKUcP9nmx5Npg0QO1/xvijvMA+sUzZpQ/0++w1/8Hci5qzQ==", + "dependencies": { + "amp": "~0.3.1", + "amp-message": "~0.1.1", + "configurable": "0.0.1", + "debug": "*", + "escape-regexp": "0.0.1" + }, + "engines": { + "node": ">= 0.11.8" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -225,6 +263,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/configurable": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/configurable/-/configurable-0.0.1.tgz", + "integrity": "sha512-OcGvB6vM11aQlbtNBxN23uqRAGzpbIiKzn4tgT49nF3QPocQ2N3TODH9901HxKNv+eYupA7TCrtRNY1jm/sK8Q==" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -320,6 +363,11 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/escape-regexp": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/escape-regexp/-/escape-regexp-0.0.1.tgz", + "integrity": "sha512-jVgdsYRa7RKxTT6MKNC3gdT+BF0Gfhpel19+HMRZJC2L0PufB0XOBuXBoXj29NKHwuktnAXd1Z1lyiH/8vOTpw==" + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1005,6 +1053,14 @@ } }, "dependencies": { + "@types/axon": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/axon/-/axon-2.0.2.tgz", + "integrity": "sha512-5aKG+CBuwganKQJTjuFwhZd0ekQnIxRBoQpIvvyLWeIY3JvxKXySt+UmG8aLt1WgiU6X+5AVmF5D7PUyWKUnBw==", + "requires": { + "@types/node": "*" + } + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -1111,11 +1167,36 @@ "negotiator": "0.6.3" } }, + "amp": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", + "integrity": "sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==" + }, + "amp-message": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz", + "integrity": "sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==", + "requires": { + "amp": "0.3.1" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "axon": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/axon/-/axon-2.0.3.tgz", + "integrity": "sha512-zGM15ltKLOMk+/tS6v4ttbCmhNm5uV09FalXc0/cKUcP9nmx5Npg0QO1/xvijvMA+sUzZpQ/0++w1/8Hci5qzQ==", + "requires": { + "amp": "~0.3.1", + "amp-message": "~0.1.1", + "configurable": "0.0.1", + "debug": "*", + "escape-regexp": "0.0.1" + } + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1171,6 +1252,11 @@ "get-intrinsic": "^1.0.2" } }, + "configurable": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/configurable/-/configurable-0.0.1.tgz", + "integrity": "sha512-OcGvB6vM11aQlbtNBxN23uqRAGzpbIiKzn4tgT49nF3QPocQ2N3TODH9901HxKNv+eYupA7TCrtRNY1jm/sK8Q==" + }, "content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1241,6 +1327,11 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "escape-regexp": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/escape-regexp/-/escape-regexp-0.0.1.tgz", + "integrity": "sha512-jVgdsYRa7RKxTT6MKNC3gdT+BF0Gfhpel19+HMRZJC2L0PufB0XOBuXBoXj29NKHwuktnAXd1Z1lyiH/8vOTpw==" + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", diff --git a/server/package.json b/server/package.json index e361342..cb5b2f8 100755 --- a/server/package.json +++ b/server/package.json @@ -1,9 +1,11 @@ { "dependencies": { + "@types/axon": "^2.0.2", "@types/cors": "^2.8.13", "@types/express": "^4.17.14", "@types/node": "^18.7.23", "@types/ws": "^8.5.3", + "axon": "^2.0.3", "cors": "^2.8.5", "express": "^4.18.2", "mongoose": "^6.6.5", diff --git a/server/peers.ts b/server/peers.ts new file mode 100644 index 0000000..e84414e --- /dev/null +++ b/server/peers.ts @@ -0,0 +1,247 @@ +import mongoose from "mongoose"; +import { initFromDatabase } from "./misc"; +import { initInitialAsteroids, initSectorData, initSectors, initStationTeams, insertSector, sendServerWarp, SerializableGlobalState, SerializableClient, SerializablePlayer, insertStation, insertNPC } from "./state"; +import Routes from "./routes"; +import { startWebSocketServer } from "./websockets"; +import { setupTimers } from "./server"; +import { Player, SectorKind } from "../src/game"; +import { mapGraph, mapHeight, mapWidth, peerCount } from "../src/mapLayout"; +import assert from "assert"; +import { sectors as serverSectors } from "./state"; +import { inspect } from "util"; +import axon from "axon"; + +interface IPeer { + name: string; + ip: string; + port: number; + pubPort: number; + wsPort: number; + updated: Date; +} + +const peerSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + }, + ip: { + type: String, + required: true, + }, + port: { + type: Number, + required: true, + }, + pubPort: { + type: Number, + required: true, + }, + wsPort: { + type: Number, + required: true, + }, + // have the schema drop old dates + updated: { + type: Date, + expires: "2m", + default: Date.now, + }, +}); + +const Peer = mongoose.model("Peer", peerSchema); + +// Get our name from the command line options +const name = process.argv[2]; +const port = process.argv[3]; +const pubPort = process.argv[4]; +// For development +const ip = "127.0.0.1"; +const wsPort = parseInt(process.argv[5]); +const httpPort = wsPort + 1; + +const peerNumber = parseInt(process.argv[6]); +assert(peerNumber >= 0 && peerNumber < peerCount); + +const sectorCount = mapWidth * mapHeight; +assert(sectorCount % peerCount === 0); +const sectorsPerPeer = Math.floor(sectorCount / peerCount); +const sectors: number[] = []; +for (let i = 0; i < sectorsPerPeer; i++) { + sectors.push(peerNumber * sectorsPerPeer + i); +} + +console.log(`Starting peer ${name} with sectors ${sectors} on port ${port} and wsPort ${wsPort}`); + +// Sets ourselves in the database +const setPeer = async () => { + await Peer.findOneAndUpdate({ name }, { ip, port, updated: Date.now(), sectors, wsPort, pubPort }, { upsert: true }); +}; + +type PeerSockets = { + request: axon.ReqSocket; + subscriber: axon.SubSocket; + // Websocket ip and port + ip: string; + port: number; + name: string; +}; + +// Global stuff +const peerMap = new Map(); +const repSocket = axon.socket("rep") as axon.RepSocket; +const pubSocket = axon.socket("pub") as axon.PubSocket; + +let interval: NodeJS.Timer | null = null; + +const setupSelf = async () => { + await setPeer(); + // Probably will just protect this with iptables + repSocket.bind(`tcp://${ip}:${port}`); + pubSocket.bind(`tcp://0.0.0.0:${pubPort}`); + + syncPeers(); + setTimeout(() => { + syncPeers(); + }, 4 * 1000); + interval = setInterval(() => { + setPeer(); + syncPeers(); + }, 20 * 1000); +}; + +const serversForSectors = new Map(); + +// Roughly keeps things synced +const syncPeers = async () => { + const peers = await Peer.find({ name: { $ne: name } }); + for (const [sector, state] of serverSectors) { + makeNetworkAware(sector, state.sectorKind!); + } + peers.forEach(async (peer) => { + if (peerMap.has(peer.name)) { + return; + } + console.log(`Connecting to peer ${peer.name} at ${peer.ip}:${peer.port} and ${peer.ip}:${peer.pubPort}`); + + const request = axon.socket("req") as axon.ReqSocket; + request.connect(`tcp://${peer.ip}:${peer.port}`); + + const subscriber = axon.socket("sub") as axon.SubSocket; + subscriber.connect(`tcp://${peer.ip}:${peer.pubPort}`); + subscriber.subscribe("sector-notification"); + subscriber.subscribe("sector-removal"); + subscriber.subscribe("player-sector"); + subscriber.on("message", (topic, data) => { + if (topic === "sector-notification") { + const sector = data.sector; + const server = data.server; + serversForSectors.set(sector, server); + awareSectors.set(sector, data.sectorKind); + return; + } + if (topic === "sector-removal") { + const sector = data.sector; + serversForSectors.delete(sector); + awareSectors.delete(sector); + return; + } + if (topic === "player-sector") { + playerSectors.set(data.id, data.sector); + return; + } + }); + + peerMap.set(peer.name, { request, subscriber, ip: peer.ip, port: peer.wsPort, name: peer.name }); + }); + for (const name of peerMap.keys()) { + if (!peers.find((peer) => peer.name === name)) { + const sockets = peerMap.get(name); + if (sockets) { + sockets.request.close(); + sockets.subscriber.close(); + } + peerMap.delete(name); + // remove from the map + serversForSectors.forEach((server, sector) => { + if (server === name) { + serversForSectors.delete(sector); + } + }); + console.log(`Disconnected from peer ${name}`); + } + } +}; + +const waitingData = new Map(); + +const awareSectors = new Map(); +const playerSectors = new Map(); + +for (let i = 0; i < mapWidth * mapHeight; i++) { + awareSectors.set(i, SectorKind.Overworld); +} + +const makeNetworkAware = (sector: number, kind: SectorKind) => { + awareSectors.set(sector, kind); + pubSocket.send("sector-notification", { sector, sectorKind: kind, server: name }); +}; + +const removeNetworkAwareness = (sector: number) => { + awareSectors.delete(sector); + pubSocket.send("sector-removal", { sector }); +}; + +const setPlayerSector = (id: number, sector: number) => { + playerSectors.set(id, sector); + pubSocket.send("player-sector", { id, sector }); +}; + +mongoose + .connect("mongodb://127.0.0.1:27017/SpaceGame", {}) + .catch((err) => { + console.log("Error connecting to database: " + err); + }) + .then(async () => { + console.log("Connected to database"); + await setupSelf(); + initSectors(sectors); + await initFromDatabase(); + await initSectorData(); + await initStationTeams(); + initInitialAsteroids(); + setupTimers(); + startWebSocketServer(wsPort); + repSocket.on("message", async (topic: string, data: SerializableClient | SerializableGlobalState | SerializablePlayer, reply: (data: any) => void) => { + console.log("Got message: " + topic); + if (topic === "player-transfer") { + console.log("Player transfer"); + data = data as SerializableClient; + waitingData.set(data.key, data); + reply(data.key); + return; + } + if (topic === "sector-transfer") { + console.log("Sector transfer"); + reply(insertSector(data as SerializableGlobalState)); + return; + } + if (topic === "station-transfer") { + console.log("Station transfer"); + reply(insertStation(data as SerializablePlayer)); + return; + } + if (topic === "npc-transfer") { + console.log("NPC transfer"); + data = data as SerializablePlayer; + console.log(data.npcReconstructionKey); + reply(insertNPC(data)); + return; + }; + console.log("Unknown topic: " + topic); + }); + }); + +Routes(httpPort); + +export { PeerSockets, peerMap, waitingData, serversForSectors, awareSectors, playerSectors, makeNetworkAware, removeNetworkAwareness, setPlayerSector }; diff --git a/server/routes.ts b/server/routes.ts index 5479ca3..f859378 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -6,13 +6,11 @@ import { useSsl } from "../src/config"; import express from "express"; import { resolve } from "path"; import cors from "cors"; - import { User, Station } from "./dataModels"; - -import { addNpc } from "../src/npc"; +import { addNpc } from "./npcs/npc"; import { market } from "./market"; -import { clients, friendlySectors, idToWebsocket, sectorFactions, sectorHasStarbase, sectorList, sectors, uid } from "./state"; -import { adminHash, hash, httpPort, sniCallback } from "./settings"; +import { clients, idToWebsocket, sectorFactions, sectorHasStarbase, factionSectors, sectorList, sectors, transferSectorToPeer, uid } from "./state"; +import { adminHash, hash, sniCallback } from "./settings"; import { recipeMap, recipes } from "../src/recipes"; import { isFreeArm } from "../src/defs/armaments"; import { createReport, generatePlayedIntervals, statEpoch, sumIntervals } from "./reports"; @@ -21,6 +19,7 @@ import { maxDecimals } from "../src/geometry"; import { genMissions, Mission } from "./missions"; import { canFriendRequest, FriendRequest } from "./friends"; import { findPlayer } from "./stateHelpers"; +import { awareSectors, peerMap, playerSectors } from "./peers"; // Http server stuff const root = resolve(__dirname + "/.."); @@ -148,6 +147,7 @@ app.get("/clearAllFriendsAndRequests", async (req, res) => { } }); +// UNSAFE app.get("/currentSectorOfPlayer", (req, res) => { const idParam = req.query.id; if (!idParam || typeof idParam !== "string") { @@ -155,7 +155,17 @@ app.get("/currentSectorOfPlayer", (req, res) => { return; } const id = parseInt(idParam); - res.send(JSON.stringify({ value: findPlayer(id) })); + const sector = playerSectors.get(id); + if (sector === undefined) { + res.send(JSON.stringify({ value: null })); + return; + } + const awareness = awareSectors.get(sector); + if (awareness === undefined) { + res.send(JSON.stringify({ value: null })); + return; + } + res.send(JSON.stringify({ value: { sectorNumber: sector, sectorKind: awareness } })); }); app.get("/nameOf", (req, res) => { @@ -271,11 +281,13 @@ app.get("/init", (req, res) => { // Create a bunch of stations const stationObjects = sectorList .map((sector) => { - if (!sectorHasStarbase[sector]) { - return []; - } + return []; + // if (!sectorHasStarbase[sector]) { + // return []; + // } - const faction = sectorFactions[sector]; + // const faction = sectorFactions[sector]; + const faction: Faction = Faction.Alliance; switch (faction) { case Faction.Rogue: return [ @@ -327,35 +339,6 @@ app.get("/init", (req, res) => { }); }); -// Probably don't need this anymore -app.get("/resetEverything", (req, res) => { - const password = req.query.password; - if (!password || typeof password !== "string") { - res.send("Invalid get parameters"); - return; - } - const hashedPassword = hash(password); - if (hashedPassword !== adminHash) { - res.send("Invalid password"); - return; - } - // Delete all the stations - Station.deleteMany({}, (err) => { - if (err) { - res.send("Database error: " + err); - return; - } - // Delete all the users - User.deleteMany({}, (err) => { - if (err) { - res.send("Database error: " + err); - return; - } - res.send("true"); - }); - }); -}); - app.get("/unlockEverything", (req, res) => { const password = req.query.password; if (!password || typeof password !== "string") { @@ -450,6 +433,7 @@ app.get("/unlockEverything", (req, res) => { }); }); +// UNSAFE app.get("/addNPC", (req, res) => { const password = req.query.password; if (!password || typeof password !== "string") { @@ -482,7 +466,7 @@ app.get("/addNPC", (req, res) => { return; } try { - addNpc(sectors.get(sectorIndex)!, what, parseInt(team), uid(), friendlySectors(parseInt(team))); + addNpc(sectors.get(sectorIndex)!, what, parseInt(team), uid()); } catch (e) { res.send("Error: " + e); return; @@ -520,6 +504,7 @@ app.get("/stopProfiling", (req, res) => { res.send("true"); }); +// UNSAFE app.get("/totalPlayers", (req, res) => { const ret = `Total: ${Array.from(sectors.values()) @@ -532,32 +517,7 @@ app.get("/totalPlayers", (req, res) => { res.send(ret); }); -app.get("/fixDataBase", (req, res) => { - const password = req.query.password; - if (!password || typeof password !== "string") { - res.send("Invalid get parameters"); - return; - } - const hashedPassword = hash(password); - if (hashedPassword !== adminHash) { - res.send("Invalid password"); - return; - } - // Round all the inventory number to the nearest integer - User.find({}, (err, users) => { - if (err) { - res.send("Database error: " + err); - return; - } - users.forEach((user) => { - const newInventory = Object.fromEntries(Object.entries(user.inventory).map(([key, value]) => [key, Math.round(value as number)])); - user.inventory = newInventory; - user.save(); - }); - res.send("true"); - }); -}); - +// UNSAFE (sort of, market prices are fixed right now so it is really fine) app.get("/priceOf", (req, res) => { const what = req.query.what; if (!what || typeof what !== "string") { @@ -572,6 +532,7 @@ app.get("/priceOf", (req, res) => { res.send(JSON.stringify({ value: price })); }); +// UNSAFE app.get("/kill", (req, res) => { const password = req.query.password; if (!password || typeof password !== "string") { @@ -612,6 +573,7 @@ app.get("/kill", (req, res) => { res.send("true"); }); +// UNSAFE app.get("/usersOnline", (req, res) => { res.send(JSON.stringify(Array.from(clients.values()).map((client) => client.name))); }); @@ -767,7 +729,21 @@ app.get("/selectedMissions", async (req, res) => { res.send(JSON.stringify(missions)); }); -export default () => { +app.get("/transferSector", (req, res) => { + const sectorParam = req.query.sector; + const toParam = req.query.to; + if (!sectorParam || typeof sectorParam !== "string" || !toParam || typeof toParam !== "string") { + res.send("Invalid get parameters"); + return; + } + transferSectorToPeer(parseInt(sectorParam), toParam).then(() => { + res.send("Transfer complete"); + }).catch((err) => { + res.send("Transfer failed: " + err); + }); +}); + +export default (port: number) => { app.use(cors()); if (useSsl) { app.use(express.static("resources")); @@ -775,15 +751,15 @@ export default () => { const httpsServer = new https.Server({ SNICallback: sniCallback}, app); - httpsServer.listen(httpPort, () => { - console.log(`Running secure http server on port ${httpPort}`); + httpsServer.listen(port, () => { + console.log(`Running secure http server on port ${port}`); }); } else { app.use(express.static("..")); const httpServer = createServer(app); - httpServer.listen(httpPort, () => { - console.log(`Running unsecure http server on port ${httpPort}`); + httpServer.listen(port, () => { + console.log(`Running unsecure http server on port ${port}`); }); } }; diff --git a/server/sector.ts b/server/sector.ts new file mode 100644 index 0000000..44afeaf --- /dev/null +++ b/server/sector.ts @@ -0,0 +1,65 @@ +import mongoose from "mongoose"; +import { Faction } from "../src/defs"; + +interface IResourceDensity { + resource: string; + density: number; +} + +const resourceDensitySchema = new mongoose.Schema({ + resource: { + type: String, + required: true, + }, + density: { + type: Number, + required: true, + min: 0, + }, +}); + +interface ISector { + id: number; + resources: IResourceDensity[]; + asteroidCount: number; + faction: Faction; + guardianCount: number; +} + +const sectorSchema = new mongoose.Schema({ + id: { + type: Number, + required: true, + }, + resources: { + type: [resourceDensitySchema], + default: [], + }, + asteroidCount: { + type: Number, + default: 0, + }, + faction: { + type: Number, + required: true, + min: 0, + max: 3, + validate: { + validator: Number.isInteger, + message: "{VALUE} is not an integer value", + } + }, + guardianCount: { + type: Number, + required: true, + min: 0, + validate: { + validator: Number.isInteger, + message: "{VALUE} is not an integer value", + } + }, +}); + +const Sector = mongoose.model("Sector", sectorSchema); + +export { Sector, ISector }; diff --git a/server/server.ts b/server/server.ts index 6824e14..6f07227 100644 --- a/server/server.ts +++ b/server/server.ts @@ -1,6 +1,3 @@ -import { createServer } from "http"; -import https from "https"; -import { WebSocketServer, WebSocket } from "ws"; import { GlobalState, Player, @@ -9,912 +6,47 @@ import { applyInputs, Ballistic, ticksPerSecond, - maxNameLength, - canDock, - copyPlayer, randomAsteroids, - TargetKind, EffectTrigger, - equip, Missile, - purchaseShip, - effectiveInfinity, processAllNpcs, - canRepair, - removeAtMostCargo, - isNearOperableEnemyStation, SectorTransition, findSectorTransitions, sectorBounds, - SectorInfo, - CloakedState, - TutorialStage, - mapSize, - applyUndockingOffset, - removeCargoFractions, - SectorKind, + isNearOperableEnemyStation, } from "../src/game"; -import { defs, defMap, Faction, armDefs, ArmUsage, emptyLoadout, UnitKind, clientUid } from "../src/defs"; -import { appendFile } from "fs"; -import { useSsl } from "../src/config"; - -import { User, Station, Checkpoint } from "./dataModels"; -import mongoose from "mongoose"; +import { defs, Faction, UnitKind } from "../src/defs"; -import { addNpc, NPC } from "../src/npc"; -import { inspect } from "util"; -import { - depositItemsIntoInventory, - depositCargo, - manufacture, - sellInventory, - sendInventory, - transferToShip, - discoverRecipe, - compositeManufacture, -} from "./inventory"; -import { market } from "./market"; +import { addNpc, NPC } from "./npcs/npc"; +import { discoverRecipe, sendInventory } from "./inventory"; import { + allResources, clients, - friendlySectors, idToWebsocket, - initInitialAsteroids, knownRecipes, - saveCheckpoint, secondaries, secondariesToActivate, - sectorAsteroidResources, + factionSectors, sectorFactions, sectorGuardianCount, - sectorInDirection, sectorList, sectors, - sectorTriggers, + serializeAllClientData, + ServerChangeKind, + serverChangePlayer, + stationIdToDefaultTeam, targets, - tutorialRespawnPoints, uid, warpList, + sectorAsteroidResources, } from "./state"; -import { CardinalDirection, headingFromCardinalDirection, mirrorAngleHorizontally, mirrorAngleVertically } from "../src/geometry"; -import { hash, sniCallback, wsPort } from "./settings"; -import Routes from "./routes"; -import { advanceTutorialStage, sendTutorialStage } from "./tutorial"; -import { assignPlayerIdToConnection, logWebSocketConnection } from "./logging"; -import { selectMission, startPlayerInMission } from "./missions"; -import { enemyCount, allyCount, flashServerMessage } from "./stateHelpers"; -import { createFriendRequest, friendWarp, revokeFriendRequest, unfriend } from "./friends"; - -mongoose - .connect("mongodb://127.0.0.1:27017/SpaceGame", {}) - .catch((err) => { - console.log("Error connecting to database: " + err); - }) - .then(async () => { - console.log("Connected to database"); - // Initialize the server state stuff - await initFromDatabase(); - initInitialAsteroids(); - }); - -Routes(); - -// This is for loading the stations from the database on server startup -const initFromDatabase = async () => { - const stations = await Station.find({}); - for (const station of stations) { - const def = defs[station.definitionIndex]; - const player: Player = { - position: station.position, - radius: def.radius, - speed: 0, - heading: 0, - health: def.health, - id: station.id, - sinceLastShot: [effectiveInfinity, effectiveInfinity, effectiveInfinity, effectiveInfinity], - energy: def.energy, - defIndex: station.definitionIndex, - arms: [], - slotData: [], - team: station.team, - side: 0, - isPC: true, - v: { x: 0, y: 0 }, - iv: { x: 0, y: 0 }, - ir: 0, - }; - const sector = sectors.get(station.sector); - if (sector) { - sector.players.set(station.id, player); - } - } -}; - -// Websocket server stuff -let server: ReturnType | https.Server; -if (useSsl) { - server = new https.Server({ SNICallback: sniCallback }); -} else { - server = createServer(); -} - -// Websocket stuff (TODO Move to its own file) -const wss = new WebSocketServer({ server }); - -const setupPlayer = (id: number, ws: WebSocket, name: string, faction: Faction) => { - let defIndex: number; - if (faction === Faction.Alliance) { - // defIndex = defMap.get("Fighter")!.index; - defIndex = defMap.get("Advanced Fighter")!.index; - } else if (faction === Faction.Confederation) { - // defIndex = defMap.get("Drone")!.index; - defIndex = defMap.get("Seeker")!.index; - } else { - console.log(`Invalid faction ${faction}`); - return; - } - - const sectorToWarpTo = faction === Faction.Alliance ? 12 : 15; - - let tutorialSector = clientUid(); - while (sectors.has(tutorialSector)) { - tutorialSector = clientUid(); - } - - clients.set(ws, { - id: id, - name, - input: { up: false, down: false, primary: false, secondary: false, right: false, left: false }, - angle: 0, - currentSector: tutorialSector, - lastMessage: "", - lastMessageTime: Date.now(), - sectorsVisited: new Set(), - inTutorial: TutorialStage.Move, - }); - - let player = { - position: { x: 0, y: 0 }, - radius: defs[defIndex].radius, - speed: 0, - heading: 0, - health: defs[defIndex].health, - id: id, - sinceLastShot: [effectiveInfinity], - energy: defs[defIndex].energy, - defIndex: defIndex, - arms: emptyLoadout(defIndex), - slotData: new Array(defs[defIndex].slots.length).fill({}), - cargo: [], - credits: 500, - team: faction, - side: 0, - isPC: true, - v: { x: 0, y: 0 }, - iv: { x: 0, y: 0 }, - ir: 0, - }; - - // player = equip(player, 0, "Basic Mining Laser", true); - // player = equip(player, 1, "Laser Beam", true); - // player = equip(player, 1, "Tomahawk Missile", true); - - const state = { - players: new Map(), - projectiles: new Map(), - asteroids: new Map(), - missiles: new Map(), - collectables: new Map(), - asteroidsDirty: false, - mines: new Map(), - projectileId: 1, - delayedActions: [], - sectorKind: SectorKind.Tutorial, - }; - - sectors.set(tutorialSector, state); - state.players.set(id, player); - - // Idk the right way to handle this right now - // Just delete the tutorial sector after a while - setTimeout(() => { - sectors.delete(tutorialSector); - tutorialRespawnPoints.delete(tutorialSector); - }, 1000 * 60 * 60 * 3); - - targets.set(id, [TargetKind.None, 0]); - secondaries.set(id, 0); - secondariesToActivate.set(id, []); - knownRecipes.set(id, new Set()); - - const sectorInfos: SectorInfo[] = []; - sectorInfos.push({ - sector: sectorToWarpTo, - resources: sectorAsteroidResources[sectorToWarpTo].map((value) => value.resource), - }); - - ws.send( - JSON.stringify({ - type: "init", - payload: { - id: id, - sector: tutorialSector, - faction, - asteroids: Array.from(state.asteroids.values()), - collectables: Array.from(state.collectables.values()), - mines: Array.from(state.mines.values()), - sectorInfos, - recipes: [], - }, - }) - ); - sendInventory(ws, id); - sendTutorialStage(ws, TutorialStage.Move); -}; - -// TODO Need to go over this carefully, checking to make sure that malicious clients can't do anything bad -wss.on("connection", (ws, req) => { - (ws as any).isAlive = true; - - const ipAddr = req.socket.remoteAddress; - - logWebSocketConnection(ipAddr); - - ws.on("message", (msg) => { - try { - const data = JSON.parse(msg.toString()); - if (data.type === "heartbeat") { - (ws as any).isAlive = true; - return; - } else if (data.type === "login") { - const name = data.payload.name; - const password = data.payload.password; - - const hashedPassword = hash(password); - - // Check if the user is in the database - User.findOne({ name, password: hashedPassword }, (err, user) => { - if (err) { - ws.send(JSON.stringify({ type: "loginFail", payload: { error: "Database error" } })); - console.log(err); - return; - } - if (!user) { - ws.send(JSON.stringify({ type: "loginFail", payload: { error: "Username/password combination not found" } })); - return; - } - - if (idToWebsocket.has(user.id)) { - ws.send(JSON.stringify({ type: "loginFail", payload: { error: "User already logged in" } })); - return; - } - - idToWebsocket.set(user.id, ws); - assignPlayerIdToConnection(ipAddr, user.id); - - const sectorInfos: SectorInfo[] = []; - if (!user.sectorsVisited) { - user.sectorsVisited = [user.currentSector]; - } - user.loginCount++; - user.loginTimes.push(Date.now()); - try { - user.save(); - } catch (err) { - console.log(err); - } - - const sectorsVisited: Set = new Set(user.sectorsVisited); - for (const sector of sectorsVisited) { - sectorInfos.push({ - sector, - resources: sectorAsteroidResources[sector].map((value) => value.resource), - }); - } - - Checkpoint.findOne({ id: user.id }, (err, checkpoint) => { - if (err) { - ws.send(JSON.stringify({ type: "loginFail", payload: { error: "Database error" } })); - console.log(err); - return; - } - if (!checkpoint) { - setupPlayer(user.id, ws, name, user.faction); - } else { - const state = sectors.get(checkpoint.sector); - if (!state) { - ws.send(JSON.stringify({ type: "error", payload: { message: "Bad checkpoint sector" } })); - console.log("Warning: Checkpoint sector not found"); - setupPlayer(user.id, ws, name, user.faction); - return; - } - const playerState = JSON.parse(checkpoint.data) as Player; - if (isNearOperableEnemyStation(playerState, state.players.values()) || enemyCount(playerState.team, checkpoint.sector) > 2) { - playerState.position.x = -5000; - playerState.position.y = 5000; - } - // All these "fixes" are for making old checkpoints work with new code - // Update the player on load to match what is expected - if (playerState.defIndex === undefined) { - playerState.defIndex = (playerState as any).definitionIndex; - (playerState as any).definitionIndex = undefined; - } - // fix the cargo - if (playerState.cargo === undefined || playerState.cargo.some((c) => !Number.isInteger(c.amount))) { - playerState.cargo = [{ what: "Teddy Bears", amount: 30 }]; - } - // fix the credits - if (playerState.credits === undefined) { - playerState.credits = 500; - } - playerState.credits = Math.round(playerState.credits); - // fix the slot data - const def = defs[playerState.defIndex]; - while (playerState.slotData.length < def.slots.length) { - playerState.arms.push(def.slots[playerState.slotData.length]); - playerState.slotData.push({}); - } - // fix the impulse - if (playerState.ir === undefined) { - playerState.ir = 0; - } - if (playerState.iv === undefined) { - playerState.iv = { x: 0, y: 0 }; - } - // fix the health and energy - if (playerState.health > def.health) { - playerState.health = def.health; - } - if (playerState.energy > def.energy) { - playerState.energy = def.energy; - } - (playerState as any).projectileId = undefined; - // fix the arms - if (playerState.arms === undefined) { - playerState.arms = (playerState as any).armIndices; - (playerState as any).armIndices = undefined; - } - - playerState.v = { x: 0, y: 0 }; - state.players.set(user.id, playerState); - clients.set(ws, { - id: user.id, - name, - input: { up: false, down: false, primary: false, secondary: false, right: false, left: false }, - angle: 0, - currentSector: checkpoint.sector, - lastMessage: "", - lastMessageTime: Date.now(), - sectorsVisited, - inTutorial: TutorialStage.Done, - }); - targets.set(user.id, [TargetKind.None, 0]); - secondaries.set(user.id, 0); - secondariesToActivate.set(user.id, []); - knownRecipes.set(user.id, new Set(user.recipesKnown)); - ws.send( - JSON.stringify({ - type: "init", - payload: { - id: user.id, - sector: checkpoint.sector, - faction: playerState.team, - asteroids: Array.from(state.asteroids.values()), - collectables: Array.from(state.collectables.values()), - mines: Array.from(state.mines.values()), - sectorInfos, - recipes: user.recipesKnown, - }, - }) - ); - sendInventory(ws, user.id); - // log to file - appendFile("log", `${new Date().toISOString()} ${name} logged in\n`, (err) => { - if (err) { - console.log(err); - } - }); - } - }); - }); - } else if (data.type === "register") { - const name = data.payload.name; - const password = data.payload.password; - const faction = data.payload.faction; - - // Check if the user is in the database - User.findOne({ name }, (err, user) => { - if (err) { - ws.send(JSON.stringify({ type: "registerFail", payload: { error: "Database error" } })); - console.log(err); - return; - } - if (user) { - ws.send(JSON.stringify({ type: "registerFail", payload: { error: "Username already taken" } })); - return; - } - if (name.length > maxNameLength) { - ws.send(JSON.stringify({ type: "registerFail", payload: { error: "Username too long" } })); - return; - } - User.create({ name, password: hash(password), faction, id: uid(), loginTimes: [Date.now()] }, (err, user) => { - if (err) { - ws.send(JSON.stringify({ type: "registerFail", payload: { error: "Database error" } })); - console.log(err); - return; - } - setupPlayer(user.id, ws, name, faction); - idToWebsocket.set(user.id, ws); - assignPlayerIdToConnection(ipAddr, user.id); - }); - }); - } else if (data.type === "input") { - const client = clients.get(ws); - if (client) { - client.input = data.payload.input; - } else { - console.log("Warning: Input data from unknown client"); - } - } else if (data.type === "angle") { - const client = clients.get(ws); - if (client) { - client.angle = data.payload.angle; - } else { - console.log("Warning: Angle data from unknown client"); - } - } else if (data.type === "dock") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player) { - if (player.docked) { - return; - } - removeCargoFractions(player); - const station = state.players.get(data.payload.stationId); - if (canDock(player, station, false)) { - const def = defs[player.defIndex]; - player.docked = data.payload.stationId; - player.heading = 0; - player.speed = 0; - player.side = 0; - player.energy = def.energy; - player.health = def.health; - player.warping = 0; - player.ir = 0; - player.iv.x = 0; - player.iv.y = 0; - player.cloak = CloakedState.Uncloaked; - player.position = { x: station!.position.x, y: station!.position.y }; - for (let i = 0; i < player.arms.length; i++) { - const armDef = armDefs[player.arms[i]]; - if (armDef && armDef.usage === ArmUsage.Ammo) { - player.slotData[i].ammo = armDef.maxAmmo; - } - } - - state.players.set(client.id, player); - - if (!client.inTutorial) { - saveCheckpoint(client.id, client.currentSector, player, client.sectorsVisited); - } else { - tutorialRespawnPoints.set(client.id, copyPlayer(player)); - } - } - } - } - } else if (data.type === "undock") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player) { - player.docked = undefined; - applyUndockingOffset(player); - state.players.set(client.id, player); - - if (!client.inTutorial) { - saveCheckpoint(client.id, client.currentSector, player, client.sectorsVisited); - } else { - tutorialRespawnPoints.set(client.id, copyPlayer(player)); - } - } - } - } else if (data.type === "repair") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player) { - const station = state.players.get(data.payload.station)!; - if (canRepair(player, station, false)) { - if (!station.repairs || station.repairs.length !== Faction.Count) { - console.log(`Warning: Station repairs array is not correctly initialized (${station.id})`); - } else { - const stationDef = defs[station.defIndex]; - const repairsNeeded = stationDef.repairsRequired! - station.repairs[player.team]; - const amountRepaired = removeAtMostCargo(player, "Spare Parts", repairsNeeded); - station.repairs[player.team] += amountRepaired; - } - } - } - } - } else if (data.type === "respawn") { - const client = clients.get(ws); - if (client) { - if (client.inTutorial) { - const state = sectors.get(client.currentSector); - if (state) { - const playerState = tutorialRespawnPoints.get(client.id); - if (playerState) { - state.players.set(client.id, copyPlayer(playerState)); - } else { - ws.send(JSON.stringify({ type: "error", payload: { message: "Missing tutorial respawn checkpoint" } })); - } - } else { - ws.send(JSON.stringify({ type: "error", payload: { message: "Tutorial sector invalid" } })); - } - return; - } - Checkpoint.findOne({ id: client.id }, (err, checkpoint) => { - if (err) { - ws.send(JSON.stringify({ type: "error", payload: { message: "Server error loading checkpoint" } })); - console.log("Error loading checkpoint: " + err); - return; - } - if (!checkpoint) { - ws.send(JSON.stringify({ type: "error", payload: { message: "Checkpoint not found" } })); - console.log("Error loading checkpoint: " + err); - return; - } - const state = sectors.get(checkpoint.sector); - if (!state) { - ws.send(JSON.stringify({ type: "error", payload: { message: "Bad checkpoint sector" } })); - console.log("Warning: Checkpoint sector not found (programming error)"); - return; - } - const playerState = JSON.parse(checkpoint.data) as Player; - // So I don't have to edit the checkpoints in the database right now - playerState.isPC = true; - if ( - isNearOperableEnemyStation(playerState, state.players.values()) || - enemyCount(playerState.team, checkpoint.sector) - allyCount(playerState.team, checkpoint.sector) > 2 - ) { - playerState.position.x = -5000; - playerState.position.y = 5000; - } - playerState.v = { x: 0, y: 0 }; - playerState.iv = { x: 0, y: 0 }; - playerState.ir = 0; - state.players.set(client.id, playerState); - ws.send( - JSON.stringify({ - type: "warp", - payload: { - to: checkpoint.sector, - asteroids: Array.from(state.asteroids.values()), - collectables: Array.from(state.collectables.values()), - mines: Array.from(state.mines.values()), - sectorInfos: [], - }, - }) - ); - client.currentSector = checkpoint.sector; - }); - } - } else if (data.type === "target") { - const client = clients.get(ws); - if (client) { - targets.set(client.id, data.payload.target); - } - } else if (data.type === "secondary") { - const client = clients.get(ws); - if (client) { - if (typeof data.payload.secondary === "number" && data.payload.secondary >= 0) { - secondaries.set(client.id, data.payload.secondary); - } - } - } else if (data.type === "secondaryActivation") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player) { - if (typeof data.payload.secondary === "number" && data.payload.secondary < player.arms.length && data.payload.secondary >= 0) { - secondariesToActivate.get(client.id)?.push(data.payload.secondary); - } - } - } - } else if (data.type === "sellCargo") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player && player.cargo) { - if (player.credits === undefined) { - player.credits = 0; - } - const price = market.get(data.payload.what); - if (price) { - player.credits += removeAtMostCargo(player, data.payload.what, Math.round(data.payload.amount)) * price; - } - } - } - } else if (data.type === "transferToShip") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player) { - transferToShip(ws, player, data.payload.what, Math.round(data.payload.amount), flashServerMessage); - } - } - } else if (data.type === "sellInventory") { - const client = clients.get(ws); - if (client) { - const player = sectors.get(client.currentSector)!.players.get(client.id); - if (player) { - sellInventory(ws, player, data.payload.what, Math.round(data.payload.amount)); - } - } - } else if (data.type === "depositCargo") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player && player.cargo) { - depositCargo(player, data.payload.what, Math.round(data.payload.amount), ws); - } - } - } else if (data.type === "dumpCargo") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player && player.cargo) { - removeAtMostCargo(player, data.payload.what, Math.round(data.payload.amount)); - } - } - } else if (data.type === "equip") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player) { - // equip does the bounds checking for the index for us - let newPlayer = equip(player, data.payload.slotIndex, data.payload.what, data.payload.fromInventory); - if (newPlayer !== player) { - state.players.set(client.id, newPlayer); - const toTake = data.payload.fromInventory ? [armDefs[newPlayer.arms[data.payload.slotIndex]].name] : []; - // There is technically a bug here, if the player equips and then logs off, but the database has an error after they log off then - // they what is deposited will be lost. I don't want to deal with it though (the correct thing is to pull their save from the database - // and deal with it that way, but if we just had a database error this is unlikely to work anyways) - depositItemsIntoInventory(ws, player, [armDefs[player.arms[data.payload.slotIndex]].name], toTake, flashServerMessage, () => { - console.log("Error depositing armament into inventory, reverting player"); - try { - const otherState = sectors.get(clients.get(idToWebsocket.get(player.id)!)!.currentSector)!; - otherState.players.set(player.id, player); - } catch (e) { - console.log("Warning: unable to revert player" + e); - } - }); - } - } - } - } else if (data.type === "chat") { - const client = clients.get(ws); - if (client) { - data.payload.message = data.payload.message.trim().substring(0, 200); - for (const [otherClient, otherClientData] of clients) { - if (otherClientData.currentSector === client.currentSector) { - otherClient.send(JSON.stringify({ type: "chat", payload: { id: client.id, message: data.payload.message } })); - } - } - } - } else if (data.type === "manufacture") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player) { - manufacture(ws, player, data.payload.what, Math.round(data.payload.amount), flashServerMessage); - } - } - } else if (data.type === "compositeManufacture") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player) { - compositeManufacture(ws, player, data.payload.what, Math.round(data.payload.amount), flashServerMessage); - } - } - } else if (data.type === "purchase") { - const client = clients.get(ws); - if (client) { - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player) { - Station.findOne({ id: player.docked }, (err, station) => { - if (err) { - ws.send(JSON.stringify({ type: "error", payload: { message: "Server error loading station" } })); - console.log("Error loading station: " + err); - return; - } - if (!station) { - ws.send(JSON.stringify({ type: "error", payload: { message: "Station not found" } })); - console.log("Error loading station: " + err); - return; - } - const newPlayer = purchaseShip(player, data.payload.index, station.shipsAvailable, data.payload.fromInventory); - if (newPlayer !== player) { - state.players.set(client.id, newPlayer); - const items = [defs[player.defIndex].name]; - if (player.arms) { - for (const armIndex of player.arms) { - items.push(armDefs[armIndex].name); - } - } - const toTake = data.payload.fromInventory ? [defs[newPlayer.defIndex].name] : []; - // There is technically a bug here, if the player equips and then logs off, but the database has an error after they log off then - // they what is deposited will be lost. I don't want to deal with it though (the correct thing is to pull their save from the database - // and deal with it that way, but if we just had a database error this is unlikely to work anyways) - depositItemsIntoInventory(ws, player, items, toTake, flashServerMessage, () => { - console.log("Error depositing ship into inventory, reverting player"); - try { - const otherState = sectors.get(clients.get(idToWebsocket.get(player.id)!)!.currentSector)!; - otherState.players.set(player.id, player); - } catch (e) { - console.log("Warning: unable to revert player" + e); - } - }); - } - }); - } - } - } else if (data.type === "warp") { - const client = clients.get(ws); - if (client) { - if (client.currentSector !== data.payload.warpTo && sectorList.includes(data.payload.warpTo)) { - if (!client.sectorsVisited.has(data.payload.warpTo)) { - flashServerMessage(client.id, "You must visit a sector before you can warp to it"); - return; - } - const state = sectors.get(client.currentSector)!; - const player = state.players.get(client.id); - if (player) { - player.warpTo = data.payload.warpTo; - player.warping = 1; - } - } - } - } else if (data.type === "tutorialStageComplete") { - const client = clients.get(ws); - if (client) { - if (client.inTutorial === data.payload.stage) { - if (client.inTutorial !== data.payload.stage) { - ws.send(JSON.stringify({ type: "error", payload: { message: "Tutorial stage mismatch" } })); - } - client.inTutorial = advanceTutorialStage(client.id, data.payload.stage, ws); - sendTutorialStage(ws, client.inTutorial); - } - } - } else if (data.type === "selectMission") { - const client = clients.get(ws); - if (client) { - if (client.inTutorial) { - flashServerMessage(client.id, "You cannot select a mission while in the tutorial", [1.0, 0.0, 0.0, 1.0]); - return; - } - const state = sectors.get(client.currentSector); - if (state) { - const player = state.players.get(client.id); - if (player) { - selectMission(ws, player, data.payload.missionId); - } - } - } - } else if (data.type === "startMission") { - const client = clients.get(ws); - if (client) { - if (client.inTutorial) { - flashServerMessage(client.id, "You cannot start a mission while in the tutorial", [1.0, 0.0, 0.0, 1.0]); - return; - } - const state = sectors.get(client.currentSector); - if (state) { - const player = state.players.get(client.id); - if (player) { - startPlayerInMission(ws, player, data.payload.missionId); - } - } - } - } else if (data.type === "friendRequest") { - const client = clients.get(ws); - if (client) { - createFriendRequest(ws, client.id, data.payload.name); - } - } else if (data.type === "revokeFriendRequest") { - const client = clients.get(ws); - if (client) { - revokeFriendRequest(ws, client.id, data.payload.name); - } - } else if (data.type === "unfriend") { - const client = clients.get(ws); - if (client) { - unfriend(ws, client.id, data.payload.id); - } - } else if (data.type === "friendWarp") { - const client = clients.get(ws); - if (client) { - const player = sectors.get(client.currentSector)?.players.get(client.id); - if (player) { - friendWarp(ws, player, data.payload.id); - } - } - } else { - console.log("Unknown message from client: ", data); - } - } catch (e) { - console.log("Error in message handler: " + e); - appendFile("errorlog", `Error: ${e}\nmessage: ${msg}\n${inspect(clients, { depth: null })}\n${Array.from(sectors.values())}\n`, (err) => { - if (err) { - console.log("Error writing to log: " + err); - } - }); - } - }); - - ws.on("close", () => { - const removedClient = clients.get(ws); - if (removedClient) { - const player = sectors.get(removedClient.currentSector)?.players.get(removedClient.id); - const state = sectors.get(removedClient.currentSector)!; - state.players.delete(removedClient.id); - targets.delete(removedClient.id); - secondaries.delete(removedClient.id); - secondariesToActivate.delete(removedClient.id); - clients.delete(ws); - idToWebsocket.delete(removedClient.id); - knownRecipes.delete(removedClient.id); - if (player) { - if (player.docked) { - if (!removedClient.inTutorial) { - saveCheckpoint(removedClient.id, removedClient.currentSector, player, removedClient.sectorsVisited, true); - } - } else { - User.findOneAndUpdate( - { id: removedClient.id }, - { - $set: { sectorsVisited: Array.from(removedClient.sectorsVisited), currentSector: removedClient.currentSector }, - $push: { logoffTimes: Date.now() }, - }, - (err) => { - if (err) { - console.log("Error saving user: " + err); - } - } - ); - } - } else if (!player) { - console.log("Warning: player not found on disconnect"); - } - } - }); -}); - -const interval = setInterval(function ping() { - wss.clients.forEach(function each(ws) { - if ((ws as any).isAlive === false) return ws.terminate(); - - (ws as any).isAlive = false; - ws.ping(); - }); -}, 30000); - -wss.on("close", function close() { - clearInterval(interval); -}); +import { CardinalDirection, mirrorAngleHorizontally, mirrorAngleVertically } from "../src/geometry"; +import { allyCount, enemyCount, flashServerMessage } from "./stateHelpers"; +import { peerMap, serversForSectors, setPlayerSector } from "./peers"; +import { WebSocket } from "ws"; +import { User } from "./dataModels"; +import { mapGraph, mapHeight, mapWidth } from "../src/mapLayout"; +import { transferableActions } from "./transferableActions"; const informDead = (player: Player) => { if (player.npc) { @@ -990,184 +122,14 @@ const discoverer = (id: number, recipe: string) => { } }; -// Updating the game state -setInterval(() => { - frame++; - const sectorTransitions: SectorTransition[] = []; - - for (const [sector, state] of sectors) { - for (const [client, data] of clients) { - const player = state.players.get(data.id); - if (data.input && player) { - applyInputs(data.input, player, data.angle); - } - } - const triggers: EffectTrigger[] = []; - const mutated = update( - state, - frame, - targets, - secondaries, - (trigger) => triggers.push(trigger), - warpList, - informDead, - flashServerMessage, - (id, collected) => removeCollectable(sector, id, collected), - (id, detonated) => removeMine(sector, id, detonated), - knownRecipes, - discoverer, - secondariesToActivate - ); - processAllNpcs(state, sector); - findSectorTransitions(state, sector, sectorTransitions); - - // TODO Consider culling the state information to only send nearby players and projectiles (this trades networking bandwidth for server CPU) - // TODO I should not be sending the players out of range or the cloaked players to the clients that should not be able to have that information - const playerData: Player[] = []; - const npcs: (NPC | undefined)[] = []; - for (const player of state.players.values()) { - npcs.push(player.npc); - player.npc = undefined; - playerData.push(player); - } - - const projectileData: Ballistic[] = Array.from(state.projectiles.values()); - let asteroidData: Asteroid[] = state.asteroidsDirty ? Array.from(state.asteroids.values()) : Array.from(mutated.asteroids); - const missileData: Missile[] = Array.from(state.missiles.values()); - - const serialized = JSON.stringify({ - type: "state", - payload: { - players: playerData, - frame, - projectiles: projectileData, - asteroids: asteroidData, - effects: triggers, - missiles: missileData, - collectables: mutated.collectables, - mines: mutated.mines, - }, - }); - - for (const [client, data] of clients) { - if (data.currentSector === sector) { - client.send(serialized); - } - } - for (const player of state.players.values()) { - player.npc = npcs.shift()!; - } - - if (frame % 60 === 0) { - const trigger = sectorTriggers.get(sector); - if (trigger) { - trigger(state); - } - } - } - - // Handle all sector transitions - for (const transition of sectorTransitions) { - const newSector = sectorInDirection(transition.from, transition.direction) ?? transition.from; - - if (newSector === transition.from) { - if (transition.direction === CardinalDirection.Up) { - transition.player.position.y = sectorBounds.y + 200; - transition.direction = CardinalDirection.Down; - transition.player.heading = mirrorAngleHorizontally(transition.player.heading); - } else if (transition.direction === CardinalDirection.Down) { - transition.player.position.y = sectorBounds.y + sectorBounds.height - 200; - transition.direction = CardinalDirection.Up; - transition.player.heading = mirrorAngleHorizontally(transition.player.heading); - } else if (transition.direction === CardinalDirection.Left) { - transition.player.position.x = sectorBounds.x + 200; - transition.direction = CardinalDirection.Right; - transition.player.heading = mirrorAngleVertically(transition.player.heading); - } else if (transition.direction === CardinalDirection.Right) { - transition.player.position.x = sectorBounds.x + sectorBounds.width - 200; - transition.direction = CardinalDirection.Left; - transition.player.heading = mirrorAngleVertically(transition.player.heading); - } - } - - const state = sectors.get(newSector); - if (state) { - const ws = idToWebsocket.get(transition.player.id); - const sectorInfo = { - sector: newSector, - resources: newSector < mapSize * mapSize ? sectorAsteroidResources[newSector].map((value) => value.resource) : [], - }; - if (ws) { - const client = clients.get(ws)!; - client.currentSector = newSector; - if (newSector < mapSize * mapSize) { - client.sectorsVisited.add(newSector); - } - ws.send( - JSON.stringify({ - type: "warp", - payload: { - to: newSector, - asteroids: Array.from(state.asteroids.values()), - collectables: Array.from(state.collectables.values()), - mines: Array.from(state.mines.values()), - sectorInfos: [sectorInfo], - }, - }) - ); - } - transition.player.position = transition.coords; - // transition.player.heading = headingFromCardinalDirection(transition.direction); - state.players.set(transition.player.id, transition.player); - } - } - - // Handle all warps - while (warpList.length > 0) { - const { player, to } = warpList.shift()!; - const state = sectors.get(to); - if (state) { - const ws = idToWebsocket.get(player.id); - if (ws) { - const client = clients.get(ws)!; - client.currentSector = to; - ws.send( - JSON.stringify({ - type: "warp", - payload: { - to, - asteroids: Array.from(state.asteroids.values()), - collectables: Array.from(state.collectables.values()), - mines: Array.from(state.mines.values()), - sectorInfos: [], - }, - }) - ); - // const enemies = enemyCount(player.team, to); - // const allies = allyCount(player.team, to); - // const count = enemies - allies; - // if (count > 3 && isEnemySector(player.team, to)) { - // spawnAllyForces(player.team, to, count); - // flashServerMessage(player.id, `${getFactionString(player.team)} forces have arrived to assist!`); - // } - } - // Lets just keep the coords and momentum of the player on warping - // player.position.x = Math.random() * 6000 - 3000; - // player.position.y = Math.random() * 6000 - 3000; - // player.heading = (3 * Math.PI) / 2; - // player.speed = 0; - state.players.set(player.id, player); - } - } -}, 1000 / ticksPerSecond); - const spawnIncrementalGuardians = (sector: number) => { const state = sectors.get(sector); if (!state) { return; } - const faction = sectorFactions[sector]; + // const faction = sectorFactions[sector]; + const faction = Math.floor(Math.random() * 4) as Faction; if (faction === null) { return; } @@ -1183,22 +145,22 @@ const spawnIncrementalGuardians = (sector: number) => { switch (faction) { case Faction.Alliance: for (let i = 0; i < count; i++) { - addNpc(state, Math.random() > 0.5 ? "Fighter" : "Advanced Fighter", Faction.Alliance, uid(), friendlySectors(faction)); + addNpc(state, Math.random() > 0.5 ? "Fighter" : "Advanced Fighter", Faction.Alliance, uid()); } break; case Faction.Confederation: for (let i = 0; i < count; i++) { - addNpc(state, Math.random() > 0.5 ? "Drone" : "Seeker", Faction.Confederation, uid(), friendlySectors(faction)); + addNpc(state, Math.random() > 0.5 ? "Drone" : "Seeker", Faction.Confederation, uid()); } break; case Faction.Rogue: for (let i = 0; i < count; i++) { - addNpc(state, Math.random() > 0.2 ? "Strafer" : "Venture", Faction.Rogue, uid(), friendlySectors(faction)); + addNpc(state, Math.random() > 0.2 ? "Strafer" : "Venture", Faction.Rogue, uid()); } break; case Faction.Scourge: for (let i = 0; i < count; i++) { - addNpc(state, Math.random() > 0.5 ? "Spartan" : "Striker", Faction.Scourge, uid(), friendlySectors(faction)); + addNpc(state, Math.random() > 0.5 ? "Spartan" : "Striker", Faction.Scourge, uid()); } } }; @@ -1209,13 +171,15 @@ const spawnSectorGuardians = (sector: number) => { return; } - const faction = sectorFactions[sector]; + // const faction = sectorFactions[sector]; + const faction: Faction = Math.floor(Math.random() * 4) as Faction; if (faction === null) { return; } const allies = allyCount(faction, sector); - const count = sectorGuardianCount[sector] - allies; + // const count = sectorGuardianCount[sector] - allies; + const count = 0; if (count <= 0) { return; } @@ -1234,60 +198,39 @@ const spawnSectorGuardians = (sector: number) => { switch (faction) { case Faction.Alliance: for (let i = 0; i < count; i++) { - addNpc(state, Math.random() > 0.5 ? "Fighter" : "Advanced Fighter", Faction.Alliance, uid(), friendlySectors(faction)); + addNpc(state, Math.random() > 0.5 ? "Fighter" : "Advanced Fighter", Faction.Alliance, uid()); } break; case Faction.Confederation: for (let i = 0; i < count; i++) { - addNpc(state, Math.random() > 0.5 ? "Drone" : "Seeker", Faction.Confederation, uid(), friendlySectors(faction)); + addNpc(state, Math.random() > 0.5 ? "Drone" : "Seeker", Faction.Confederation, uid()); } break; case Faction.Rogue: for (let i = 0; i < count; i++) { - addNpc(state, Math.random() > 0.2 ? "Strafer" : "Venture", Faction.Rogue, uid(), friendlySectors(faction)); + addNpc(state, Math.random() > 0.2 ? "Strafer" : "Venture", Faction.Rogue, uid()); } break; case Faction.Scourge: for (let i = 0; i < count; i++) { - addNpc(state, Math.random() > 0.5 ? "Spartan" : "Striker", Faction.Scourge, uid(), friendlySectors(faction)); + addNpc(state, Math.random() > 0.5 ? "Spartan" : "Striker", Faction.Scourge, uid()); } } }; -setInterval(() => { - for (let i = 0; i < sectorList.length; i++) { - spawnIncrementalGuardians(i); - } -}, 20 * 990); - -setInterval(() => { - for (let i = 0; i < sectorList.length; i++) { - spawnSectorGuardians(i); - } -}, 120 * 60 * 1000); - -const repairStationsInSectorForTeam = (sector: number, team: Faction) => { +const repairStationsInSector = (sector: number) => { const state = sectors.get(sector); if (!state) { return; } for (const player of state.players.values()) { - if (player.inoperable) { + if (player.inoperable && stationIdToDefaultTeam.has(player.id)) { + const team = stationIdToDefaultTeam.get(player.id)!; player.repairs![team] += 1; } } }; -setInterval(() => { - for (const sector of sectorList) { - const faction = sectorFactions[sector]; - if (faction === null) { - continue; - } - repairStationsInSectorForTeam(sector, faction); - } -}, 2 * 60 * 1000); - const respawnEmptyAsteroids = (state: GlobalState, sector: number) => { let removedCount = 0; const removed: number[] = []; @@ -1305,7 +248,8 @@ const respawnEmptyAsteroids = (state: GlobalState, sector: number) => { sectorBounds, Date.now(), uid, - sectorAsteroidResources[sectorList.findIndex((s) => s === sector)], + // sectorAsteroidResources[sectorList.findIndex((s) => s === sector)], + allResources, Array.from(state.players.values()).filter((a) => { const def = defs[a.defIndex]; return def.kind === UnitKind.Station; @@ -1323,12 +267,319 @@ const respawnEmptyAsteroids = (state: GlobalState, sector: number) => { } }; -setInterval(() => { - for (const [sector, state] of sectors) { - respawnEmptyAsteroids(state, sector); +const warpNonNPCToSector = (ws: WebSocket, player: Player, sector: number) => { + setPlayerSector(player.id, sector); + const state = sectors.get(sector); + if (state) { + ws.send( + JSON.stringify({ + type: "warp", + payload: { + to: sector, + asteroids: Array.from(state.asteroids.values()), + collectables: Array.from(state.collectables.values()), + mines: Array.from(state.mines.values()), + sectorInfos: [], + }, + }) + ); + state.players.set(player.id, player); + } else { + const serverName = serversForSectors.get(sector); + if (serverName) { + serverChangePlayer(ws, player, serverName); + } else { + flashServerMessage(player.id, `Server not found for this sector! (${sector})`, [1.0, 0.0, 0.0, 1.0]); + } + } +}; + +const warpNPCToSector = (player: Player, sector: number) => { + const state = sectors.get(sector); + if (state) { + state.players.set(player.id, player); + } else { + const peerSockets = peerMap.get(serversForSectors.get(sector)!); + if (peerSockets) { + (player as any).sector = sector; + (player as any).npcReconstructionKey = Object.getPrototypeOf(player.npc).constructor.name; + (player as any).input = player.npc!.input; + player.npc = undefined; + peerSockets.request.send("npc-transfer", player, (success: string) => { + if (success !== "OK") { + console.log("Error transferring npc: " + success); + } + }); + } else { + console.log(`Server not found for this sector! (${sector})`); + } + } +}; + +const insertRespawnedPlayer = (ws: WebSocket, player: Player, sector: number) => { + const state = sectors.get(sector); + if (!state) { + ws.send(JSON.stringify({ type: "error", payload: { message: "Sector not found on server for respawn" } })); + console.log("Warning: Sector not found on server for respawn"); + return; + } + // So I don't have to edit the checkpoints in the database right now + player.isPC = true; + if (isNearOperableEnemyStation(player, state.players.values()) || enemyCount(player.team, sector) - allyCount(player.team, sector) > 2) { + player.position.x = -5000; + player.position.y = 5000; + } + player.v = { x: 0, y: 0 }; + player.iv = { x: 0, y: 0 }; + player.ir = 0; + state.players.set(player.id, player); + ws.send( + JSON.stringify({ + type: "warp", + payload: { + to: sector, + asteroids: Array.from(state.asteroids.values()), + collectables: Array.from(state.collectables.values()), + mines: Array.from(state.mines.values()), + sectorInfos: [], + }, + }) + ); +}; + +const respawnPlayer = (ws: WebSocket, player: Player, sector: number) => { + setPlayerSector(player.id, sector); + if (sectors.has(sector)) { + insertRespawnedPlayer(ws, player, sector); + } else { + const newServerName = serversForSectors.get(sector); + if (newServerName) { + serverChangePlayer(ws, player, newServerName, ServerChangeKind.Respawn); + } else { + flashServerMessage(player.id, "Server not found for this sector!", [1.0, 0.0, 0.0, 1.0]); + } + } +}; + +const insertSpawnedPlayer = (ws: WebSocket, player: Player, sector: number) => { + const state = sectors.get(sector); + if (!state) { + ws.send(JSON.stringify({ type: "error", payload: { message: "Sector missing from server for spawn" } })); + console.log("Warning: Sector missing from server for spawn"); + return; + } + if (isNearOperableEnemyStation(player, state.players.values()) || enemyCount(player.team, sector) > 2) { + player.position.x = -5000; + player.position.y = 5000; + } + state.players.set(player.id, player); + + const client = clients.get(ws); + if (!client) { + console.log("Warning: Client not found for spawn"); + return; } -}, 1 * 60 * 1000); -server.listen(wsPort, () => { - console.log(`${useSsl ? "Secure" : "Unsecure"} websocket server running on port ${wsPort}`); -}); + const sectorInfos = Array.from(client.sectorsVisited).map((sector) => ({ + sector, + resources: sectorAsteroidResources[sector].map((r) => r.resource), + })); + + // console.log("Sector info for player is ", sectorInfos, client.sectorsVisited); + + ws.send( + JSON.stringify({ + type: "init", + payload: { + id: player.id, + sector: sector, + faction: player.team, + asteroids: Array.from(state.asteroids.values()), + collectables: Array.from(state.collectables.values()), + mines: Array.from(state.mines.values()), + sectorInfos, + recipes: Array.from(knownRecipes.get(player.id) || []), + }, + }) + ); + sendInventory(ws, player.id); +}; + +const spawnPlayer = (ws: WebSocket, player: Player, sector: number) => { + setPlayerSector(player.id, sector); + if (sectors.has(sector)) { + insertSpawnedPlayer(ws, player, sector); + } else { + const newServerName = serversForSectors.get(sector); + if (newServerName) { + serverChangePlayer(ws, player, newServerName, ServerChangeKind.Spawn); + } else { + flashServerMessage(player.id, "Server not found for this sector!", [1.0, 0.0, 0.0, 1.0]); + } + } +}; + +const setupTimers = () => { + setInterval(() => { + for (let i = 0; i < sectorList.length; i++) { + spawnIncrementalGuardians(i); + } + }, 20 * 990); + + setInterval(() => { + for (let i = 0; i < sectorList.length; i++) { + spawnSectorGuardians(i); + } + }, 120 * 60 * 1000); + + setInterval(() => { + for (const sector of sectorList) { + repairStationsInSector(sector); + } + }, 20 * 1000); + + setInterval(() => { + for (const [sector, state] of sectors) { + respawnEmptyAsteroids(state, sector); + } + }, 1 * 60 * 1000); + + // Updating the game state + setInterval(() => { + frame++; + const sectorTransitions: SectorTransition[] = []; + + for (const [sector, state] of sectors) { + for (const [client, data] of clients) { + const player = state.players.get(data.id); + if (data.input && player) { + applyInputs(data.input, player, data.angle); + } + } + const triggers: EffectTrigger[] = []; + const mutated = update( + state, + frame, + targets, + secondaries, + (trigger) => triggers.push(trigger), + warpList, + informDead, + flashServerMessage, + (id, collected) => removeCollectable(sector, id, collected), + (id, detonated) => removeMine(sector, id, detonated), + knownRecipes, + discoverer, + secondariesToActivate, + transferableActions, + sector + ); + processAllNpcs(state, sector); + findSectorTransitions(state, sector, sectorTransitions); + + // TODO Consider culling the state information to only send nearby players and projectiles (this trades networking bandwidth for server CPU) + // TODO I should not be sending the players out of range or the cloaked players to the clients that should not be able to have that information + const playerData: Player[] = []; + const npcs: (NPC | undefined)[] = []; + for (const player of state.players.values()) { + npcs.push(player.npc); + player.npc = undefined; + playerData.push(player); + } + + const projectileData: Ballistic[] = Array.from(state.projectiles.values()); + let asteroidData: Asteroid[] = state.asteroidsDirty ? Array.from(state.asteroids.values()) : Array.from(mutated.asteroids); + const missileData: Missile[] = Array.from(state.missiles.values()); + + const serialized = JSON.stringify({ + type: "state", + payload: { + players: playerData, + frame, + projectiles: projectileData, + asteroids: asteroidData, + effects: triggers, + missiles: missileData, + collectables: mutated.collectables, + mines: mutated.mines, + }, + }); + + for (const [client, data] of clients) { + if (data.currentSector === sector) { + client.send(serialized); + } + } + for (const player of state.players.values()) { + player.npc = npcs.shift()!; + } + + if (frame % 60 === 0) { + for (let i = 0; i < state.sectorChecks!.length; i++) { + const check = state.sectorChecks![i]; + const toRemove = transferableActions[check.index](state, sector, check.data); + if (toRemove) { + state.sectorChecks!.splice(i, 1); + i--; + } + } + } + } + + // Handle all sector transitions + for (const transition of sectorTransitions) { + const newSector = mapGraph.get(transition.from)?.out[transition.direction]?.to?.sector ?? transition.from; + // console.log("Transitioning player", transition.player.id, "from sector", transition.from, "to sector", newSector); + + if (newSector === transition.from) { + if (transition.direction === CardinalDirection.Up) { + transition.player.position.y = sectorBounds.y + 200; + transition.direction = CardinalDirection.Down; + transition.player.heading = mirrorAngleHorizontally(transition.player.heading); + } else if (transition.direction === CardinalDirection.Down) { + transition.player.position.y = sectorBounds.y + sectorBounds.height - 200; + transition.direction = CardinalDirection.Up; + transition.player.heading = mirrorAngleHorizontally(transition.player.heading); + } else if (transition.direction === CardinalDirection.Left) { + transition.player.position.x = sectorBounds.x + 200; + transition.direction = CardinalDirection.Right; + transition.player.heading = mirrorAngleVertically(transition.player.heading); + } else if (transition.direction === CardinalDirection.Right) { + transition.player.position.x = sectorBounds.x + sectorBounds.width - 200; + transition.direction = CardinalDirection.Left; + transition.player.heading = mirrorAngleVertically(transition.player.heading); + } + } + + const ws = idToWebsocket.get(transition.player.id); + transition.player.position = transition.coords; + if (ws) { + const client = clients.get(ws)!; + client.currentSector = newSector; + if (newSector < mapWidth * mapHeight) { + client.sectorsVisited.add(newSector); + } + warpNonNPCToSector(ws, transition.player, newSector); + } else { + // Is npc + warpNPCToSector(transition.player, newSector); + } + } + + // Handle all warps + while (warpList.length > 0) { + const { player, to } = warpList.shift()!; + const ws = idToWebsocket.get(player.id); + if (ws) { + const client = clients.get(ws)!; + client.currentSector = to; + warpNonNPCToSector(ws, player, to); + } else { + // Is npc + // warpNPCToSector(player, to); + } + } + }, 1000 / ticksPerSecond); +}; + +export { setupTimers, respawnPlayer, insertRespawnedPlayer, spawnPlayer, insertSpawnedPlayer }; diff --git a/server/state.ts b/server/state.ts index a2e4bca..10793b3 100644 --- a/server/state.ts +++ b/server/state.ts @@ -5,19 +5,29 @@ import { Player, randomAsteroids, TargetKind, - mapSize, sectorBounds, TutorialStage, - copyPlayer, removeCargoFractions, SectorKind, + Ballistic, + Asteroid, + Missile, + Collectable, + Mine, + TransferableAction, } from "../src/game"; import { WebSocket } from "ws"; -import { armDefs, defs, Faction, initDefs, UnitKind } from "../src/defs"; +import { defs, Faction, initDefs, UnitKind } from "../src/defs"; import { CardinalDirection } from "../src/geometry"; -import { market, initMarket } from "./market"; -import { NPC } from "../src/npc"; -import { Checkpoint, User } from "./dataModels"; +import { initMarket } from "./market"; +import { NPC, npcReconstructors } from "./npcs/npc"; +import { Checkpoint, Station, User } from "./dataModels"; +import { awareSectors, peerMap, PeerSockets, removeNetworkAwareness, waitingData } from "./peers"; +import { insertRespawnedPlayer, insertSpawnedPlayer } from "./server"; +import { ISector, Sector } from "./sector"; +import { HydratedDocument } from "mongoose"; +import { mapHeight, mapWidth, ResourceDensity } from "../src/mapLayout"; +import { DelayedAction } from "../src/defs/delayedAction"; // Initialize the definitions (Do this before anything else to avoid problems) initDefs(); @@ -31,104 +41,16 @@ const uid = () => { return ret; }; -// This data will ultimately be stored in the database -// TODO Make the sector list have names like 1-1, 1-2, 2-1, 2-2, etc. -const sectorList = new Array(mapSize * mapSize).fill(0).map((_, i) => i); -const sectorAsteroidResources = sectorList.map((_) => [{ resource: "Prifecite", density: 1 }]); -const sectorAsteroidCounts = sectorList.map((_) => 15); +const sectorList: number[] = []; +const sectorAsteroidResources: ResourceDensity[][] = []; +const sectorAsteroidCounts: number[] = []; -sectorAsteroidResources[0] = [ +const allResources = [ + { resource: "Prifecite", density: 1 }, { resource: "Russanite", density: 1 }, - { resource: "Hemacite", density: 1 }, -]; -sectorAsteroidResources[1] = [ { resource: "Aziracite", density: 1 }, { resource: "Hemacite", density: 1 }, ]; -sectorAsteroidResources[2] = [ - { resource: "Aziracite", density: 1 }, - { resource: "Hemacite", density: 1 }, -]; -sectorAsteroidResources[3] = [ - { resource: "Russanite", density: 1 }, - { resource: "Hemacite", density: 1 }, -]; - -sectorAsteroidResources[5] = [ - { resource: "Prifecite", density: 1 }, - { resource: "Russanite", density: 1 }, -]; -sectorAsteroidResources[6] = [ - { resource: "Prifecite", density: 1 }, - { resource: "Russanite", density: 1 }, -]; - -sectorAsteroidCounts[6] = 35; -sectorAsteroidCounts[1] = 22; -sectorAsteroidCounts[2] = 22; - -sectorAsteroidCounts[12] = 30; -sectorAsteroidCounts[15] = 30; - -const sectorFactions: (Faction | null)[] = sectorList.map((_) => null); -sectorFactions[0] = Faction.Scourge; -sectorFactions[3] = Faction.Scourge; - -sectorFactions[1] = Faction.Rogue; -sectorFactions[2] = Faction.Rogue; -sectorFactions[5] = Faction.Rogue; -sectorFactions[6] = Faction.Rogue; - -sectorFactions[12] = Faction.Alliance; -sectorFactions[13] = Faction.Alliance; -sectorFactions[8] = Faction.Alliance; -sectorFactions[4] = Faction.Alliance; -sectorFactions[9] = Faction.Alliance; - -sectorFactions[14] = Faction.Confederation; -sectorFactions[15] = Faction.Confederation; -sectorFactions[11] = Faction.Confederation; -sectorFactions[7] = Faction.Confederation; -sectorFactions[10] = Faction.Confederation; - -const friendlySectors = (faction: Faction) => { - const ret: number[] = []; - for (let i = 0; i < sectorFactions.length; i++) { - if (sectorFactions[i] === faction) { - ret.push(i); - } - } - return ret; -}; - -const sectorGuardianCount = sectorList.map((_) => 0); - -sectorGuardianCount[0] = 6; -sectorGuardianCount[3] = 6; - -sectorGuardianCount[1] = 6; -sectorGuardianCount[2] = 6; -sectorGuardianCount[5] = 15; -sectorGuardianCount[6] = 15; - -sectorGuardianCount[12] = 24; -sectorGuardianCount[13] = 15; -sectorGuardianCount[8] = 15; -sectorGuardianCount[4] = 6; -sectorGuardianCount[9] = 6; - -sectorGuardianCount[14] = 15; -sectorGuardianCount[15] = 24; -sectorGuardianCount[11] = 15; -sectorGuardianCount[7] = 6; -sectorGuardianCount[10] = 6; - -const sectorHasStarbase = sectorList.map((_) => false); -sectorHasStarbase[5] = true; - -sectorHasStarbase[12] = true; - -sectorHasStarbase[15] = true; type ClientData = { id: number; @@ -143,38 +65,173 @@ type ClientData = { tutorialNpc?: NPC; }; -/* - x -> - y 0 1 2 3 - | 4 5 6 7 - v 8 9 10 11 - 12 13 14 15 -*/ - -const sectorInDirection = (sector: number, direction: CardinalDirection) => { - if (sector >= mapSize * mapSize) { - return null; - } - const x = sector % mapSize; - const y = Math.floor(sector / mapSize); - if (direction === CardinalDirection.Up) { - if (y === 0) return null; - return sector - mapSize; - } else if (direction === CardinalDirection.Down) { - if (y === mapSize - 1) return null; - return sector + mapSize; - } else if (direction === CardinalDirection.Left) { - if (x === 0) return null; - return sector - 1; - } else if (direction === CardinalDirection.Right) { - if (x === mapSize - 1) return null; - return sector + 1; - } - return null; +type SerializableClientData = Omit & { + sectorsVisited: number[]; + tutorialNpcId?: number; +}; + +const serializableClientData = (client: ClientData): SerializableClientData => { + client = { ...client }; + (client as unknown as SerializableClientData).tutorialNpcId = client.tutorialNpc?.player.id; + client.tutorialNpc = undefined; + (client as unknown as SerializableClientData).sectorsVisited = Array.from(client.sectorsVisited); + return client as unknown as SerializableClientData; +}; + +const repairClientData = (client: SerializableClientData): ClientData => { + const ret = { ...client } as unknown as ClientData; + ret.sectorsVisited = new Set(client.sectorsVisited); + return ret; +}; + +const getTutorialNpc = (client: ClientData, state: GlobalState): NPC | undefined => { + if (client.tutorialNpc) return client.tutorialNpc; + const id = (client as unknown as SerializableClientData).tutorialNpcId; + if (id === undefined) return undefined; + return state.players.get(id)?.npc; }; const clients: Map = new Map(); const idToWebsocket = new Map(); +// Targeting is handled by the clients, but the server needs to know +// Same pattern with secondaries +// BTW I do not like this design +const targets: Map = new Map(); +const secondaries: Map = new Map(); +const secondariesToActivate: Map = new Map(); +const knownRecipes: Map> = new Map(); + +enum ServerChangeKind { + Warp, + Respawn, + Spawn, +} + +const serializeAllClientData = (ws: WebSocket, player: Player, key: string, kind: ServerChangeKind): SerializableClient | null => { + const client = clients.get(ws); + if (!client) return null; + const target = targets.get(client.id); + const secondary = secondaries.get(client.id); + const toActivate = secondariesToActivate.get(client.id); + const recipesKnown = knownRecipes.get(client.id) || new Set(); + + return { + clientData: serializableClientData(client), + target, + secondary, + toActivate, + recipesKnown: Array.from(recipesKnown), + player, + key, + kind, + }; +}; + +type SerializableClient = { + clientData: SerializableClientData; + target: [TargetKind, number] | undefined; + secondary: number | undefined; + toActivate: number[] | undefined; + recipesKnown: string[]; + player: Player; + key: string; + kind: ServerChangeKind; +}; + +const deserializeClientData = (ws: WebSocket, data: SerializableClient) => { + const client = repairClientData(data.clientData); + const sector = sectors.get(client.currentSector); + if (!sector) { + console.warn("Missing sector", client.currentSector); + return; + } + clients.set(ws, client); + idToWebsocket.set(client.id, ws); + if (data.target) { + targets.set(client.id, data.target); + } else { + console.warn("Missing client target"); + } + if (data.secondary) { + secondaries.set(client.id, data.secondary); + } else { + console.warn("Missing client secondary"); + } + if (data.toActivate) { + secondariesToActivate.set(client.id, data.toActivate); + } else { + console.warn("Missing client toActivate"); + } + if (data.recipesKnown) { + knownRecipes.set(client.id, new Set(data.recipesKnown)); + } else { + console.warn("Missing client recipesKnown"); + } + switch (data.kind) { + case ServerChangeKind.Warp: + sector.players.set(client.id, data.player); + const sectorInfo = { + sector: client.currentSector, + resources: sectorAsteroidResources[client.currentSector].map((resDen) => resDen.resource), + }; + ws.send( + JSON.stringify({ + type: "warp", + payload: { + to: client.currentSector, + asteroids: Array.from(sector.asteroids.values()), + collectables: Array.from(sector.collectables.values()), + mines: Array.from(sector.mines.values()), + sectorInfos: [sectorInfo], + }, + }) + ); + break; + case ServerChangeKind.Respawn: + insertRespawnedPlayer(ws, data.player, client.currentSector); + break; + case ServerChangeKind.Spawn: + insertSpawnedPlayer(ws, data.player, client.currentSector); + break; + default: + throw new Error("Unknown server change kind"); + } +}; + +const serverWarps = new Map(); + +// Note: this does not remove the player from the GlobalState object for the current server +const serverChangePlayer = (ws: WebSocket, player: Player, serverName: string, kind = ServerChangeKind.Warp) => { + const key = uid().toString(); + const serialized = serializeAllClientData(ws, player, key, kind); + if (!serialized) { + console.warn("No serialized client data"); + return; + } + serverWarps.set(key, ws); + const server = peerMap.get(serverName); + if (!server) { + console.warn("No server for", serverName); + return; + } + server.request.send("player-transfer", serialized, (key: string) => { + console.log(`Received key from ${server.name}`, key); + sendServerWarp(key, `ws://${server.ip}:${server.port}`); + }); +}; + +const sendServerWarp = (key: string, to: string) => { + if (!serverWarps.has(key)) { + console.warn("No server warp for key", key); + return; + } + try { + const ws = serverWarps.get(key)!; + ws.send(JSON.stringify({ type: "changeServers", payload: { to, key } })); + } catch (e) { + console.error("Error sending server warp", e); + } +}; const getPlayerFromId = (id: number) => { const ws = idToWebsocket.get(id); @@ -185,36 +242,73 @@ const getPlayerFromId = (id: number) => { }; const sectors: Map = new Map(); -const sectorTriggers: Map void> = new Map(); + +setInterval(() => { + for (const [sectorId, sector] of sectors) { + if (sector.dynamic && (sector.creationTime || 0) + 1000 * 60 * 60 * 3 < Date.now()) { + let hasPC = false; + for (const player of sector.players.values()) { + if (player.isPC) { + hasPC = true; + break; + } + } + if (!hasPC) { + sectors.delete(sectorId); + removeNetworkAwareness(sectorId); + } + } + } +}, 1000 * 60 * 60 * 30); + const warpList: { player: Player; to: number }[] = []; -sectorList.forEach((sector) => { - sectors.set(sector, { - players: new Map(), - projectiles: new Map(), - asteroids: new Map(), - missiles: new Map(), - collectables: new Map(), - asteroidsDirty: false, - mines: new Map(), - projectileId: 1, - delayedActions: [], - sectorKind: SectorKind.Overworld, +const initSectors = (serverSectors: number[]) => { + sectorList.push(...serverSectors); + sectorList.forEach((sector) => { + sectors.set(sector, { + players: new Map(), + projectiles: new Map(), + asteroids: new Map(), + missiles: new Map(), + collectables: new Map(), + asteroidsDirty: false, + mines: new Map(), + projectileId: 1, + delayedActions: [], + sectorKind: SectorKind.Overworld, + sectorChecks: [], + dynamic: false, + creationTime: Date.now(), + }); }); -}); - -// Server state +}; -// Targeting is handled by the clients, but the server needs to know -// Same pattern with secondaries -// BTW I do not like this design -const targets: Map = new Map(); -const secondaries: Map = new Map(); -const secondariesToActivate: Map = new Map(); +const sectorFactions: (Faction | null)[] = new Array(mapWidth * mapHeight).fill(null); +const sectorGuardianCount = new Array(mapWidth * mapHeight).fill(0); +const sectorHasStarbase = sectorList.map((_) => false); +const factionSectors: number[][] = new Array(Faction.Count).fill([]); -const knownRecipes: Map> = new Map(); +const initSectorData = async () => { + for (let i = 0; i < mapWidth * mapHeight; i++) { + const sectorInfo = await Sector.findOne({ sector: i }); + if (!sectorInfo) { + throw new Error("Missing sector info"); + } + sectorAsteroidResources.push(sectorInfo.resources); + sectorAsteroidCounts.push(sectorInfo.asteroidCount); + sectorFactions[i] = sectorInfo.faction; + sectorGuardianCount[i] = sectorInfo.guardianCount; -// const asteroidBounds = { x: -3000, y: -3000, width: 6000, height: 6000 }; + if (sectorInfo.faction !== null) { + factionSectors[sectorInfo.faction].push(i); + } + } + const stations = await Station.find(); + for (const station of stations) { + sectorHasStarbase[station.sector] = true; + } +}; const initInitialAsteroids = () => { for (let i = 0; i < sectorList.length; i++) { @@ -223,14 +317,29 @@ const initInitialAsteroids = () => { const def = defs[a.defIndex]; return def.kind === UnitKind.Station; }); - const asteroids = randomAsteroids(sectorAsteroidCounts[i], sectorBounds, sectorList[i], uid, sectorAsteroidResources[i], stationsInSector); + + const asteroids = randomAsteroids( + sectorAsteroidCounts[sectorList[i]], + sectorBounds, + sectorList[i], + uid, + sectorAsteroidResources[sectorList[i]], + stationsInSector + ); for (const asteroid of asteroids) { sector.asteroids.set(asteroid.id, asteroid); } } }; -const tutorialRespawnPoints = new Map(); +const stationIdToDefaultTeam = new Map(); + +const initStationTeams = async () => { + const stations = await Station.find({}); + for (const station of stations) { + stationIdToDefaultTeam.set(station.id, station.team); + } +}; const saveCheckpoint = (id: number, sector: number, player: Player, sectorsVisited: Set, isLogoff = false) => { if (player.health <= 0) { @@ -258,27 +367,212 @@ const saveCheckpoint = (id: number, sector: number, player: Player, sectorsVisit }); }; +type SerializableGlobalState = { + // Players handled separately + projectiles: Ballistic[]; + asteroids: Asteroid[]; + missiles: Missile[]; + collectables: Collectable[]; + mines: Mine[]; + asteroidsDirty?: boolean; + projectileId?: number; + delayedActions?: DelayedAction[]; + sectorKind?: SectorKind; + sectorNumber: number; + sectorChecks?: TransferableAction[]; + dynamic?: boolean; + creationTime?: number; +}; + +const serializeGlobalState = (state: GlobalState, sectorNumber: number): SerializableGlobalState => { + return { + projectiles: Array.from(state.projectiles.values()), + asteroids: Array.from(state.asteroids.values()), + missiles: Array.from(state.missiles.values()), + collectables: Array.from(state.collectables.values()), + mines: Array.from(state.mines.values()), + asteroidsDirty: state.asteroidsDirty, + projectileId: state.projectileId, + delayedActions: state.delayedActions, + sectorKind: state.sectorKind, + sectorNumber, + sectorChecks: state.sectorChecks, + dynamic: state.dynamic, + creationTime: state.creationTime, + }; +}; + +const deserializeGlobalState = (state: SerializableGlobalState): GlobalState => { + return { + players: new Map(), + projectiles: new Map(state.projectiles.map((p) => [p.id, p])), + asteroids: new Map(state.asteroids.map((a) => [a.id, a])), + missiles: new Map(state.missiles.map((m) => [m.id, m])), + collectables: new Map(state.collectables.map((c) => [c.id, c])), + mines: new Map(state.mines.map((m) => [m.id, m])), + asteroidsDirty: state.asteroidsDirty || false, + projectileId: state.projectileId || 1, + delayedActions: state.delayedActions || [], + sectorKind: state.sectorKind || SectorKind.Overworld, + sectorChecks: state.sectorChecks || [], + dynamic: state.dynamic || false, + creationTime: state.creationTime || Date.now(), + }; +}; + +const insertSector = (state: SerializableGlobalState) => { + if (sectors.has(state.sectorNumber)) { + return "Sector already exists on this server"; + } + const repairedState = deserializeGlobalState(state); + sectors.set(state.sectorNumber, repairedState); + return "OK"; +}; + +const transferSectorToPeer = (sector: number, peer: string) => { + const promise = new Promise((resolve, reject) => { + const state = sectors.get(sector); + if (!state) { + // TODO: We can handle this problem better + reject("Sector not on this server"); + return; + } + const serializableState = serializeGlobalState(state, sector); + const peerSockets = peerMap.get(peer); + if (!peerSockets) { + reject("Peer not found"); + return; + } + peerSockets.request.send("sector-transfer", serializableState, (success: string) => { + if (success === "OK") { + resolve(); + sectors.delete(sector); + console.log("Number of players in sector is now: " + state.players.size); + while (state.players.size > 0) { + const player = state.players.values().next().value; + state.players.delete(player.id); + if (player) { + if (player.isPC) { + const ws = idToWebsocket.get(player.id); + if (ws) { + serverChangePlayer(ws, player, peerSockets.name); + } else { + console.log("Missing websocket for player: " + player.id); + } + continue; + } + if (player.npc) { + player.sector = sector; + player.npcReconstructionKey = Object.getPrototypeOf(player.npc).constructor.name; + player.input = player.npc.input; + player.npc = undefined; + peerSockets.request.send("npc-transfer", player, (success: string) => { + if (success !== "OK") { + console.log("Error transferring npc: " + success); + } + }); + continue; + } + const def = defs[player.defIndex]; + if (def.kind === UnitKind.Station) { + player.sector = sector; + peerSockets.request.send("station-transfer", player, (success: string) => { + if (success !== "OK") { + console.log("Error transferring station: " + success); + } + }); + continue; + } + } + } + } else { + reject("Transfer failed: " + success); + } + }); + }); + return promise; +}; + +type SerializablePlayer = Player & { sector: number; npcReconstructionKey?: string; input?: Input }; + +const insertStation = (station: SerializablePlayer) => { + console.log("Inserting station: " + station.id); + const state = sectors.get(station.sector); + if (!state) { + return "Sector not on this server: " + station.sector; + } + delete (station as any).sector; + state.players.set(station.id, station); + return "OK"; +}; + +const insertNPC = (npc: SerializablePlayer) => { + console.log("Inserting npc: " + npc.id); + const state = sectors.get(npc.sector); + if (!state) { + return "Sector not on this server: " + npc.sector; + } + delete (npc as any).sector; + const npcReconstructionKey = npc.npcReconstructionKey; + delete (npc as any).npcReconstructionKey; + if (!npcReconstructionKey) { + return "Missing npcReconstructionKey"; + } + const constructor = npcReconstructors.get(npcReconstructionKey); + if (!constructor) { + return "Bad npcReconstructionKey: " + npcReconstructionKey; + } + const input = npc.input; + delete (npc as any).input; + if (!input) { + return "Missing input"; + } + npc.npc = constructor(npc); + npc.npc.input = input; + state.players.set(npc.id, npc); + return "OK"; +}; + export { + ServerChangeKind, + // ClientData, + // SerializableClientData, + // serializableClientData, + // repairClientData, + SerializableClient, + SerializableGlobalState, + SerializablePlayer, + deserializeClientData, sectorList, sectorAsteroidResources, sectorAsteroidCounts, + allResources, sectorFactions, sectorGuardianCount, sectorHasStarbase, clients, idToWebsocket, sectors, - sectorTriggers, warpList, targets, secondaries, secondariesToActivate, knownRecipes, - tutorialRespawnPoints, uid, - sectorInDirection, saveCheckpoint, - friendlySectors, + factionSectors, initInitialAsteroids, getPlayerFromId, + serializeAllClientData, + sendServerWarp, + serverChangePlayer, + initSectors, + initSectorData, + initStationTeams, + stationIdToDefaultTeam, + insertSector, + transferSectorToPeer, + insertStation, + insertNPC, + getTutorialNpc, }; diff --git a/server/stateHelpers.ts b/server/stateHelpers.ts index 0b1fa3e..c109861 100644 --- a/server/stateHelpers.ts +++ b/server/stateHelpers.ts @@ -84,6 +84,7 @@ const sendMissionComplete = (id: number, message: string) => { } }; +// BROKEN (sort of) const findPlayer = (id: number): SectorOfPlayerResult => { for (const [sectorNumber, state] of sectors) { if (state.players.has(id)) { diff --git a/server/testPeer.sh b/server/testPeer.sh new file mode 100755 index 0000000..bd70e31 --- /dev/null +++ b/server/testPeer.sh @@ -0,0 +1,5 @@ +#! /bin/bash + +node peers.js carter 5555 5565 8080 0 & +node peers.js sheppard 5556 5566 8082 1 & +node peers.js oneill 5557 5567 8084 2 & diff --git a/server/transferableActions.ts b/server/transferableActions.ts new file mode 100644 index 0000000..00b60ea --- /dev/null +++ b/server/transferableActions.ts @@ -0,0 +1,87 @@ +import { Faction } from "../src/defs"; +import { equip, GlobalState, TutorialStage } from "../src/game"; +import { completeMission } from "./missions"; +import { clients, idToWebsocket, sectors } from "./state"; +import { enemyCountState } from "./stateHelpers"; +import { sendTutorialStage, spawnTutorialStation } from "./tutorial"; + +const transferableActionsMap = new Map(); + +const transferableActions: ((state: GlobalState, sector: number, data: any) => boolean)[] = []; + +// There is technically a bug with this, if the npc hits the boundary of the sector it will not be in the sector for one server tick +// If the check is run in that tick, the tutorial will advance when it shouldn't +transferableActions.push((state: GlobalState, sector: number, data: { id: number }) => { + let hasNPCs = false; + for (const player of state.players.values()) { + if (player.npc) { + hasNPCs = true; + break; + } + } + if (!hasNPCs) { + const ws = idToWebsocket.get(data.id); + if (ws) { + const client = clients.get(ws); + if (client) { + client.inTutorial = TutorialStage.SwitchSecondary; + sendTutorialStage(ws, TutorialStage.SwitchSecondary); + const state = sectors.get(client.currentSector); + if (state) { + const player = state.players.get(client.id); + if (player) { + state.players.set(client.id, equip(player, 1, "Javelin Missile", true)); + return true; + } + } + } + } + } + return false; +}); +transferableActionsMap.set("tutorialVenture", transferableActions.length - 1); + +// This has the same bug as above +transferableActions.push((state: GlobalState, sector: number, data: { id: number }) => { + let hasNPCs = false; + for (const player of state.players.values()) { + if (player.npc) { + hasNPCs = true; + break; + } + } + if (!hasNPCs) { + const ws = idToWebsocket.get(data.id); + if (ws) { + const client = clients.get(ws); + if (client) { + client.inTutorial = TutorialStage.Dock; + sendTutorialStage(ws, TutorialStage.Dock); + spawnTutorialStation(ws); + return true; + } + } + } + return false; +}); +transferableActionsMap.set("tutorialStrafer", transferableActions.length - 1); + +transferableActions.push((state: GlobalState, sector: number, data: { missionId: number, forFaction: Faction }) => { + if (enemyCountState(data.forFaction, state) === 0) { + completeMission(data.missionId); + return true; + } + return false; +}); +transferableActionsMap.set("clearance", transferableActions.length - 1); + +transferableActions.push((state: GlobalState, sector: number, data: { missionId: number, targetId: number }) => { + if (!state.players.has(data.targetId!)) { + completeMission(data.missionId); + return true; + } + return false; +}); +transferableActionsMap.set("assassination", transferableActions.length - 1); + +export { transferableActions, transferableActionsMap }; diff --git a/server/tutorial.ts b/server/tutorial.ts index e39da57..2c088b4 100644 --- a/server/tutorial.ts +++ b/server/tutorial.ts @@ -1,17 +1,63 @@ -import { copyPlayer, effectiveInfinity, equip, mapSize, Player, randomAsteroids, sectorBounds, TutorialStage } from "../src/game"; +import { copyPlayer, effectiveInfinity, equip, Player, randomAsteroids, sectorBounds, TutorialStage } from "../src/game"; import { WebSocket } from "ws"; -import { clients, saveCheckpoint, sectors, tutorialRespawnPoints, uid } from "./state"; +import { clients, getTutorialNpc, saveCheckpoint, sectors, uid } from "./state"; import { defMap, Faction } from "../src/defs"; -import { addTutorialRoamingVenture, addTutorialStrafer, NPC } from "../src/npc"; +import { addTutorialRoamingVenture, addTutorialStrafer, NPC } from "./npcs/npc"; import { discoverRecipe, updateClientRecipes } from "./inventory"; +import { mapHeight, mapWidth } from "../src/mapLayout"; +import { transferableActionsMap } from "./transferableActions"; +import mongoose from "mongoose"; +import { playerSectors } from "./peers"; -const spawnTutorialStation = (ws: WebSocket) => { +interface ITutorialRespawn { + id: number; + data: string; + time: Date; + sector: number; +} + +const TutorialRespawn = mongoose.model( + "TutorialRespawn", + new mongoose.Schema({ + id: { + type: Number, + required: true, + }, + data: { + type: String, + required: true, + }, + time: { + type: Date, + required: true, + expires: "1d", + }, + sector: { + type: Number, + required: true, + }, + }) +); + +const saveTutorialRespawn = (player: Player) => { + const id = player.id; + const data = JSON.stringify(player); + const sector = playerSectors.get(id); + TutorialRespawn.findOneAndUpdate({ id }, { id, data, time: new Date(), sector }, { upsert: true }, (err) => { + if (err) { + console.error("Unable to save tutorial respawn point", err); + } + }); +}; + +const spawnTutorialStation = async (ws: WebSocket) => { const client = clients.get(ws); if (client) { const sector = sectors.get(client.currentSector); if (sector) { - const player = tutorialRespawnPoints.get(client.id); - if (player) { + const save = await TutorialRespawn.findOne({ id: client.id }); + if (save) { + const player = JSON.parse(save.data); const def = (player.team === Faction.Alliance ? defMap.get("Alliance Starbase") : defMap.get("Confederacy Starbase"))!; const station: Player = { position: { x: 0, y: 0 }, @@ -27,7 +73,6 @@ const spawnTutorialStation = (ws: WebSocket) => { slotData: [], team: player.team, side: 0, - isPC: true, v: { x: 0, y: 0 }, iv: { x: 0, y: 0 }, ir: 0, @@ -52,21 +97,8 @@ const advanceTutorialStage = (id: number, stage: TutorialStage, ws: WebSocket) = if (state) { const player = state.players.get(id); if (player) { - const npc = addTutorialRoamingVenture(state, uid(), player.position); - (npc as NPC).killed = () => { - const client = clients.get(ws); - if (client) { - client.inTutorial = TutorialStage.SwitchSecondary; - sendTutorialStage(ws, TutorialStage.SwitchSecondary); - const state = sectors.get(client.currentSector); - if (state) { - const player = state.players.get(client.id); - if (player) { - state.players.set(client.id, equip(player, 1, "Javelin Missile", true)); - } - } - } - }; + addTutorialRoamingVenture(state, uid(), player.position); + state.sectorChecks?.push({ index: transferableActionsMap.get("tutorialVenture")!, data: { id } }); } } } @@ -113,20 +145,12 @@ const advanceTutorialStage = (id: number, stage: TutorialStage, ws: WebSocket) = const player = state.players.get(client.id); if (player) { const npc = addTutorialStrafer(state, uid(), player.position); - (npc as NPC).killed = () => { - { - const client = clients.get(ws); - if (client) { - client.inTutorial = TutorialStage.Dock; - sendTutorialStage(ws, TutorialStage.Dock); - spawnTutorialStation(ws); - } - } - }; client.tutorialNpc = npc; + npc.player.doNotShootYet = true; + state.sectorChecks?.push({ index: transferableActionsMap.get("tutorialStrafer")!, data: { id } }); const equippedPlayer = equip(player, 2, "Laser Beam", true); state.players.set(client.id, equippedPlayer); - tutorialRespawnPoints.set(client.id, copyPlayer(equippedPlayer)); + saveTutorialRespawn(equippedPlayer); } } } @@ -136,7 +160,13 @@ const advanceTutorialStage = (id: number, stage: TutorialStage, ws: WebSocket) = { const client = clients.get(ws); if (client) { - (client.tutorialNpc as any).doNotShootYet = false; + const state = sectors.get(client.currentSector); + if (state) { + const npc = getTutorialNpc(client, state); + if (npc) { + npc.player.doNotShootYet = false; + } + } } } return TutorialStage.LaserBeam; @@ -145,11 +175,7 @@ const advanceTutorialStage = (id: number, stage: TutorialStage, ws: WebSocket) = const client = clients.get(ws); if (client) { updateClientRecipes(ws, client.id); - const player = tutorialRespawnPoints.get(id); discoverRecipe(ws, client.id, "Refined Prifetium"); - if (player) { - client.sectorsVisited.add(player.team === Faction.Alliance ? 12 : 15); - } } } return TutorialStage.Deposit; @@ -167,9 +193,12 @@ const advanceTutorialStage = (id: number, stage: TutorialStage, ws: WebSocket) = { const client = clients.get(ws); if (client) { - const player = tutorialRespawnPoints.get(id); - if (player) { - client.sectorsVisited.add(player.team === Faction.Alliance ? 12 : 15); + const state = sectors.get(client.currentSector); + if (state) { + const player = state.players.get(client.id); + if (player) { + client.sectorsVisited.add(player.team === Faction.Alliance ? 12 : 15); + } } } } @@ -177,7 +206,7 @@ const advanceTutorialStage = (id: number, stage: TutorialStage, ws: WebSocket) = case TutorialStage.Map: { const client = clients.get(ws); - if (client && client.currentSector < mapSize * mapSize) { + if (client && client.currentSector < mapWidth * mapHeight) { const state = sectors.get(client.currentSector); if (state) { const player = state.players.get(client.id); @@ -201,4 +230,4 @@ const sendTutorialStage = (ws: WebSocket, stage: TutorialStage) => { ws.send(JSON.stringify({ type: "tutorialStage", payload: stage })); }; -export { advanceTutorialStage, sendTutorialStage }; +export { advanceTutorialStage, sendTutorialStage, spawnTutorialStation, saveTutorialRespawn, TutorialRespawn, ITutorialRespawn }; diff --git a/server/websockets.ts b/server/websockets.ts new file mode 100644 index 0000000..fbecae2 --- /dev/null +++ b/server/websockets.ts @@ -0,0 +1,709 @@ +import { appendFile } from "fs"; +import { createServer } from "http"; +import https from "https"; +import { HydratedDocument } from "mongoose"; +import { inspect } from "util"; +import { WebSocketServer, WebSocket } from "ws"; +import { useSsl } from "../src/config"; +import { armDefs, ArmUsage, defs, Faction } from "../src/defs"; +import { + applyUndockingOffset, + canDock, + canRepair, + CloakedState, + copyPlayer, + equip, + isNearOperableEnemyStation, + maxNameLength, + Player, + purchaseShip, + removeAtMostCargo, + removeCargoFractions, + SectorInfo, + TargetKind, + TutorialStage, +} from "../src/game"; +import { mapHeight, mapWidth } from "../src/mapLayout"; +import { Checkpoint, Station, User } from "./dataModels"; +import { createFriendRequest, friendWarp, revokeFriendRequest, unfriend } from "./friends"; +import { + compositeManufacture, + depositCargo, + depositItemsIntoInventory, + manufacture, + sellInventory, + sendInventory, + transferToShip, +} from "./inventory"; +import { assignPlayerIdToConnection, logWebSocketConnection } from "./logging"; +import { market } from "./market"; +import { setupPlayer } from "./misc"; +import { selectMission, startPlayerInMission } from "./missions"; +import { waitingData } from "./peers"; +import { respawnPlayer, spawnPlayer } from "./server"; +import { hash, sniCallback, wsPort } from "./settings"; +import { + clients, + deserializeClientData, + idToWebsocket, + knownRecipes, + saveCheckpoint, + secondaries, + secondariesToActivate, + // sectorAsteroidResources, + sectorList, + sectors, + targets, + uid, +} from "./state"; +import { allyCount, enemyCount, flashServerMessage } from "./stateHelpers"; +import { advanceTutorialStage, ITutorialRespawn, saveTutorialRespawn, sendTutorialStage, TutorialRespawn } from "./tutorial"; + +export function startWebSocketServer(wsPort: number) { + // Websocket server stuff + let server: ReturnType | https.Server; + if (useSsl) { + server = new https.Server({ SNICallback: sniCallback }); + } else { + server = createServer(); + } + + // Websocket stuff (TODO Move to its own file) + const wss = new WebSocketServer({ server }); + + // TODO Need to go over this carefully, checking to make sure that malicious clients can't do anything bad + wss.on("connection", (ws, req) => { + (ws as any).isAlive = true; + + const ipAddr = req.socket.remoteAddress; + + logWebSocketConnection(ipAddr); + + ws.on("message", (msg) => { + try { + const data = JSON.parse(msg.toString()); + if (data.type === "heartbeat") { + (ws as any).isAlive = true; + return; + } else if (data.type === "serverHopKey") { + const key = data.payload.key; + deserializeClientData(ws, waitingData.get(key)!); + } else if (data.type === "login") { + const name = data.payload.name; + const password = data.payload.password; + + const hashedPassword = hash(password); + + // Check if the user is in the database + User.findOne({ name, password: hashedPassword }, (err, user) => { + if (err) { + ws.send(JSON.stringify({ type: "loginFail", payload: { error: "Database error" } })); + console.log(err); + return; + } + if (!user) { + ws.send(JSON.stringify({ type: "loginFail", payload: { error: "Username/password combination not found" } })); + return; + } + + if (idToWebsocket.has(user.id)) { + ws.send(JSON.stringify({ type: "loginFail", payload: { error: "User already logged in" } })); + return; + } + + idToWebsocket.set(user.id, ws); + + assignPlayerIdToConnection(ipAddr, user.id); + + user.loginCount++; + user.loginTimes.push(Date.now()); + try { + user.save(); + } catch (err) { + console.log(err); + } + + Checkpoint.findOne({ id: user.id }, (err, checkpoint) => { + if (err) { + ws.send(JSON.stringify({ type: "loginFail", payload: { error: "Database error" } })); + console.log(err); + return; + } + if (!checkpoint) { + setupPlayer(user.id, ws, name, user.faction); + } else { + if (!user.sectorsVisited) { + if (checkpoint.sector >= 0 && checkpoint.sector < mapWidth * mapHeight) { + user.sectorsVisited = [checkpoint.sector]; + } else { + user.sectorsVisited = []; + } + } + const sectorsVisited: Set = new Set(user.sectorsVisited); + if (checkpoint.sector >= 0 && checkpoint.sector < mapWidth * mapHeight) { + sectorsVisited.add(checkpoint.sector); + } + + clients.set(ws, { + id: user.id, + name, + input: { up: false, down: false, primary: false, secondary: false, right: false, left: false }, + angle: 0, + currentSector: checkpoint.sector, + lastMessage: "", + lastMessageTime: Date.now(), + sectorsVisited, + inTutorial: TutorialStage.Done, + }); + targets.set(user.id, [TargetKind.None, 0]); + secondaries.set(user.id, 0); + secondariesToActivate.set(user.id, []); + knownRecipes.set(user.id, new Set(user.recipesKnown)); + + const playerState = JSON.parse(checkpoint.data) as Player; + // All these "fixes" are for making old checkpoints work with new code + // Update the player on load to match what is expected + if (playerState.defIndex === undefined) { + playerState.defIndex = (playerState as any).definitionIndex; + (playerState as any).definitionIndex = undefined; + } + // fix the cargo + if (playerState.cargo === undefined || playerState.cargo.some((c) => !Number.isInteger(c.amount))) { + playerState.cargo = [{ what: "Teddy Bears", amount: 30 }]; + } + // fix the credits + if (playerState.credits === undefined) { + playerState.credits = 500; + } + playerState.credits = Math.round(playerState.credits); + // fix the slot data + const def = defs[playerState.defIndex]; + while (playerState.slotData.length < def.slots.length) { + playerState.arms.push(def.slots[playerState.slotData.length]); + playerState.slotData.push({}); + } + // fix the impulse + if (playerState.ir === undefined) { + playerState.ir = 0; + } + if (playerState.iv === undefined) { + playerState.iv = { x: 0, y: 0 }; + } + // fix the health and energy + if (playerState.health > def.health) { + playerState.health = def.health; + } + if (playerState.energy > def.energy) { + playerState.energy = def.energy; + } + (playerState as any).projectileId = undefined; + // fix the arms + if (playerState.arms === undefined) { + playerState.arms = (playerState as any).armIndices; + (playerState as any).armIndices = undefined; + } + playerState.v = { x: 0, y: 0 }; + + spawnPlayer(ws, playerState, checkpoint.sector); + // log to file + appendFile("log", `${new Date().toISOString()} ${name} logged in\n`, (err) => { + if (err) { + console.log(err); + } + }); + } + }); + }); + } else if (data.type === "register") { + const name = data.payload.name; + const password = data.payload.password; + const faction = data.payload.faction; + + // Check if the user is in the database + User.findOne({ name }, (err, user) => { + if (err) { + ws.send(JSON.stringify({ type: "registerFail", payload: { error: "Database error" } })); + console.log(err); + return; + } + if (user) { + ws.send(JSON.stringify({ type: "registerFail", payload: { error: "Username already taken" } })); + return; + } + if (name.length > maxNameLength) { + ws.send(JSON.stringify({ type: "registerFail", payload: { error: "Username too long" } })); + return; + } + User.create({ name, password: hash(password), faction, id: uid(), loginTimes: [Date.now()] }, (err, user) => { + if (err) { + ws.send(JSON.stringify({ type: "registerFail", payload: { error: "Database error" } })); + console.log(err); + return; + } + setupPlayer(user.id, ws, name, faction); + idToWebsocket.set(user.id, ws); + + assignPlayerIdToConnection(ipAddr, user.id); + }); + }); + } else if (data.type === "input") { + const client = clients.get(ws); + if (client) { + client.input = data.payload.input; + } else { + console.log("Warning: Input data from unknown client"); + } + } else if (data.type === "angle") { + const client = clients.get(ws); + if (client) { + client.angle = data.payload.angle; + } else { + console.log("Warning: Angle data from unknown client"); + } + } else if (data.type === "dock") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player) { + if (player.docked) { + return; + } + removeCargoFractions(player); + const station = state.players.get(data.payload.stationId); + if (canDock(player, station, false)) { + const def = defs[player.defIndex]; + player.docked = data.payload.stationId; + player.heading = 0; + player.speed = 0; + player.side = 0; + player.energy = def.energy; + player.health = def.health; + player.warping = 0; + player.ir = 0; + player.iv.x = 0; + player.iv.y = 0; + player.cloak = CloakedState.Uncloaked; + player.position = { x: station!.position.x, y: station!.position.y }; + for (let i = 0; i < player.arms.length; i++) { + const armDef = armDefs[player.arms[i]]; + if (armDef && armDef.usage === ArmUsage.Ammo) { + player.slotData[i].ammo = armDef.maxAmmo; + } + } + + state.players.set(client.id, player); + + if (!client.inTutorial) { + saveCheckpoint(client.id, client.currentSector, player, client.sectorsVisited); + } else { + saveTutorialRespawn(player); + } + } + } + } + } else if (data.type === "undock") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player) { + player.docked = undefined; + applyUndockingOffset(player); + state.players.set(client.id, player); + + if (!client.inTutorial) { + saveCheckpoint(client.id, client.currentSector, player, client.sectorsVisited); + } else { + saveTutorialRespawn(player); + } + } + } + } else if (data.type === "repair") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player) { + const station = state.players.get(data.payload.station)!; + if (canRepair(player, station, false)) { + if (!station.repairs || station.repairs.length !== Faction.Count) { + console.log(`Warning: Station repairs array is not correctly initialized (${station.id})`); + } else { + const stationDef = defs[station.defIndex]; + const repairsNeeded = stationDef.repairsRequired! - station.repairs[player.team]; + const amountRepaired = removeAtMostCargo(player, "Spare Parts", repairsNeeded); + station.repairs[player.team] += amountRepaired; + } + } + } + } + } else if (data.type === "respawn") { + const client = clients.get(ws); + if (client) { + if (client.inTutorial) { + const state = sectors.get(client.currentSector); + if (state) { + TutorialRespawn.findOne({ id: client.id }, (err, tutorialRespawn: HydratedDocument) => { + if (err) { + ws.send(JSON.stringify({ type: "error", payload: { message: "Server error loading tutorial respawn checkpoint" } })); + console.log("Error loading tutorial respawn checkpoint: " + err); + return; + } + if (tutorialRespawn) { + const playerState = JSON.parse(tutorialRespawn.data); + if (playerState) { + respawnPlayer(ws, playerState, tutorialRespawn.sector); + } else { + ws.send(JSON.stringify({ type: "error", payload: { message: "Missing tutorial respawn checkpoint" } })); + } + } else { + ws.send(JSON.stringify({ type: "error", payload: { message: "Missing tutorial respawn checkpoint" } })); + } + }); + } + return; + } + Checkpoint.findOne({ id: client.id }, (err, checkpoint) => { + if (err) { + ws.send(JSON.stringify({ type: "error", payload: { message: "Server error loading checkpoint" } })); + console.log("Error loading checkpoint: " + err); + return; + } + if (!checkpoint) { + ws.send(JSON.stringify({ type: "error", payload: { message: "Checkpoint not found" } })); + console.log("Error loading checkpoint: " + err); + return; + } + client.currentSector = checkpoint.sector; + const playerState = JSON.parse(checkpoint.data) as Player; + respawnPlayer(ws, playerState, checkpoint.sector); + }); + } + } else if (data.type === "target") { + const client = clients.get(ws); + if (client) { + targets.set(client.id, data.payload.target); + } + } else if (data.type === "secondary") { + const client = clients.get(ws); + if (client) { + if (typeof data.payload.secondary === "number" && data.payload.secondary >= 0) { + secondaries.set(client.id, data.payload.secondary); + } + } + } else if (data.type === "secondaryActivation") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player) { + if (typeof data.payload.secondary === "number" && data.payload.secondary < player.arms.length && data.payload.secondary >= 0) { + secondariesToActivate.get(client.id)?.push(data.payload.secondary); + } + } + } + } else if (data.type === "sellCargo") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player && player.cargo) { + if (player.credits === undefined) { + player.credits = 0; + } + const price = market.get(data.payload.what); + if (price) { + player.credits += removeAtMostCargo(player, data.payload.what, Math.round(data.payload.amount)) * price; + } + } + } + } else if (data.type === "transferToShip") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player) { + transferToShip(ws, player, data.payload.what, Math.round(data.payload.amount), flashServerMessage); + } + } + } else if (data.type === "sellInventory") { + const client = clients.get(ws); + if (client) { + const player = sectors.get(client.currentSector)!.players.get(client.id); + if (player) { + sellInventory(ws, player, data.payload.what, Math.round(data.payload.amount)); + } + } + } else if (data.type === "depositCargo") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player && player.cargo) { + depositCargo(player, data.payload.what, Math.round(data.payload.amount), ws); + } + } + } else if (data.type === "dumpCargo") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player && player.cargo) { + removeAtMostCargo(player, data.payload.what, Math.round(data.payload.amount)); + } + } + } else if (data.type === "equip") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player) { + // equip does the bounds checking for the index for us + let newPlayer = equip(player, data.payload.slotIndex, data.payload.what, data.payload.fromInventory); + if (newPlayer !== player) { + state.players.set(client.id, newPlayer); + const toTake = data.payload.fromInventory ? [armDefs[newPlayer.arms[data.payload.slotIndex]].name] : []; + // There is technically a bug here, if the player equips and then logs off, but the database has an error after they log off then + // they what is deposited will be lost. I don't want to deal with it though (the correct thing is to pull their save from the database + // and deal with it that way, but if we just had a database error this is unlikely to work anyways) + depositItemsIntoInventory(ws, player, [armDefs[player.arms[data.payload.slotIndex]].name], toTake, flashServerMessage, () => { + console.log("Error depositing armament into inventory, reverting player"); + try { + const otherState = sectors.get(clients.get(idToWebsocket.get(player.id)!)!.currentSector)!; + otherState.players.set(player.id, player); + } catch (e) { + console.log("Warning: unable to revert player" + e); + } + }); + } + } + } + } else if (data.type === "chat") { + const client = clients.get(ws); + if (client) { + data.payload.message = data.payload.message.trim().substring(0, 200); + for (const [otherClient, otherClientData] of clients) { + if (otherClientData.currentSector === client.currentSector) { + otherClient.send(JSON.stringify({ type: "chat", payload: { id: client.id, message: data.payload.message } })); + } + } + } + } else if (data.type === "manufacture") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player) { + manufacture(ws, player, data.payload.what, Math.round(data.payload.amount), flashServerMessage); + } + } + } else if (data.type === "compositeManufacture") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player) { + compositeManufacture(ws, player, data.payload.what, Math.round(data.payload.amount), flashServerMessage); + } + } + } else if (data.type === "purchase") { + const client = clients.get(ws); + if (client) { + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player) { + Station.findOne({ id: player.docked }, (err, station) => { + if (err) { + ws.send(JSON.stringify({ type: "error", payload: { message: "Server error loading station" } })); + console.log("Error loading station: " + err); + return; + } + if (!station) { + ws.send(JSON.stringify({ type: "error", payload: { message: "Station not found" } })); + console.log("Error loading station: " + err); + return; + } + const newPlayer = purchaseShip(player, data.payload.index, station.shipsAvailable, data.payload.fromInventory); + if (newPlayer !== player) { + state.players.set(client.id, newPlayer); + const items = [defs[player.defIndex].name]; + if (player.arms) { + for (const armIndex of player.arms) { + items.push(armDefs[armIndex].name); + } + } + const toTake = data.payload.fromInventory ? [defs[newPlayer.defIndex].name] : []; + // There is technically a bug here, if the player equips and then logs off, but the database has an error after they log off then + // they what is deposited will be lost. I don't want to deal with it though (the correct thing is to pull their save from the database + // and deal with it that way, but if we just had a database error this is unlikely to work anyways) + depositItemsIntoInventory(ws, player, items, toTake, flashServerMessage, () => { + console.log("Error depositing ship into inventory, reverting player"); + try { + const otherState = sectors.get(clients.get(idToWebsocket.get(player.id)!)!.currentSector)!; + otherState.players.set(player.id, player); + } catch (e) { + console.log("Warning: unable to revert player" + e); + } + }); + } + }); + } + } + } else if (data.type === "warp") { + const client = clients.get(ws); + if (client) { + if (client.currentSector !== data.payload.warpTo) { + // if (!client.sectorsVisited.has(data.payload.warpTo)) { + // flashServerMessage(client.id, "You must visit a sector before you can warp to it"); + // return; + // } + const state = sectors.get(client.currentSector)!; + const player = state.players.get(client.id); + if (player) { + player.warpTo = data.payload.warpTo; + player.warping = 1; + } + } + } + } else if (data.type === "tutorialStageComplete") { + const client = clients.get(ws); + if (client) { + if (client.inTutorial === data.payload.stage) { + if (client.inTutorial !== data.payload.stage) { + ws.send(JSON.stringify({ type: "error", payload: { message: "Tutorial stage mismatch" } })); + } + client.inTutorial = advanceTutorialStage(client.id, data.payload.stage, ws); + sendTutorialStage(ws, client.inTutorial); + } + } + } else if (data.type === "selectMission") { + const client = clients.get(ws); + if (client) { + if (client.inTutorial) { + flashServerMessage(client.id, "You cannot select a mission while in the tutorial", [1.0, 0.0, 0.0, 1.0]); + return; + } + const state = sectors.get(client.currentSector); + if (state) { + const player = state.players.get(client.id); + if (player) { + selectMission(ws, player, data.payload.missionId); + } + } + } + } else if (data.type === "startMission") { + const client = clients.get(ws); + if (client) { + if (client.inTutorial) { + flashServerMessage(client.id, "You cannot start a mission while in the tutorial", [1.0, 0.0, 0.0, 1.0]); + return; + } + const state = sectors.get(client.currentSector); + if (state) { + const player = state.players.get(client.id); + if (player) { + startPlayerInMission(ws, player, data.payload.missionId); + } + } + } + } else if (data.type === "friendRequest") { + const client = clients.get(ws); + if (client) { + createFriendRequest(ws, client.id, data.payload.name); + } + } else if (data.type === "revokeFriendRequest") { + const client = clients.get(ws); + if (client) { + revokeFriendRequest(ws, client.id, data.payload.name); + } + } else if (data.type === "unfriend") { + const client = clients.get(ws); + if (client) { + unfriend(ws, client.id, data.payload.id); + } + } else if (data.type === "friendWarp") { + const client = clients.get(ws); + if (client) { + const player = sectors.get(client.currentSector)?.players.get(client.id); + if (player) { + friendWarp(ws, player, data.payload.id); + } + } + } else { + console.log("Unknown message from client: ", data); + } + } catch (e) { + console.log("Error in message handler: " + e); + appendFile("errorlog", `Error: ${e}\nmessage: ${msg}\n${inspect(clients, { depth: null })}\n${Array.from(sectors.values())}\n`, (err) => { + if (err) { + console.log("Error writing to log: " + err); + } + }); + } + }); + + ws.on("close", () => { + try { + const removedClient = clients.get(ws); + if (removedClient) { + clients.delete(ws); + const player = sectors.get(removedClient.currentSector)?.players.get(removedClient.id); + const state = sectors.get(removedClient.currentSector); + targets.delete(removedClient.id); + secondaries.delete(removedClient.id); + secondariesToActivate.delete(removedClient.id); + idToWebsocket.delete(removedClient.id); + knownRecipes.delete(removedClient.id); + state?.players.delete(removedClient.id); + if (player) { + if (player.docked) { + if (!removedClient.inTutorial) { + saveCheckpoint(removedClient.id, removedClient.currentSector, player, removedClient.sectorsVisited, true); + } + } else { + User.findOneAndUpdate( + { id: removedClient.id }, + { + $set: { sectorsVisited: Array.from(removedClient.sectorsVisited), currentSector: removedClient.currentSector }, + $push: { logoffTimes: Date.now() }, + }, + (err) => { + if (err) { + console.log("Error saving user: " + err); + } + } + ); + } + } else if (!player) { + console.log("Warning: player not found on disconnect (this is normal for a server switch)"); + } + } + } catch (e) { + console.log("Error in close handler: " + e); + appendFile("errorlog", `Error: ${e}\n${inspect(clients, { depth: null })}\n${Array.from(sectors.values())}\n`, (err) => { + if (err) { + console.log("Error writing to log: " + err); + } + }); + } + }); + }); + + const interval = setInterval(function ping() { + wss.clients.forEach(function each(ws) { + if ((ws as any).isAlive === false) return ws.terminate(); + + (ws as any).isAlive = false; + ws.ping(); + }); + }, 30000); + + wss.on("close", function close() { + clearInterval(interval); + }); + + server.listen(wsPort, () => { + console.log(`${useSsl ? "Secure" : "Unsecure"} websocket server running on port ${wsPort}`); + }); +} diff --git a/src/2dDrawing.ts b/src/2dDrawing.ts index 4564e80..bff5a87 100644 --- a/src/2dDrawing.ts +++ b/src/2dDrawing.ts @@ -852,7 +852,6 @@ const weaponTexts: WeaponTextData[] = []; let weaponTextInitialized = false; const rasterizeWeaponText = () => { - console.log("rasterizing weapon text"); if (!lastSelf) { return; } diff --git a/src/3dDrawing.ts b/src/3dDrawing.ts index b83382d..cf9e3d4 100644 --- a/src/3dDrawing.ts +++ b/src/3dDrawing.ts @@ -3,7 +3,7 @@ import { addLoadingText, currentSector, getUseAlternativeBackgroundsPref, isFire import { glMatrix, mat2, mat4, vec3, vec4 } from "gl-matrix"; import { loadObj, Model, modelMap, models } from "./modelLoader"; import { asteroidDefs, collectableDefs, defs, mineDefs, missileDefs } from "./defs"; -import { Asteroid, Ballistic, ChatMessage, CloakedState, Collectable, mapSize, Mine, Missile, Player, sectorBounds } from "./game"; +import { Asteroid, Ballistic, ChatMessage, CloakedState, Collectable, Mine, Missile, Player, sectorBounds } from "./game"; import { infinityNorm, l2NormSquared, Position, Rectangle } from "./geometry"; import { appendBottomBars, @@ -42,6 +42,7 @@ import { createParticleBuffers, drawParticles, initParticleTextures } from "./pa import { drawProjectile } from "./3dProjectileDrawing"; import { projectileLightColorUnnormed } from "./defs/projectiles"; import { Debouncer, EagerDebouncer } from "./dialogs/helpers"; +import { mapHeight, mapWidth } from "./mapLayout"; let canvas: HTMLCanvasElement; let overlayCanvas: HTMLCanvasElement; @@ -352,6 +353,8 @@ const init3dDrawing = (callback: () => void) => { "heavy_tomahawk.obj", "maintainer.obj", "infiltrator.obj", + "emp_mine.obj", + "gun_platform.obj", ].map((url) => loadObj(url, gl, programInfo)) ) .then(async () => { @@ -401,6 +404,11 @@ const clientPlayerUpdate = (player: Player) => { mat4.scale(modelMatrix, modelMatrix, [Math.max(1, 10 / (warpFramesLeft + 3)), Math.min(1, warpFramesLeft / 10), 1]); } + if (player.dp) { + const deployingAmount = (player.dp / def.deployment!) * 0.7 + 0.3; + mat4.scale(modelMatrix, modelMatrix, [deployingAmount, deployingAmount, deployingAmount]); + } + player.modelMatrix = modelMatrix; }; @@ -414,7 +422,8 @@ const drawPlayer = (player: Player, lightSources: PointLightData[], isHighlighte const def = defs[player.defIndex]; let bufferData = models[def.modelIndex]; - if (player.isPC || def.kind === UnitKind.Station) { + // No names for gun platforms (index 15) + if (player.isPC || (def.kind === UnitKind.Station && player.defIndex !== 15)) { const name = getNameOfPlayer(player); if (name !== undefined) { let nameData = nameDataCache.get(name); @@ -427,41 +436,42 @@ const drawPlayer = (player: Player, lightSources: PointLightData[], isHighlighte } } - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); - } - - { - const numComponents = 2; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); - } - - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); - } - - // gl.bindVertexArray(bufferData.vertexArrayObject); - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); + // } + + // { + // const numComponents = 2; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); + // } + + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); + // } + + // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + + gl.bindVertexArray(bufferData.vertexArrayObject); // Uniforms gl.activeTexture(gl.TEXTURE0); @@ -527,6 +537,8 @@ const drawPlayer = (player: Player, lightSources: PointLightData[], isHighlighte const offset = 0; gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); + gl.bindVertexArray(null); + { const numComponents = 3; const type = gl.FLOAT; @@ -617,40 +629,42 @@ const drawTarget = (target: Player, where: Rectangle) => { gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, targetDisplayProjectionMatrix); - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); - } - - { - const numComponents = 2; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); - } - - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); - } - - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); + // } + + // { + // const numComponents = 2; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); + // } + + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); + // } + + // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + + gl.bindVertexArray(bufferData.vertexArrayObject); // Uniforms gl.activeTexture(gl.TEXTURE0); @@ -681,6 +695,8 @@ const drawTarget = (target: Player, where: Rectangle) => { const offset = 0; gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); + gl.bindVertexArray(null); + // Draw the players status bars gl.clear(gl.DEPTH_BUFFER_BIT); @@ -801,48 +817,47 @@ const drawAsteroid = (asteroid: Asteroid, lightSources: PointLightData[], isHigh const def = asteroidDefs[asteroid.defIndex]; let bufferData = models[def.modelIndex]; - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); - } - - { - const numComponents = 2; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); - } - - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); - } - - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); - + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); + // } + + // { + // const numComponents = 2; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); + // } + + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); + // } + + gl.bindVertexArray(bufferData.vertexArrayObject); + // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + // Uniforms gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, bufferData.texture); gl.uniform1i(programInfo.uniformLocations.uSampler, 0); - // gl.uniform3fv(programInfo.uniformLocations.baseColor, teamColorsFloat[player.team]); - // find the closest lights let lights: [number, PointLightData][] = []; for (let i = 0; i < pointLightCount; i++) { @@ -897,6 +912,8 @@ const drawAsteroid = (asteroid: Asteroid, lightSources: PointLightData[], isHigh const offset = 0; gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); + gl.bindVertexArray(null); + // Draw the resource bar const resources = Math.max(asteroid.resources, 0) / def.resources; gl.uniform3fv(programInfo.uniformLocations.healthAndEnergyAndScale, [resources, 0, def.radius / 10]); @@ -921,40 +938,42 @@ const drawCollectable = (collectable: Collectable, lightSources: PointLightData[ const def = collectableDefs[collectable.index]; const bufferData = models[def.modelIndex]; - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); - } - - { - const numComponents = 2; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); - } - - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); - } - - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); + // } + + // { + // const numComponents = 2; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); + // } + + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); + // } + + // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + + gl.bindVertexArray(bufferData.vertexArrayObject); // Uniforms gl.activeTexture(gl.TEXTURE0); @@ -1015,6 +1034,8 @@ const drawCollectable = (collectable: Collectable, lightSources: PointLightData[ const type = gl.UNSIGNED_SHORT; const offset = 0; gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); + + gl.bindVertexArray(null); }; const drawTargetAsteroid = (asteroid: Asteroid, where: Rectangle) => { @@ -1051,40 +1072,42 @@ const drawTargetAsteroid = (asteroid: Asteroid, where: Rectangle) => { gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, targetDisplayProjectionMatrix); - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); - } - - { - const numComponents = 2; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); - } - - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); - } - - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); + // } + + // { + // const numComponents = 2; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); + // } + + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); + // } + + // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + + gl.bindVertexArray(bufferData.vertexArrayObject); // Uniforms gl.activeTexture(gl.TEXTURE0); @@ -1116,6 +1139,8 @@ const drawTargetAsteroid = (asteroid: Asteroid, where: Rectangle) => { const offset = 0; gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); + gl.bindVertexArray(null); + // Draw the resource bar const resources = Math.max(asteroid.resources, 0) / def.resources; gl.uniform3fv(programInfo.uniformLocations.healthAndEnergyAndScale, [resources, 0, def.radius / 10]); @@ -1208,40 +1233,42 @@ const drawMine = (mine: Mine, lightSources: PointLightData[], desaturation = 0) const def = mineDefs[mine.defIndex]; let bufferData = models[def.modelIndex]; - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); - } - - { - const numComponents = 2; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); - } - - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); - } - - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); + // } + + // { + // const numComponents = 2; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); + // } + + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); + // } + + // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + + gl.bindVertexArray(bufferData.vertexArrayObject); // Uniforms gl.activeTexture(gl.TEXTURE0); @@ -1297,6 +1324,8 @@ const drawMine = (mine: Mine, lightSources: PointLightData[], desaturation = 0) const type = gl.UNSIGNED_SHORT; const offset = 0; gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); + + gl.bindVertexArray(null); }; const clientMissileUpdate = (missile: Missile) => { @@ -1314,40 +1343,42 @@ const drawMissile = (missile: Missile, lightSources: PointLightData[]) => { const def = missileDefs[missile.defIndex]; let bufferData = models[def.modelIndex]; - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); - } - - { - const numComponents = 2; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); - } - - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); - } - - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); + // } + + // { + // const numComponents = 2; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); + // } + + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); + // } + + // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + + gl.bindVertexArray(bufferData.vertexArrayObject); // Uniforms gl.activeTexture(gl.TEXTURE0); @@ -1403,6 +1434,8 @@ const drawMissile = (missile: Missile, lightSources: PointLightData[]) => { const type = gl.UNSIGNED_SHORT; const offset = 0; gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); + + gl.bindVertexArray(null); }; const setLineUniforms = () => { @@ -1472,40 +1505,42 @@ const doPreviewRendering = (previewRequest: PreviewRequest) => { gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, targetDisplayProjectionMatrix); - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); - } - - { - const numComponents = 2; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); - } - - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); - } - - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); + // } + + // { + // const numComponents = 2; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexTextureCoordBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); + // } + + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, bufferData.vertexNormalBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); + // } + + // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferData.indexBuffer); + + gl.bindVertexArray(bufferData.vertexArrayObject); // Uniforms gl.activeTexture(gl.TEXTURE0); @@ -1535,6 +1570,9 @@ const doPreviewRendering = (previewRequest: PreviewRequest) => { const pixelData = new Uint8Array(4 * 800 * 800); gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); + + gl.bindVertexArray(null); + gl.readPixels(0, canvas.height - 800, 800, 800, gl.RGBA, gl.UNSIGNED_BYTE, pixelData); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); @@ -1578,8 +1616,8 @@ const drawEverything = (target: Player | undefined, targetAsteroid: Asteroid | u canvasGameTopLeft = canvasCoordsToGameCoords(0, 0); canvasGameBottomRight = canvasCoordsToGameCoords(canvas.width, canvas.height); - const sectorX = currentSector % mapSize; - const sectorY = Math.floor(currentSector / mapSize); + const sectorX = currentSector % mapWidth; + const sectorY = Math.floor(currentSector / mapWidth); // Macro origin is at the top left of sector 0,0 canvasMacroTopLeft.x = ((sectorX + 0.5) * sectorBounds.width + canvasGameTopLeft.x) / 2; canvasMacroTopLeft.y = ((sectorY + 0.5) * sectorBounds.width + canvasGameTopLeft.y) / 2; @@ -1627,7 +1665,7 @@ const drawEverything = (target: Player | undefined, targetAsteroid: Asteroid | u if (isRemotelyOnscreen(player.position)) { clientPlayerUpdate(player); - if (!player.inoperable && !player.docked) { + if (!player.inoperable && !player.docked && !player.dp) { const def = defs[player.defIndex]; if (def.pointLights) { for (const light of def.pointLights) { diff --git a/src/3dProjectileDrawing.ts b/src/3dProjectileDrawing.ts index e39d584..edf76cc 100644 --- a/src/3dProjectileDrawing.ts +++ b/src/3dProjectileDrawing.ts @@ -6,40 +6,42 @@ import { Ballistic } from "./game"; import { Model, modelMap } from "./modelLoader"; const drawer = (projectile: Ballistic, model: Model, randomizeMatrix = false) => { - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, model.vertexBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); - } - - { - const numComponents = 2; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, model.vertexTextureCoordBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); - } - - { - const numComponents = 3; - const type = gl.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - gl.bindBuffer(gl.ARRAY_BUFFER, model.vertexNormalBuffer); - gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); - gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); - } - - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.indexBuffer); + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, model.vertexBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); + // } + + // { + // const numComponents = 2; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, model.vertexTextureCoordBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); + // } + + // { + // const numComponents = 3; + // const type = gl.FLOAT; + // const normalize = false; + // const stride = 0; + // const offset = 0; + // gl.bindBuffer(gl.ARRAY_BUFFER, model.vertexNormalBuffer); + // gl.vertexAttribPointer(programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); + // gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); + // } + + // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.indexBuffer); + + gl.bindVertexArray(model.vertexArrayObject); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, model.texture); @@ -70,6 +72,8 @@ const drawer = (projectile: Ballistic, model: Model, randomizeMatrix = false) => const type = gl.UNSIGNED_SHORT; const offset = 0; gl.drawElements(gl.TRIANGLES, vertexCount, type, offset); + + gl.bindVertexArray(null); }; const drawPrimary = (projectile: Ballistic) => { diff --git a/src/defs/armaments.ts b/src/defs/armaments.ts index f899ce6..6392922 100644 --- a/src/defs/armaments.ts +++ b/src/defs/armaments.ts @@ -4,7 +4,9 @@ import { availableCargoCapacity, CloakedState, EffectAnchorKind, + effectiveInfinity, EffectTrigger, + equip, findAllPlayersOverlappingCircle, GlobalState, Mine, @@ -14,7 +16,7 @@ import { TargetKind, } from "../game"; import { canonicalizeAngle, findHeadingBetween, l2Norm, l2NormSquared, Position, Rectangle } from "../geometry"; -import { defs, PointLightData, SlotKind, UnitKind } from "./shipsAndStations"; +import { defs, emptyLoadout, PointLightData, SlotKind, UnitKind } from "./shipsAndStations"; import { clientUid as uid } from "../defs"; import { asteroidDefs } from "./asteroids"; import { projectileDefs } from "./projectiles"; @@ -1039,15 +1041,8 @@ const initArmaments = () => { slotData.ammo--; state.delayedActions.push({ frames: 140, - action: (applyEffect: (trigger: EffectTrigger) => void) => { - applyEffect({ effectIndex: 22 }); - for (const otherPlayer of state.players.values()) { - if (otherPlayer === player) { - continue; - } - otherPlayer.disabled = (otherPlayer.disabled ?? 0) + 1200; - } - }, + action: "emp", + data: player.id, }); applyEffect({ effectIndex: 21, from: { kind: EffectAnchorKind.Player, value: player.id } }); } @@ -1059,6 +1054,113 @@ const initArmaments = () => { tier: 1, }); + // EMP Mine - 22 + mineDefs.push({ + explosionEffectIndex: 10, + explosionMutator(mine, state) { + // reuse the mine as the circle object for the collision detection for the explosion + mine.radius = 45; + const players = findAllPlayersOverlappingCircle(mine, state.players.values()); + for (let i = 0; i < players.length; i++) { + players[i].disabled = (players[i].disabled ?? 0) + 120; + } + }, + model: "emp_mine", + pointLights: [ + { position: { x: 0, y: 0, z: 0.5 }, color: [0.0, 0.0, 3.0] }, + { position: { x: 0, y: 0, z: -0.5 }, color: [0.0, 0.0, 3.0] }, + ], + deploymentTime: 25, + }); + const empMineIndex = mineDefs.length - 1; + armDefs.push({ + name: "EMP Mine", + description: "A mine which emits an electromagnetic pulse when triggered", + kind: SlotKind.Mine, + usage: ArmUsage.Ammo, + targeted: TargetedKind.Untargeted, + maxAmmo: 25, + fireMutator: (state, player, targetKind, target, applyEffect, slotId, flashServerMessage, whatMutated) => { + const slotData = player.slotData[slotId]; + if (player.energy > 1 && slotData.sinceFired > 33 && slotData.ammo > 0) { + player.energy -= 1; + slotData.sinceFired = 0; + slotData.ammo--; + const id = uid(); + const mine: Mine = { + id, + position: { x: player.position.x, y: player.position.y }, + speed: 0, + heading: Math.random() * 2 * Math.PI, + radius: 15, + team: player.team, + defIndex: empMineIndex, + left: 1400, + deploying: 30, + }; + state.mines.set(id, mine); + whatMutated.mines.push(mine); + applyEffect({ effectIndex: 12, from: { kind: EffectAnchorKind.Absolute, value: player.position } }); + } + }, + equipMutator: (player, slotIndex) => { + player.slotData[slotIndex] = { sinceFired: 1000, ammo: 25 }; + }, + frameMutator: (player, slotIndex) => { + const slotData = player.slotData[slotIndex]; + slotData.sinceFired++; + }, + cost: 200, + tier: 1, + }); + + // Gun Platform - 23 + armDefs.push({ + name: "Gun Platform", + description: "A deployable gun platform", + kind: SlotKind.Large, + usage: ArmUsage.Ammo, + targeted: TargetedKind.Untargeted, + maxAmmo: 1, + fireMutator: (state, player, targetKind, target, applyEffect, slotIndex, flashServerMessage, whatMutated) => { + const slotData = player.slotData[slotIndex]; + if (slotData.ammo > 0) { + slotData.ammo--; + const id = uid(); + const def = defs[15]; + let platform: Player = { + position: { x: player.position.x, y: player.position.y }, + radius: def.radius, + speed: 0, + heading: Math.random() * 2 * Math.PI, + health: def.health, + id, + sinceLastShot: [1000], + energy: def.energy, + defIndex: 15, + arms: emptyLoadout(15), + slotData: [{}], + team: player.team, + side: 0, + dp: 1, + v: { x: 0, y: 0 }, + iv: { x: 0, y: 0 }, + ir: 0, + }; + platform = equip(platform, 0, "Heavy Tomahawk Missile", true); + platform.slotData[0].ammo = effectiveInfinity; + state.players.set(id, platform); + // TODO Make a new sound for this + applyEffect({ effectIndex: 12, from: { kind: EffectAnchorKind.Absolute, value: player.position } }); + } + }, + equipMutator: (player, slotIndex) => { + player.slotData[slotIndex] = { ammo: 1 }; + }, + cost: 100, + tier: 1, + }); + for (let i = 0; i < armDefs.length; i++) { const def = armDefs[i]; armDefMap.set(def.name, { index: i, def }); diff --git a/src/defs/delayedAction.ts b/src/defs/delayedAction.ts new file mode 100644 index 0000000..778c7cb --- /dev/null +++ b/src/defs/delayedAction.ts @@ -0,0 +1,21 @@ +import { EffectTrigger, GlobalState } from "../game" + +type DelayedAction = { + frames: number; + action: string; + data: any; +}; + +const delayedActionDefMap = new Map void, data: any) => void>(); + +delayedActionDefMap.set("emp", (state: GlobalState, applyEffect: (trigger: EffectTrigger) => void, data: any) => { + applyEffect({ effectIndex: 22 }); + for (const otherPlayer of state.players.values()) { + if (otherPlayer.id === data) { + continue; + } + otherPlayer.disabled = (otherPlayer.disabled ?? 0) + 1200; + } +}); + +export { DelayedAction, delayedActionDefMap }; diff --git a/src/defs/shipsAndStations.ts b/src/defs/shipsAndStations.ts index 943ee98..2fcbeb4 100644 --- a/src/defs/shipsAndStations.ts +++ b/src/defs/shipsAndStations.ts @@ -56,6 +56,7 @@ type UnitDefinition = { model: string; modelIndex?: number; pointLights?: PointLightData[]; + deployment?: number; }; const defs: UnitDefinition[] = []; @@ -486,6 +487,32 @@ const initShipsAndStations = () => { isCloaky: true, model: "infiltrator", }); + // Gun Platform - 15 + defs.push({ + name: "Gun Platform", + description: "A static gun platform", + health: 800, + speed: 0, + energy: 1100, + energyRegen: 0.5, + primaryReloadTime: 10, + primaryDamage: 15, + radius: 25, + kind: UnitKind.Station, + slots: [SlotKind.Normal], + hardpoints: [{ x: 0, y: 0 }], + dockable: false, + deathEffect: 4, + healthRegen: 0.1, + repairsRequired: 8, + primaryDefIndex: 0, + mass: effectiveInfinity, + model: "gun_platform", + deployment: 120, + pointLights: [ + { position: { x: 0, y: 0, z: 5 }, color: [2.0, 2.0, 2.0] }, + ], + }); for (let i = 0; i < defs.length; i++) { const def = defs[i]; diff --git a/src/dialogs/map.ts b/src/dialogs/map.ts index 97a0205..f7f71aa 100644 --- a/src/dialogs/map.ts +++ b/src/dialogs/map.ts @@ -1,16 +1,16 @@ import { horizontalCenter, pop, push } from "../dialog"; import { currentSector, isInMission, sectorData } from "../globals"; import { sendWarp } from "../net"; -import { mapSize } from "../game"; import { selectedMissionsDialog, setupSelectedMissionsDialog } from "./selectedMissions"; import { abortWrapper } from "./abortMission"; import { sideBySideDivs } from "./helpers"; +import { mapHeight, mapWidth } from "../mapLayout"; const populateSectorInfo = (sector: number) => { const sectorInfo = document.getElementById("sectorInfo") as HTMLDivElement; if (sectorInfo) { - const sectorX = sector % mapSize; - const sectorY = Math.floor(sector / mapSize); + const sectorX = sector % mapWidth; + const sectorY = Math.floor(sector / mapWidth); if (sectorData.has(sector)) { const data = sectorData.get(sector); if (data) { @@ -36,29 +36,30 @@ const populateSectorInfo = (sector: number) => { const mapHtml = '
' + - new Array(mapSize * mapSize) + new Array(mapWidth * mapHeight) .fill(0) .map((_, i) => { - const x = i % mapSize; - const y = Math.floor(i / mapSize); + const x = i % mapWidth; + const y = Math.floor(i / mapWidth); return `
${x}-${y}
`; }) .join("") + "
"; const sectorNumberToXY = (sector: number) => { - if (sector > mapSize * mapSize) { + if (sector > mapWidth * mapHeight) { return isInMission() ? "Mission Sector" : "Tutorial Sector"; } - const x = sector % mapSize; - const y = Math.floor(sector / mapSize); + const x = sector % mapWidth; + const y = Math.floor(sector / mapWidth); return `${x}-${y}`; }; const setCurrentSectorText = () => { const currentSectorText = document.getElementById("currentSectorText"); if (currentSectorText) { - currentSectorText.innerText = `Current Sector: ${sectorNumberToXY(currentSector)}`; + // currentSectorText.innerText = `Current Sector: ${sectorNumberToXY(currentSector)}`; + currentSectorText.innerText = `Current Sector: ${currentSector}`; } }; @@ -67,13 +68,13 @@ const mapDialog = () => { `

Map

`, `

`, `
-
${mapHtml}
-
+
${mapHtml}
+
`, sideBySideDivs([ ``, - ``, + ``, ], true), ``, ])}`; @@ -86,11 +87,23 @@ const setupMapDialog = () => { document.getElementById("seeActiveMissions")?.addEventListener("click", () => { push(selectedMissionsDialog(), setupSelectedMissionsDialog, "selectedMissions"); }); - for (let i = 0; i < mapSize * mapSize; i++) { + for (let i = 0; i < mapWidth * mapHeight; i++) { document.getElementById(`sector-${i}`)?.addEventListener("click", () => { populateSectorInfo(i); }); } + // document.getElementById("warpButton")?.addEventListener("click", () => { + // try { + // const toSector = parseInt((document.getElementById("sectorInput") as HTMLInputElement)?.value); + // abortWrapper(() => { + // sendWarp(toSector); + // pop(); + // }); + // } catch (e) { + // console.log(e); + // } + // }); + setCurrentSectorText(); }; diff --git a/src/dialogs/settings.ts b/src/dialogs/settings.ts index 6093106..3dfa75e 100644 --- a/src/dialogs/settings.ts +++ b/src/dialogs/settings.ts @@ -23,7 +23,7 @@ const settingsDialog = () => ` `, `Particle Count`, - `Use experimental backgrounds (can be laggy)`, + `Use experimental backgrounds (IS BROKEN DUE TO MAP REWORK!)`, `
${keylayoutSelector()}
`, ``, ``, diff --git a/src/dialogs/social.ts b/src/dialogs/social.ts index 9159951..ffa3846 100644 --- a/src/dialogs/social.ts +++ b/src/dialogs/social.ts @@ -30,7 +30,8 @@ const sectorLocationTemplate = (value: SectorOfPlayerResult) => { if (value.sectorKind === SectorKind.Mission) { return "In Mission"; } - return sectorNumberToXY(value.sectorNumber); + // return sectorNumberToXY(value.sectorNumber); + return value.sectorNumber.toString(); }; const warpIfNotDocked = (id: number) => { diff --git a/src/game.ts b/src/game.ts index 07063b4..003d4b8 100644 --- a/src/game.ts +++ b/src/game.ts @@ -18,6 +18,7 @@ import { mineDefs, AsteroidDef, } from "./defs"; +import { DelayedAction, delayedActionDefMap } from "./defs/delayedAction"; import { projectileDefs } from "./defs/projectiles"; import { Circle, @@ -39,11 +40,10 @@ import { pointInRectangle, canonicalizeAngle, } from "./geometry"; -import { NPC } from "./npc"; +import { ResourceDensity } from "./mapLayout"; import { seek } from "./pathing"; import { sfc32 } from "./prng"; - -// TODO Move the geometry stuff to a separate file +import { LootTable } from "./defs/collectables"; type Entity = Circle & { id: number; speed: number; heading: number }; @@ -127,8 +127,23 @@ type Player = Entity & { modelMatrix?: any; // Delayed damage dd?: DelayedDamage[]; + // For the tutorial only + doNotShootYet?: boolean; + // deploying + dp?: number; }; +interface NPC { + player: Player; + input: Input; + angle: number | undefined; + selectedSecondary: number; + secondariesToFire: number[]; + lootTable: LootTable; + targetId: number; + process: (state: GlobalState, sector: number) => void; +} + type Asteroid = Circle & { id: number; resources: number; @@ -281,17 +296,17 @@ type EffectTrigger = { to?: EffectAnchor; }; -type DelayedAction = { - frames: number; - action: (applyEffect: (trigger: EffectTrigger) => void) => void; -}; - enum SectorKind { Overworld = 0, Tutorial = 1, Mission = 2, } +type TransferableAction = { + index: number; + data: any; +}; + type GlobalState = { players: Map; projectiles: Map; @@ -303,6 +318,9 @@ type GlobalState = { projectileId?: number; delayedActions?: DelayedAction[]; sectorKind?: SectorKind; + sectorChecks?: TransferableAction[]; + dynamic?: boolean; + creationTime?: number; }; const setCanDockOrRepair = (player: Player, state: GlobalState) => { @@ -487,15 +505,6 @@ const kill = ( if (toDrop !== null) { collectables.push(createCollectableFromDef(toDrop, player.position)); } - if (player.npc.killed) { - // This should be protected with a try/catch because the killed handler can interact with anything - // and the game update loop is not protected against exceptions - try { - player.npc.killed(); - } catch (e) { - console.error(e); - } - } } } }; @@ -584,6 +593,8 @@ const applyCollisionForce = (collider: Player, collidee: Circle, collideeMass = // Idk if this is the right approach or not, but I need something that cuts down on unnecessary things being sent over the websocket type Mutated = { asteroids: Set; collectables: Collectable[]; mines: Mine[] }; +// TODO Move the update function out of the src directory so that we don't need this dependency injection + // Like usual the update function is a monstrosity // It could probably use some refactoring const update = ( @@ -599,7 +610,9 @@ const update = ( removeMine: (id: number, detonated: boolean) => void, knownRecipes: Map>, discoverRecipe: (id: number, recipe: string) => void, - secondariesToActivate: Map + secondariesToActivate: Map, + transferableActions: ((state: GlobalState, sector: number, data: any) => boolean)[], + sectorNumber: number ) => { const ret: Mutated = { asteroids: new Set(), collectables: [], mines: [] }; @@ -608,7 +621,7 @@ const update = ( const action = state.delayedActions[i]; action.frames--; if (action.frames <= 0) { - action.action(applyEffect); + delayedActionDefMap.get(action.action)(state, applyEffect, action.data); state.delayedActions.splice(i, 1); i--; } @@ -956,10 +969,19 @@ const update = ( player.energy = Math.max(0, player.energy - 2 * deltaEnergy); } } else { + // Handle deployment + if (player.dp) { + if (player.dp < def.deployment) { + player.dp++; + } else { + player.dp = undefined; + } + } + // Have stations spin slowly player.heading = positiveMod(player.heading + 0.003, 2 * Math.PI); // Have the stations fire their primary weapons - if (!player.inoperable && !player.disabled) { + if (!player.inoperable && !player.disabled && !player.dp) { let closestEnemy: Player | undefined; let closestEnemyDistanceSquared = Infinity; for (const [otherId, otherPlayer] of state.players) { @@ -1011,6 +1033,21 @@ const update = ( state.projectileId++; } } + const targetKind = TargetKind.Player; + for (let slotId = 0; slotId < player.slotData.length; slotId++) { + const armDef = armDefs[player.arms[slotId]]; + // Targeted weapons + if (armDef.targeted === TargetedKind.Targeted) { + armDef.fireMutator(state, player, targetKind, closestEnemy, applyEffect, slotId, flashServerMessage, ret); + // Untargeted weapons + } else if (armDef.targeted === TargetedKind.Untargeted) { + if (slotId !== undefined && slotId < player.arms.length) { + if (armDef.fireMutator) { + armDef.fireMutator(state, player, TargetKind.None, undefined, applyEffect, slotId, flashServerMessage, ret); + } + } + } + } } } else if (player.inoperable) { for (let i = 0; i < player.repairs.length; i++) { @@ -1026,6 +1063,13 @@ const update = ( if (player.disabled > 0) { player.disabled = Math.max(0, player.disabled - 3); } + // Run the secondary frameMutators + player.arms.forEach((armament, index) => { + const armDef = armDefs[armament]; + if (armDef.frameMutator) { + armDef.frameMutator(player, index, state, flashServerMessage); + } + }); } // Update primary times since last shot (secondaries are handled in the frameMutators in the armDefs) for (let i = 0; i < player.sinceLastShot.length; i++) { @@ -1070,6 +1114,17 @@ const update = ( for (const collectable of ret.collectables) { state.collectables.set(collectable.id, collectable); } + // Sector checks + if (frameNumber % 60 === 0) { + for (let i = 0; i < state.sectorChecks.length; i++) { + const action = state.sectorChecks[i]; + const toRemove = transferableActions[action.index](state, sectorNumber, action.data); + if (toRemove) { + state.sectorChecks.splice(i, 1); + i--; + } + } + } return ret; }; @@ -1221,14 +1276,7 @@ const applyInputs = (input: Input, player: Player, angle?: number) => { player.omega = player.heading - (player.omega % (2 * Math.PI)); }; -const randomAsteroids = ( - count: number, - bounds: Rectangle, - seed: number, - uid: () => number, - typeDensities: { resource: string; density: number }[], - stations: Player[] -) => { +const randomAsteroids = (count: number, bounds: Rectangle, seed: number, uid: () => number, typeDensities: ResourceDensity[], stations: Player[]) => { if (asteroidDefs.length === 0) { throw new Error("Asteroid defs not initialized"); } @@ -1354,6 +1402,7 @@ const findPreviousTargetAsteroid = (player: Player, current: Asteroid | undefine return ret; }; +// Note that this function return the new player object which has been modified (old is unmodified) const equip = (player: Player, slotIndex: number, what: string | number, noCost?: boolean) => { const def = defs[player.defIndex]; if (slotIndex >= def.slots.length) { @@ -1516,19 +1565,6 @@ const randomNearbyPointInSector = (point: Position, distance: number) => { return ret; }; -const isValidSectorInDirection = (sector: number, direction: CardinalDirection) => { - if (direction === CardinalDirection.Up) { - return sector >= mapSize; - } else if (direction === CardinalDirection.Down) { - return sector < mapSize * (mapSize - 1); - } else if (direction === CardinalDirection.Left) { - return sector % mapSize !== 0; - } else if (direction === CardinalDirection.Right) { - return sector % mapSize !== mapSize - 1; - } - return false; -}; - type SectorInfo = { sector: number; resources: string[]; @@ -1575,10 +1611,13 @@ type ClientFriendRequest = { outgoing: boolean; }; -type SectorOfPlayerResult = { - sectorNumber: number; - sectorKind: SectorKind; -} | "respawning" | null; +type SectorOfPlayerResult = + | { + sectorNumber: number; + sectorKind: SectorKind; + } + | "respawning" + | null; export { GlobalState, @@ -1605,6 +1644,8 @@ export { ClientFriendRequest, SectorKind, SectorOfPlayerResult, + TransferableAction, + NPC, update, applyInputs, processAllNpcs, @@ -1639,7 +1680,6 @@ export { effectiveInfinity, serverMessagePersistTime, // clientMineDeploymentUpdater, - isValidSectorInDirection, sectorBounds, sectorDelta, mapSize, diff --git a/src/geometry.ts b/src/geometry.ts index da8e4e9..7a9f888 100644 --- a/src/geometry.ts +++ b/src/geometry.ts @@ -7,12 +7,16 @@ type Rectangle = { x: number; y: number; width: number; height: number }; type Line = { from: Position; to: Position }; enum CardinalDirection { - Up, - Right, - Down, - Left, + Up = 0, + Right = 1, + Down = 2, + Left = 3, } +const oppositeDirection = (direction: CardinalDirection) => { + return (direction + 2) % 4; +}; + const sumPositions = (...positions: Position[]) => { return positions.reduce( (sum, position) => { @@ -198,6 +202,7 @@ export { Rectangle, Line, CardinalDirection, + oppositeDirection, positiveMod, maxDecimals, infinityNorm, diff --git a/src/globals.ts b/src/globals.ts index 2d4edd7..a9bea8e 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -4,8 +4,9 @@ import { defaultKeyLayout } from "./config"; import { Faction } from "./defs"; import { runPostUpdaterOnly, updateDom } from "./dialog"; import { redrawTip } from "./dialogs/dead"; -import { ClientFriendRequest, GlobalState, mapSize, Player, SectorInfo, TutorialStage } from "./game"; +import { ClientFriendRequest, GlobalState, Player, SectorInfo, TutorialStage } from "./game"; import { azertyBindings, dvorakBindings, KeyBindings, KeyLayouts, qwertyBindings, useKeybindings } from "./keybindings"; +import { mapHeight, mapWidth } from "./mapLayout"; import { getRestRaw } from "./rest"; import { tutorialPrompters } from "./tutorial"; @@ -169,7 +170,7 @@ const setTutorialStage = (newTutorialStage: TutorialStage) => { }; const isInMission = () => { - return tutorialStage === TutorialStage.Done && currentSector >= mapSize * mapSize; + return tutorialStage === TutorialStage.Done && currentSector >= mapWidth * mapHeight; } let missionComplete = false; diff --git a/src/index.ts b/src/index.ts index 27cc9fc..1fedb12 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { connect, bindAction, sendDock, sendTarget, sendSecondary, sendAngle, sendRepair, sendTutorialStageComplete } from "./net"; +import { connect, bindAction, sendDock, sendTarget, sendSecondary, sendAngle, sendRepair, sendTutorialStageComplete, changeServers, sendServerHopKey } from "./net"; import { Player, Ballistic, @@ -355,12 +355,7 @@ const initMine = (mine: Mine) => { mine.pitch = Math.random(); }; -const run = () => { - addLoadingText("Initializing client game state"); - initBlankState(); - - addLoadingText("Binding network handlers"); - +const bindAllActions = () => { bindAction( "init", (data: { @@ -433,8 +428,6 @@ const run = () => { console.error("Error from server: " + data.message); }); - bindDockingUpdaters(); - bindAction("state", (data: any) => { state.players.clear(); state.projectiles.clear(); @@ -533,7 +526,6 @@ const run = () => { initMine(mine); state.mines.set(mine.id, mine); } - // initStars(data.to); clearEffects(); setCurrentSector(data.to); setCurrentSectorText(); @@ -582,9 +574,6 @@ const run = () => { } }); - bindManufacturingUpdaters(); - bindInventoryUpdaters(); - bindAction("inventory", (entries: CargoEntry[]) => { clearInventory(); for (const entry of entries) { @@ -627,9 +616,29 @@ const run = () => { bindAction("setMissionTarget", (targetId) => { setMissionTargetId(targetId); }); + + bindAction("changeServers", (data: { to: string, key: string }) => { + changeServers(data.to, () => { + bindAllActions(); + sendServerHopKey(data.key); + }); + }); +}; + +const run = () => { + addLoadingText("Initializing client game state"); + initBlankState(); + + addLoadingText("Binding network handlers"); + bindAllActions(); + bindDockingUpdaters(); + bindPostUpdater("arms", rasterizeWeaponText); + bindManufacturingUpdaters(); + bindInventoryUpdaters(); + addLoadingText("Launching..."); displayLoginDialog(); diff --git a/src/mapLayout.ts b/src/mapLayout.ts new file mode 100644 index 0000000..3465df0 --- /dev/null +++ b/src/mapLayout.ts @@ -0,0 +1,12 @@ +import { createTorus } from "./sectorGraph"; + +const width = 6; +const height = 3; + +const mapGraph = createTorus(width, height); + +const peerCount = 3; + +type ResourceDensity = { resource: string; density: number }; + +export { ResourceDensity, mapGraph, width as mapWidth, height as mapHeight, peerCount }; diff --git a/src/modelLoader.ts b/src/modelLoader.ts index cff0ab5..aa5ee22 100644 --- a/src/modelLoader.ts +++ b/src/modelLoader.ts @@ -197,6 +197,17 @@ class Model { this.indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.indices), gl.STATIC_DRAW); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); + + // find the max index + // let maxIndex = 0; + // for (const index of this.indices) { + // if (index > maxIndex) { + // maxIndex = index; + // } + // } + + // console.log(`Max index: ${maxIndex}, num vertices: ${this.vertices.length / 3}, num indices: ${this.indices.length}`); this.texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.texture); @@ -245,6 +256,8 @@ class Model { gl.enableVertexAttribArray(programInfo.attribLocations.vertexNormal); } + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); + gl.bindVertexArray(null); } } @@ -264,7 +277,6 @@ const loadObj = (file: string, gl: WebGL2RenderingContext, programInfo: any) => }); }; -// Shouldn't be in this file, but it is const loadTexture = (file: string, gl: WebGL2RenderingContext) => { return new Promise((resolve, reject) => { const texture = gl.createTexture(); diff --git a/src/net.ts b/src/net.ts index 5713ae9..0b80487 100644 --- a/src/net.ts +++ b/src/net.ts @@ -5,40 +5,22 @@ import { addLoadingText } from "./globals"; let serverSocket: WebSocket; -const login = (name: string, password: string) => { - serverSocket.send( - JSON.stringify({ - type: "login", - payload: { name, password }, - }) - ); -}; - -const register = (name: string, password: string, faction: Faction) => { - serverSocket.send( - JSON.stringify({ - type: "register", - payload: { name, password, faction }, - }) - ); -}; - const bindings: Map void> = new Map(); let heartbeatInterval: number; // Client connection code -const connect = (callback: (socket: WebSocket) => void) => { +const connect = (callback: () => void, to: string = wsUrl) => { addLoadingText("Connecting to server..."); - const socket = new WebSocket(wsUrl); + const socket = new WebSocket(to); socket.onopen = () => { - console.log(`Connected to the server at ${wsUrl}`); + console.log(`Connected to the server at ${to}`); serverSocket = socket; addLoadingText("Connected to server!"); heartbeatInterval = window.setInterval(() => { socket.send(JSON.stringify({ type: "heartbeat" })); }, 25 * 1000); - callback(socket); + callback(); }; socket.onclose = () => { console.log("Disconnected from the server"); @@ -55,6 +37,12 @@ const connect = (callback: (socket: WebSocket) => void) => { }; }; +const changeServers = (url: string, callback: () => void) => { + serverSocket.close(); + clearInterval(heartbeatInterval); + connect(callback, url); +}; + const bindAction = (action: string, callback: (data: any) => void) => { bindings.set(action, callback); }; @@ -63,6 +51,24 @@ const unbindAllActions = () => { serverSocket.onmessage = null; }; +const login = (name: string, password: string) => { + serverSocket.send( + JSON.stringify({ + type: "login", + payload: { name, password }, + }) + ); +}; + +const register = (name: string, password: string, faction: Faction) => { + serverSocket.send( + JSON.stringify({ + type: "register", + payload: { name, password, faction }, + }) + ); +}; + const sendInput = (input: Input) => { const inputToSend = { up: input.up, @@ -324,8 +330,19 @@ const sendFriendWarp = (id: number) => { ); }; +const sendServerHopKey = (key: string) => { + serverSocket.send( + JSON.stringify({ + type: "serverHopKey", + payload: { key }, + }) + ); +}; + export { connect, + changeServers, + sendServerHopKey, bindAction, unbindAllActions, login, diff --git a/src/sectorGraph.ts b/src/sectorGraph.ts new file mode 100644 index 0000000..9145a94 --- /dev/null +++ b/src/sectorGraph.ts @@ -0,0 +1,137 @@ +import { CardinalDirection, oppositeDirection } from "./geometry"; + +type SectorGraphNode = { + out: [SectorGraphEdge, SectorGraphEdge, SectorGraphEdge, SectorGraphEdge]; + in: [SectorGraphEdge, SectorGraphEdge, SectorGraphEdge, SectorGraphEdge]; + sector: number; +}; + +type SectorGraphEdge = { + from: SectorGraphNode; + to: SectorGraphNode; + isReflection?: boolean; +}; + +type SectorGraph = Map; + +const createReflectionEdge = (node: SectorGraphNode, direction: CardinalDirection) => { + const reflectionEdge: SectorGraphEdge = { + from: node, + to: node, + isReflection: true, + }; + node.out[direction] = reflectionEdge; + node.in[oppositeDirection(direction)] = reflectionEdge; +}; + +const createEdge = (from: SectorGraphNode, to: SectorGraphNode, direction: CardinalDirection) => { + const edgeA: SectorGraphEdge = { + from, + to, + }; + from.out[direction] = edgeA; + to.in[oppositeDirection(direction)] = edgeA; + const edgeB: SectorGraphEdge = { + from: to, + to: from, + }; + to.out[oppositeDirection(direction)] = edgeB; + from.in[direction] = edgeB; +}; + +const getEdge = (graph: SectorGraph, from: number, direction: number) => { + const node = graph.get(from); + if (node === undefined) { + return undefined; + } + return node.out[direction]; +}; + +const removeContiguousSubgraph = (graph: SectorGraph, start: number) => { + const visited = new Set(); + const toVisit = [start]; + while (toVisit.length > 0) { + const sector = toVisit.pop(); + if (sector === undefined) { + continue; + } + if (visited.has(sector)) { + continue; + } + visited.add(sector); + const node = graph.get(sector); + if (node === undefined) { + continue; + } + for (const edge of node.out) { + if (edge === undefined) { + continue; + } + const { from, to } = edge; + if (from.sector === sector) { + toVisit.push(to.sector); + } + } + } +}; + +const createIsolatedSector = (graph: SectorGraph, sector: number) => { + const node: SectorGraphNode = { + out: [undefined, undefined, undefined, undefined], + in: [undefined, undefined, undefined, undefined], + sector, + }; + graph.set(sector, node); + for (let i = 0; i < 4; i++) { + createReflectionEdge(node, i); + } +}; + +const createTorus = (width: number, height: number) => { + const graph = new Map(); + for (let i = 0; i < width; i++) { + for (let j = 0; j < height; j++) { + const sector = j * width + i; + const node: SectorGraphNode = { + out: [undefined, undefined, undefined, undefined], + in: [undefined, undefined, undefined, undefined], + sector, + }; + graph.set(sector, node); + } + } + + for (let i = 0; i < width; i++) { + for (let j = 0; j < height; j++) { + const sector = j * width + i; + const node = graph.get(sector); + createEdge(node, graph.get((((j + height - 1) % height) * width) + i)!, CardinalDirection.Up); + createEdge(node, graph.get((((j + 1) % height) * width) + i)!, CardinalDirection.Down); + createEdge(node, graph.get(j * width + ((i + width - 1) % width))!, CardinalDirection.Left); + createEdge(node, graph.get(j * width + ((i + 1) % width))!, CardinalDirection.Right); + } + } + + return graph; +}; + +const mergeDisjointSubgraphs = (into: Map, ...graphs: SectorGraph[]) => { + for (const graph of graphs) { + for (const [sector, node] of graph) { + into.set(sector, node); + } + } +}; + +export { + SectorGraph, + SectorGraphNode, + SectorGraphEdge, + createReflectionEdge, + createEdge, + createIsolatedSector, + getEdge, + removeContiguousSubgraph, + createTorus, + mergeDisjointSubgraphs, +}; diff --git a/src/tutorial.ts b/src/tutorial.ts index 3f6879c..30b0492 100644 --- a/src/tutorial.ts +++ b/src/tutorial.ts @@ -3,9 +3,10 @@ import { hasArm } from "./defs/armaments"; import { peekTag } from "./dialog"; import { sectorNumberToXY } from "./dialogs/map"; import { pushMessage, rasterizeWeaponText } from "./2dDrawing"; -import { availableCargoCapacity, mapSize, TutorialStage } from "./game"; +import { availableCargoCapacity, TutorialStage } from "./game"; import { currentSector, faction, inventory, keybind, lastSelf, selectedSecondary, state } from "./globals"; import { targetAsteroidId, targetId } from "./index"; +import { mapHeight, mapWidth } from "./mapLayout"; let promptInterval: number; let promptTimeout: number; @@ -58,7 +59,7 @@ tutorialCheckers.set(TutorialStage.LaserBeam, () => { }); tutorialCheckers.set(TutorialStage.Map, () => { - return currentSector < mapSize * mapSize; + return currentSector < mapWidth * mapHeight; }); tutorialCheckers.set(TutorialStage.Dock, () => { @@ -66,7 +67,7 @@ tutorialCheckers.set(TutorialStage.Dock, () => { }); tutorialCheckers.set(TutorialStage.Deposit, () => { - return inventory["Prifecite"] > 50; + return inventory["Prifecite"] > 45; }); tutorialCheckers.set(TutorialStage.Manufacture1, () => { diff --git a/styles.css b/styles.css index 4ff3a61..7d66240 100644 --- a/styles.css +++ b/styles.css @@ -187,19 +187,19 @@ body { } .grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 3%; + display: inline-grid; + grid-template-columns: repeat(6, 1fr); + gap: 1%; } .square { aspect-ratio: 1/ 1; display: flex; align-items: center; - padding: 2%; + padding: 1%; background-color: #1e1e1e; color: #fff; - height: 10vh; + height: 6.67vh; justify-content: center; } diff --git a/tests/sectorGraphTests.ts b/tests/sectorGraphTests.ts new file mode 100644 index 0000000..a3404ed --- /dev/null +++ b/tests/sectorGraphTests.ts @@ -0,0 +1,28 @@ +import { assert } from "console"; +import { inspect } from "util"; +import { CardinalDirection } from "../src/geometry"; +import { SectorGraph, createEdge, createReflectionEdge, createIsolatedSector, SectorGraphNode, createTorus } from "../src/sectorGraph"; + +console.log("sectorGraphTests.ts"); + +const isolatedGraph = new Map(); +createIsolatedSector(isolatedGraph, 0); + +assert(isolatedGraph.get(0)!.out[CardinalDirection.Up]?.to.sector === 0); +assert(isolatedGraph.get(0)!.out[CardinalDirection.Down]?.to.sector === 0); +assert(isolatedGraph.get(0)!.out[CardinalDirection.Left]?.to.sector === 0); +assert(isolatedGraph.get(0)!.out[CardinalDirection.Right]?.to.sector === 0); + +const torusGraph = createTorus(3, 2); + +assert(torusGraph.get(0)!.out[CardinalDirection.Up]?.to.sector === 3); +assert(torusGraph.get(0)!.out[CardinalDirection.Down]?.to.sector === 3); + +assert(torusGraph.get(3)!.out[CardinalDirection.Up]?.to.sector === 0); +assert(torusGraph.get(3)!.out[CardinalDirection.Down]?.to.sector === 0); + +assert(torusGraph.get(5)!.out[CardinalDirection.Left]?.to.sector === 4); +assert(torusGraph.get(5)!.out[CardinalDirection.Right]?.to.sector === 3); + +assert(torusGraph.get(3)!.out[CardinalDirection.Left]?.to.sector === 5); +assert(torusGraph.get(3)!.out[CardinalDirection.Right]?.to.sector === 4); diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..32fa4e7 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "lib": [ + "es2017" + ], + "downlevelIteration": true, + "esModuleInterop": true, + "typeRoots": [ + "../server/node_modules/@types" + ] + } +} \ No newline at end of file