diff --git a/games/nukes/package-lock.json b/games/nukes/package-lock.json
index c82a23f1..dcba20fd 100644
--- a/games/nukes/package-lock.json
+++ b/games/nukes/package-lock.json
@@ -27,7 +27,8 @@
"react-router-dom": "^6.21.1",
"styled-components": "^6.1.6",
"typescript": "^5.2.2",
- "vite": "^5.0.0"
+ "vite": "^5.0.0",
+ "vite-plugin-checker": "^0.6.4"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -1482,6 +1483,33 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -1503,6 +1531,19 @@
"node": ">=4"
}
},
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1699,6 +1740,18 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1819,6 +1872,30 @@
"node": ">=4"
}
},
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -1834,6 +1911,15 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
+ "node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "dev": true,
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2762,6 +2848,20 @@
"is-callable": "^1.1.3"
}
},
+ "node_modules/fs-extra": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
+ "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2946,6 +3046,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true
+ },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -3138,6 +3244,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-boolean-object": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
@@ -3496,6 +3614,18 @@
"node": ">=6"
}
},
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -3672,6 +3802,27 @@
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
"dev": true
},
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4087,6 +4238,18 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
@@ -4556,6 +4719,12 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "dev": true
+ },
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -4736,6 +4905,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@@ -4830,6 +5008,202 @@
}
}
},
+ "node_modules/vite-plugin-checker": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.6.4.tgz",
+ "integrity": "sha512-2zKHH5oxr+ye43nReRbC2fny1nyARwhxdm0uNYp/ERy4YvU9iZpNOsueoi/luXw5gnpqRSvjcEPxXbS153O2wA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "ansi-escapes": "^4.3.0",
+ "chalk": "^4.1.1",
+ "chokidar": "^3.5.1",
+ "commander": "^8.0.0",
+ "fast-glob": "^3.2.7",
+ "fs-extra": "^11.1.0",
+ "npm-run-path": "^4.0.1",
+ "semver": "^7.5.0",
+ "strip-ansi": "^6.0.0",
+ "tiny-invariant": "^1.1.0",
+ "vscode-languageclient": "^7.0.0",
+ "vscode-languageserver": "^7.0.0",
+ "vscode-languageserver-textdocument": "^1.0.1",
+ "vscode-uri": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "peerDependencies": {
+ "eslint": ">=7",
+ "meow": "^9.0.0",
+ "optionator": "^0.9.1",
+ "stylelint": ">=13",
+ "typescript": "*",
+ "vite": ">=2.0.0",
+ "vls": "*",
+ "vti": "*",
+ "vue-tsc": ">=1.3.9"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ },
+ "meow": {
+ "optional": true
+ },
+ "optionator": {
+ "optional": true
+ },
+ "stylelint": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ },
+ "vls": {
+ "optional": true
+ },
+ "vti": {
+ "optional": true
+ },
+ "vue-tsc": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-checker/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/vite-plugin-checker/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/vite-plugin-checker/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/vite-plugin-checker/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/vite-plugin-checker/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/vite-plugin-checker/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/vscode-jsonrpc": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz",
+ "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.0.0 || >=10.0.0"
+ }
+ },
+ "node_modules/vscode-languageclient": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz",
+ "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==",
+ "dev": true,
+ "dependencies": {
+ "minimatch": "^3.0.4",
+ "semver": "^7.3.4",
+ "vscode-languageserver-protocol": "3.16.0"
+ },
+ "engines": {
+ "vscode": "^1.52.0"
+ }
+ },
+ "node_modules/vscode-languageserver": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz",
+ "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==",
+ "dev": true,
+ "dependencies": {
+ "vscode-languageserver-protocol": "3.16.0"
+ },
+ "bin": {
+ "installServerIntoExtension": "bin/installServerIntoExtension"
+ }
+ },
+ "node_modules/vscode-languageserver-protocol": {
+ "version": "3.16.0",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz",
+ "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==",
+ "dev": true,
+ "dependencies": {
+ "vscode-jsonrpc": "6.0.0",
+ "vscode-languageserver-types": "3.16.0"
+ }
+ },
+ "node_modules/vscode-languageserver-textdocument": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz",
+ "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==",
+ "dev": true
+ },
+ "node_modules/vscode-languageserver-types": {
+ "version": "3.16.0",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz",
+ "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==",
+ "dev": true
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
+ "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
+ "dev": true
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/games/nukes/package.json b/games/nukes/package.json
index 92ff6b68..c1e236a8 100644
--- a/games/nukes/package.json
+++ b/games/nukes/package.json
@@ -29,6 +29,7 @@
"react-router-dom": "^6.21.1",
"styled-components": "^6.1.6",
"typescript": "^5.2.2",
- "vite": "^5.0.0"
+ "vite": "^5.0.0",
+ "vite-plugin-checker": "^0.6.4"
}
}
diff --git a/games/nukes/src/controls-render/launch-highlight.tsx b/games/nukes/src/controls-render/launch-highlight.tsx
new file mode 100644
index 00000000..3cee6a6c
--- /dev/null
+++ b/games/nukes/src/controls-render/launch-highlight.tsx
@@ -0,0 +1,33 @@
+import styled from 'styled-components';
+import { EntityType } from '../world/world-state-types';
+import { useSelectedObject } from '../controls/selection';
+import { usePointer } from '../controls/pointer';
+
+export function LaunchHighlight() {
+ const selectedObject = useSelectedObject();
+ const pointer = usePointer();
+
+ if (selectedObject?.type !== EntityType.LAUNCH_SITE) {
+ return null;
+ }
+
+ return (
+
+ {pointer.x}, {pointer.y}
+
+ );
+}
+
+const HighlightContainer = styled.div`
+ position: absolute;
+ transform: translate(calc(var(--x) * 1px), calc(var(--y) * 1px));
+ pointer-events: none;
+ color: red;
+`;
diff --git a/games/nukes/src/controls/command.ts b/games/nukes/src/controls/command.ts
new file mode 100644
index 00000000..fb09ea64
--- /dev/null
+++ b/games/nukes/src/controls/command.ts
@@ -0,0 +1,46 @@
+import { useCustomEvent } from '../events';
+import { EntityType, Explosion, Missile, WorldState } from '../world/world-state-types';
+import { usePointer } from './pointer';
+import { useSelectedObject } from './selection';
+
+export function Command({
+ worldState,
+ setWorldState,
+}: {
+ worldState: WorldState;
+ setWorldState: (worldState: WorldState) => void;
+}) {
+ const selectedObject = useSelectedObject();
+ const pointer = usePointer();
+
+ useCustomEvent('world-click', () => {
+ if (selectedObject?.type !== EntityType.LAUNCH_SITE || pointer.pointingObjects.length === 0) {
+ return;
+ }
+
+ const missile: Missile = {
+ id: Math.random() + '',
+ launch: selectedObject.position,
+ launchTimestamp: worldState.timestamp,
+
+ target: pointer.pointingObjects[0].position,
+ targetTimestamp: worldState.timestamp + 10,
+ };
+
+ const explosion: Explosion = {
+ id: Math.random() + '',
+ startTimestamp: missile.targetTimestamp,
+ endTimestamp: missile.targetTimestamp + 5,
+ position: missile.target,
+ radius: 30,
+ };
+
+ setWorldState({
+ ...worldState,
+ missiles: [...worldState.missiles, missile],
+ explosions: [...worldState.explosions, explosion],
+ });
+ });
+
+ return null;
+}
diff --git a/games/nukes/src/controls/pointer.tsx b/games/nukes/src/controls/pointer.tsx
new file mode 100644
index 00000000..43642691
--- /dev/null
+++ b/games/nukes/src/controls/pointer.tsx
@@ -0,0 +1,75 @@
+import React, { createContext, useContext, useReducer } from 'react';
+import { City, LaunchSite } from '../world/world-state-types';
+
+type PointableObject = LaunchSite | City;
+
+type PointerDispatchAction =
+ | {
+ type: 'move';
+ x: number;
+ y: number;
+ }
+ | {
+ type: 'point' | 'unpoint';
+ object: PointableObject;
+ };
+
+type Pointer = {
+ x: number;
+ y: number;
+ pointingObjects: PointableObject[];
+};
+
+const initialPointer: Pointer = { x: 0, y: 0, pointingObjects: [] };
+
+const pointerReducer: React.Reducer = (
+ pointer: Pointer,
+ action: PointerDispatchAction,
+) => {
+ if (action.type === 'move') {
+ return { x: action.x, y: action.y, pointingObjects: pointer.pointingObjects };
+ } else if (action.type === 'point' && !pointer.pointingObjects.some((object) => object.id === action.object.id)) {
+ return { x: pointer.x, y: pointer.y, pointingObjects: [...pointer.pointingObjects, action.object] };
+ } else if (action.type === 'unpoint' && pointer.pointingObjects.some((object) => object.id === action.object.id)) {
+ return {
+ x: pointer.x,
+ y: pointer.y,
+ pointingObjects: pointer.pointingObjects.filter((object) => object.id === action.object.id),
+ };
+ } else {
+ return pointer;
+ }
+};
+
+const PointerContext = createContext(initialPointer);
+
+const PointerDispatchContext = createContext>(() => {});
+
+export function PointerContextWrapper({ children }: { children: React.ReactNode }) {
+ const [selection, reducer] = useReducer(pointerReducer, initialPointer);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePointer() {
+ const pointer = useContext(PointerContext);
+
+ return pointer;
+}
+
+export function usePointerMove() {
+ const dispatch = useContext(PointerDispatchContext);
+ return (x: number, y: number) => dispatch({ type: 'move', x, y });
+}
+
+export function useObjectPointer() {
+ const dispatch = useContext(PointerDispatchContext);
+ return [
+ (object: PointableObject) => dispatch({ type: 'point', object }),
+ (object: PointableObject) => dispatch({ type: 'unpoint', object }),
+ ];
+}
diff --git a/games/nukes/src/controls/selection.tsx b/games/nukes/src/controls/selection.tsx
index 4b945208..58a72a98 100644
--- a/games/nukes/src/controls/selection.tsx
+++ b/games/nukes/src/controls/selection.tsx
@@ -4,10 +4,10 @@ import { LaunchSite } from '../world/world-state-types';
type SelectionDispatchAction =
| {
- action: 'clear';
+ type: 'clear';
}
| {
- action: 'set';
+ type: 'set';
object: LaunchSite;
};
@@ -21,9 +21,9 @@ const selectionReducer: React.Reducer = (
selection: Selection,
action: SelectionDispatchAction,
) => {
- if (action.action === 'clear') {
+ if (action.type === 'clear') {
return initialSelection;
- } else if (action.action === 'set') {
+ } else if (action.type === 'set') {
return { ...selection, selectedObject: action.object };
} else {
return selection;
@@ -34,7 +34,7 @@ const selectionReducer: React.Reducer = (
const SelectionContext = createContext(initialSelection);
// definition of dispatch function for selection context
-const SelectionDispatchContext = createContext>(selectionReducer);
+const SelectionDispatchContext = createContext>(() => {});
export function SelectionContextWrapper({ children }: { children: React.ReactNode }) {
const [selection, reducer] = useReducer(selectionReducer, initialSelection);
@@ -50,11 +50,19 @@ export function useObjectSelection(object: LaunchSite) {
const dispatch = useContext(SelectionDispatchContext);
const selection = useContext(SelectionContext);
- return [selection.selectedObject?.id === object.id, () => dispatch({ action: 'set', object })] as const;
+ return [selection.selectedObject?.id === object.id, () => dispatch({ type: 'set', object })] as const;
}
+export function useSelectedObject() {
+ const selection = useContext(SelectionContext);
+
+ return selection.selectedObject;
+}
+
+// definition of clear selection function for selection context
+
export function useClearSelection() {
const dispatch = useContext(SelectionDispatchContext);
- return () => dispatch({ action: 'clear' });
+ return () => dispatch({ type: 'clear' });
}
diff --git a/games/nukes/src/events.ts b/games/nukes/src/events.ts
new file mode 100644
index 00000000..ec45560b
--- /dev/null
+++ b/games/nukes/src/events.ts
@@ -0,0 +1,21 @@
+import { useEffect } from 'react';
+
+export function dispatchCustomEvent(eventName: string, data?: T) {
+ const event = new CustomEvent(eventName, {
+ bubbles: true,
+ detail: data,
+ });
+ document.dispatchEvent(event);
+}
+
+export function useCustomEvent(eventName: string, callback: (data: T) => void) {
+ useEffect(() => {
+ const handler = (event: Event | CustomEvent) => {
+ callback((event as CustomEvent).detail as T);
+ };
+ document.addEventListener(eventName, handler, false);
+ return () => {
+ document.removeEventListener(eventName, handler, false);
+ };
+ }, [eventName, callback]);
+}
diff --git a/games/nukes/src/game-states/state-intro.tsx b/games/nukes/src/game-states/state-intro.tsx
index 90136b58..230f8767 100644
--- a/games/nukes/src/game-states/state-intro.tsx
+++ b/games/nukes/src/game-states/state-intro.tsx
@@ -5,9 +5,6 @@ const Intro: GameStateComponent = ({ setGameState }) => {
return (
<>
intro
-
-
-
>
diff --git a/games/nukes/src/game-states/state-tech-world.tsx b/games/nukes/src/game-states/state-tech-world.tsx
index bb54eb7b..f7ce8d14 100644
--- a/games/nukes/src/game-states/state-tech-world.tsx
+++ b/games/nukes/src/game-states/state-tech-world.tsx
@@ -5,11 +5,15 @@ import { createWorldState } from '../world/world-state-create';
import { updateWorldState } from '../world/world-state-updates';
import { WorldState } from '../world/world-state-types';
import { SelectionContextWrapper } from '../controls/selection';
-import { WorldStateRender } from '../render/world-state-render';
+import { WorldStateRender } from '../world-render/world-state-render';
+
+import { LaunchHighlight } from '../controls-render/launch-highlight';
import { GameState, GameStateComponent } from './types';
+import { PointerContextWrapper } from '../controls/pointer';
+import { Command } from '../controls/command';
-const WorldComponent: GameStateComponent = ({ setGameState }) => {
+const WorldComponent: GameStateComponent = ({}) => {
const [worldState, setWorldState] = useState(() => createWorldState());
const updateWorld = useCallback(
(worldState: WorldState, deltaTime: number) => setWorldState(updateWorldState(worldState, deltaTime)),
@@ -18,10 +22,15 @@ const WorldComponent: GameStateComponent = ({ setGameState }) => {
return (
-
-
-
-
+
+
+
+
+
+
+
+
+
);
};
diff --git a/games/nukes/src/render/city-render.tsx b/games/nukes/src/world-render/city-render.tsx
similarity index 74%
rename from games/nukes/src/render/city-render.tsx
rename to games/nukes/src/world-render/city-render.tsx
index 66fff944..ca9a6bfa 100644
--- a/games/nukes/src/render/city-render.tsx
+++ b/games/nukes/src/world-render/city-render.tsx
@@ -1,10 +1,15 @@
import styled from 'styled-components';
import { City } from '../world/world-state-types';
+import { useObjectPointer } from '../controls/pointer';
export function CityRender({ city }: { city: City }) {
+ const [point, unpoint] = useObjectPointer();
+
return (
point(city)}
+ onMouseLeave={() => unpoint(city)}
style={
{
'--x': city.position.x,
diff --git a/games/nukes/src/render/explosion-render.tsx b/games/nukes/src/world-render/explosion-render.tsx
similarity index 100%
rename from games/nukes/src/render/explosion-render.tsx
rename to games/nukes/src/world-render/explosion-render.tsx
diff --git a/games/nukes/src/render/launch-site-render.tsx b/games/nukes/src/world-render/launch-site-render.tsx
similarity index 81%
rename from games/nukes/src/render/launch-site-render.tsx
rename to games/nukes/src/world-render/launch-site-render.tsx
index c77f2d4c..10e596a4 100644
--- a/games/nukes/src/render/launch-site-render.tsx
+++ b/games/nukes/src/world-render/launch-site-render.tsx
@@ -2,12 +2,16 @@ import styled from 'styled-components';
import { LaunchSite } from '../world/world-state-types';
import { useObjectSelection } from '../controls/selection';
+import { useObjectPointer } from '../controls/pointer';
export function LaunchSiteRender({ launchSite }: { launchSite: LaunchSite }) {
const [isSelected, select] = useObjectSelection(launchSite);
+ const [point, unpoint] = useObjectPointer();
return (
point(launchSite)}
+ onMouseLeave={() => unpoint(launchSite)}
onClick={() => select()}
style={
{
diff --git a/games/nukes/src/render/missile-render.tsx b/games/nukes/src/world-render/missile-render.tsx
similarity index 100%
rename from games/nukes/src/render/missile-render.tsx
rename to games/nukes/src/world-render/missile-render.tsx
diff --git a/games/nukes/src/render/sector-render.tsx b/games/nukes/src/world-render/sector-render.tsx
similarity index 100%
rename from games/nukes/src/render/sector-render.tsx
rename to games/nukes/src/world-render/sector-render.tsx
diff --git a/games/nukes/src/render/state-render.tsx b/games/nukes/src/world-render/state-render.tsx
similarity index 73%
rename from games/nukes/src/render/state-render.tsx
rename to games/nukes/src/world-render/state-render.tsx
index 32fa5f24..d5e259f5 100644
--- a/games/nukes/src/render/state-render.tsx
+++ b/games/nukes/src/world-render/state-render.tsx
@@ -2,7 +2,7 @@ import styled from 'styled-components';
import { State } from '../world/world-state-types';
-export function StateRender({ state }: { state: State }) {
+export function StateRender(_props: { state: State }) {
return ;
}
diff --git a/games/nukes/src/render/world-state-render.tsx b/games/nukes/src/world-render/world-state-render.tsx
similarity index 81%
rename from games/nukes/src/render/world-state-render.tsx
rename to games/nukes/src/world-render/world-state-render.tsx
index b6440909..d7c434f5 100644
--- a/games/nukes/src/render/world-state-render.tsx
+++ b/games/nukes/src/world-render/world-state-render.tsx
@@ -1,6 +1,7 @@
import styled from 'styled-components';
import { WorldState } from '../world/world-state-types';
+import { usePointerMove } from '../controls/pointer';
import { SectorRender } from './sector-render';
import { StateRender } from './state-render';
@@ -8,11 +9,16 @@ import { CityRender } from './city-render';
import { LaunchSiteRender } from './launch-site-render';
import { MissileRender } from './missile-render';
import { ExplosionRender } from './explosion-render';
+import { dispatchCustomEvent } from '../events';
export function WorldStateRender({ state }: { state: WorldState }) {
- // wrap this into styled components globl css
+ const pointerMove = usePointerMove();
+
return (
-
+ pointerMove(event.clientX, event.clientY)}
+ onClick={() => dispatchCustomEvent('world-click')}
+ >
{state.sectors.map((sector) => (
))}
diff --git a/games/nukes/src/world/world-state-create.ts b/games/nukes/src/world/world-state-create.ts
index 18b29389..f4a13c96 100644
--- a/games/nukes/src/world/world-state-create.ts
+++ b/games/nukes/src/world/world-state-create.ts
@@ -1,5 +1,5 @@
import { distance } from '../math/position-utils';
-import { SectorType, WorldState } from './world-state-types';
+import { EntityType, SectorType, WorldState } from './world-state-types';
export function createWorldState(): WorldState {
const result: WorldState = {
@@ -17,14 +17,28 @@ export function createWorldState(): WorldState {
stateId: 'test-state',
position: { x: 100, y: 100 },
},
+ {
+ id: 'test-city2',
+ name: 'TestCity2',
+ stateId: 'test-state',
+ position: { x: 150, y: 100 },
+ },
+ {
+ id: 'test-city3',
+ name: 'TestCity3',
+ stateId: 'test-state',
+ position: { x: 150, y: 50 },
+ },
],
launchSites: [
{
+ type: EntityType.LAUNCH_SITE,
id: 'test-launch-site-1',
stateId: 'test-state',
position: { x: 100, y: 100 },
},
{
+ type: EntityType.LAUNCH_SITE,
id: 'test-launch-site-2',
stateId: 'test-state',
position: { x: 200, y: 200 },
@@ -58,7 +72,7 @@ function generateSectors(cols: number, rows: number, sectorSize: number) {
const centerColX = cols / 2;
const centerRowY = rows / 2;
- return Array.from({ length: cols * rows }).map((v, i) => {
+ return Array.from({ length: cols * rows }).map((_v, i) => {
const x = i % cols;
const y = Math.floor(i / rows);
diff --git a/games/nukes/src/world/world-state-types.ts b/games/nukes/src/world/world-state-types.ts
index f34d8f72..c70195de 100644
--- a/games/nukes/src/world/world-state-types.ts
+++ b/games/nukes/src/world/world-state-types.ts
@@ -26,6 +26,10 @@ export type City = {
position: Position;
};
+export enum EntityType {
+ LAUNCH_SITE = 'LAUNCH_SITE`',
+}
+
export enum SectorType {
WATER = 'WATER',
GROUND = 'GROUND',
@@ -38,6 +42,7 @@ export type Sector = {
};
export type LaunchSite = {
+ type: EntityType.LAUNCH_SITE;
id: LaunchSiteId;
position: Position;
stateId?: StateId;
diff --git a/games/nukes/src/world/world-state-updates.ts b/games/nukes/src/world/world-state-updates.ts
index e98ab5b4..313b14ec 100644
--- a/games/nukes/src/world/world-state-updates.ts
+++ b/games/nukes/src/world/world-state-updates.ts
@@ -1,4 +1,4 @@
-import { WorldState, Missile } from './world-state-types';
+import { WorldState } from './world-state-types';
export function updateWorldState(state: WorldState, deltaTime: number): WorldState {
const worldTimestamp = state.timestamp + deltaTime;
diff --git a/games/nukes/vite.config.ts b/games/nukes/vite.config.ts
index 5a33944a..928491b4 100644
--- a/games/nukes/vite.config.ts
+++ b/games/nukes/vite.config.ts
@@ -1,7 +1,8 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import checker from 'vite-plugin-checker';
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react()],
-})
+ plugins: [react(), checker({ typescript: true })],
+});