Compare commits
No commits in common. "53e68c227936f8448e4a4c1310141227cf2f0564" and "8ca8f894e49b756306da343ff5f3d657af1795bd" have entirely different histories.
53e68c2279
...
8ca8f894e4
|
|
@ -14,12 +14,10 @@
|
|||
"@react-three/fiber": "^9.6.1",
|
||||
"@react-three/rapier": "^2.2.0",
|
||||
"@types/three": "^0.184.1",
|
||||
"blockly": "^12.5.1",
|
||||
"install": "^0.13.0",
|
||||
"mobx": "^6.15.4",
|
||||
"mobx-react-lite": "^4.1.1",
|
||||
"mobx-utils": "^6.1.1",
|
||||
"nipplejs": "^1.0.4",
|
||||
"npm": "^11.16.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
|
|
|
|||
329
pnpm-lock.yaml
329
pnpm-lock.yaml
|
|
@ -20,9 +20,6 @@ importers:
|
|||
'@types/three':
|
||||
specifier: ^0.184.1
|
||||
version: 0.184.1
|
||||
blockly:
|
||||
specifier: ^12.5.1
|
||||
version: 12.5.1
|
||||
install:
|
||||
specifier: ^0.13.0
|
||||
version: 0.13.0
|
||||
|
|
@ -35,9 +32,6 @@ importers:
|
|||
mobx-utils:
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1(mobx@6.15.4)
|
||||
nipplejs:
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
npm:
|
||||
specifier: ^11.16.0
|
||||
version: 11.16.0
|
||||
|
|
@ -99,9 +93,6 @@ importers:
|
|||
|
||||
packages:
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
|
||||
|
||||
'@babel/code-frame@7.29.7':
|
||||
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -173,34 +164,6 @@ packages:
|
|||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@csstools/color-helpers@5.1.0':
|
||||
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@csstools/css-calc@2.1.4':
|
||||
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^3.0.5
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
|
||||
'@csstools/css-color-parser@3.1.0':
|
||||
resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^3.0.5
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
|
||||
'@csstools/css-parser-algorithms@3.0.5':
|
||||
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
|
||||
'@csstools/css-tokenizer@3.0.4':
|
||||
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@dimforge/rapier3d-compat@0.12.0':
|
||||
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
|
||||
|
||||
|
|
@ -673,10 +636,6 @@ packages:
|
|||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ajv@6.15.0:
|
||||
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
|
||||
|
||||
|
|
@ -695,10 +654,6 @@ packages:
|
|||
bidi-js@1.0.3:
|
||||
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||
|
||||
blockly@12.5.1:
|
||||
resolution: {integrity: sha512-etXLpUtEkcRibHGwIJ4BsvnIzMJJs0C0yPIjE/W0NCtj8ACha/a7Q9n7Ib6+j7N4EzQ0p28YPZMnypi5pNIj1g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
brace-expansion@5.0.6:
|
||||
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
|
@ -740,17 +695,9 @@ packages:
|
|||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
cssstyle@4.6.0:
|
||||
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
data-urls@5.0.0:
|
||||
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
|
@ -760,9 +707,6 @@ packages:
|
|||
supports-color:
|
||||
optional: true
|
||||
|
||||
decimal.js@10.6.0:
|
||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
|
|
@ -779,10 +723,6 @@ packages:
|
|||
electron-to-chromium@1.5.364:
|
||||
resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==}
|
||||
|
||||
entities@6.0.1:
|
||||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -912,22 +852,6 @@ packages:
|
|||
hls.js@1.6.16:
|
||||
resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==}
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
|
|
@ -961,9 +885,6 @@ packages:
|
|||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-potential-custom-element-name@1.0.1:
|
||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
|
||||
is-promise@2.2.2:
|
||||
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
|
||||
|
||||
|
|
@ -978,15 +899,6 @@ packages:
|
|||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
jsdom@26.1.0:
|
||||
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
canvas: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
|
||||
jsesc@3.1.0:
|
||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -1094,9 +1006,6 @@ packages:
|
|||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
|
|
@ -1150,9 +1059,6 @@ packages:
|
|||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
nipplejs@1.0.4:
|
||||
resolution: {integrity: sha512-YtvRZDuyWQ+tOj0nUjfnZt4ZQmJVWfK9+yPmV5OXpQ4k4AkOGcSfKvSXbPNkV0ERqc4eakkBSZ7/Wyx1y6h/Sg==}
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
|
|
@ -1231,9 +1137,6 @@ packages:
|
|||
- validate-npm-package-name
|
||||
- which
|
||||
|
||||
nwsapi@2.2.23:
|
||||
resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
|
@ -1246,9 +1149,6 @@ packages:
|
|||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -1330,21 +1230,11 @@ packages:
|
|||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
rrweb-cssom@0.8.0:
|
||||
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
sass@1.100.0:
|
||||
resolution: {integrity: sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
hasBin: true
|
||||
|
||||
saxes@6.0.0:
|
||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||
engines: {node: '>=v12.22.7'}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
|
|
@ -1386,9 +1276,6 @@ packages:
|
|||
peerDependencies:
|
||||
react: '>=17.0'
|
||||
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
|
||||
three-mesh-bvh@0.8.3:
|
||||
resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==}
|
||||
peerDependencies:
|
||||
|
|
@ -1406,21 +1293,6 @@ packages:
|
|||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tldts-core@6.1.86:
|
||||
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
|
||||
|
||||
tldts@6.1.86:
|
||||
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
|
||||
hasBin: true
|
||||
|
||||
tough-cookie@5.1.2:
|
||||
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tr46@5.1.1:
|
||||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
troika-three-text@0.52.4:
|
||||
resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==}
|
||||
peerDependencies:
|
||||
|
|
@ -1530,33 +1402,12 @@ packages:
|
|||
yaml:
|
||||
optional: true
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
webgl-constants@1.1.1:
|
||||
resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==}
|
||||
|
||||
webgl-sdf-generator@1.1.1:
|
||||
resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==}
|
||||
|
||||
webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||
engines: {node: '>=18'}
|
||||
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
||||
|
||||
whatwg-mimetype@4.0.0:
|
||||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-url@14.2.0:
|
||||
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
|
@ -1566,25 +1417,6 @@ packages:
|
|||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ws@8.21.0:
|
||||
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
xml-name-validator@5.0.0:
|
||||
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
xmlchars@2.2.0:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
|
|
@ -1636,14 +1468,6 @@ packages:
|
|||
|
||||
snapshots:
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
dependencies:
|
||||
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
lru-cache: 10.4.3
|
||||
|
||||
'@babel/code-frame@7.29.7':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
|
|
@ -1746,26 +1570,6 @@ snapshots:
|
|||
'@babel/helper-string-parser': 7.29.7
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
|
||||
'@csstools/color-helpers@5.1.0': {}
|
||||
|
||||
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||
dependencies:
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
|
||||
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||
dependencies:
|
||||
'@csstools/color-helpers': 5.1.0
|
||||
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
|
||||
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
|
||||
dependencies:
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
|
||||
'@csstools/css-tokenizer@3.0.4': {}
|
||||
|
||||
'@dimforge/rapier3d-compat@0.12.0': {}
|
||||
|
||||
'@dimforge/rapier3d-compat@0.19.2': {}
|
||||
|
|
@ -2200,8 +2004,6 @@ snapshots:
|
|||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ajv@6.15.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
|
|
@ -2219,15 +2021,6 @@ snapshots:
|
|||
dependencies:
|
||||
require-from-string: 2.0.2
|
||||
|
||||
blockly@12.5.1:
|
||||
dependencies:
|
||||
jsdom: 26.1.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- canvas
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
brace-expansion@5.0.6:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
|
|
@ -2269,24 +2062,12 @@ snapshots:
|
|||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
cssstyle@4.6.0:
|
||||
dependencies:
|
||||
'@asamuzakjp/css-color': 3.2.0
|
||||
rrweb-cssom: 0.8.0
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
data-urls@5.0.0:
|
||||
dependencies:
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.2.0
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decimal.js@10.6.0: {}
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
detect-gpu@5.0.70:
|
||||
|
|
@ -2299,8 +2080,6 @@ snapshots:
|
|||
|
||||
electron-to-chromium@1.5.364: {}
|
||||
|
||||
entities@6.0.1: {}
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
|
@ -2435,28 +2214,6 @@ snapshots:
|
|||
|
||||
hls.js@1.6.16: {}
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
dependencies:
|
||||
whatwg-encoding: 3.1.1
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
|
@ -2477,8 +2234,6 @@ snapshots:
|
|||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-potential-custom-element-name@1.0.1: {}
|
||||
|
||||
is-promise@2.2.2: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
|
@ -2492,33 +2247,6 @@ snapshots:
|
|||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
jsdom@26.1.0:
|
||||
dependencies:
|
||||
cssstyle: 4.6.0
|
||||
data-urls: 5.0.0
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 4.0.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.23
|
||||
parse5: 7.3.0
|
||||
rrweb-cssom: 0.8.0
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 5.1.2
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
whatwg-encoding: 3.1.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.2.0
|
||||
ws: 8.21.0
|
||||
xml-name-validator: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
|
@ -2595,8 +2323,6 @@ snapshots:
|
|||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
|
@ -2636,8 +2362,6 @@ snapshots:
|
|||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
nipplejs@1.0.4: {}
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
optional: true
|
||||
|
||||
|
|
@ -2645,8 +2369,6 @@ snapshots:
|
|||
|
||||
npm@11.16.0: {}
|
||||
|
||||
nwsapi@2.2.23: {}
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
|
|
@ -2664,10 +2386,6 @@ snapshots:
|
|||
dependencies:
|
||||
p-limit: 3.1.0
|
||||
|
||||
parse5@7.3.0:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
|
@ -2745,10 +2463,6 @@ snapshots:
|
|||
'@rolldown/binding-win32-arm64-msvc': 1.0.2
|
||||
'@rolldown/binding-win32-x64-msvc': 1.0.2
|
||||
|
||||
rrweb-cssom@0.8.0: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sass@1.100.0:
|
||||
dependencies:
|
||||
chokidar: 5.0.0
|
||||
|
|
@ -2757,10 +2471,6 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@parcel/watcher': 2.5.6
|
||||
|
||||
saxes@6.0.0:
|
||||
dependencies:
|
||||
xmlchars: 2.2.0
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
|
@ -2788,8 +2498,6 @@ snapshots:
|
|||
dependencies:
|
||||
react: 19.2.6
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
three-mesh-bvh@0.8.3(three@0.184.0):
|
||||
dependencies:
|
||||
three: 0.184.0
|
||||
|
|
@ -2811,20 +2519,6 @@ snapshots:
|
|||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
|
||||
tldts-core@6.1.86: {}
|
||||
|
||||
tldts@6.1.86:
|
||||
dependencies:
|
||||
tldts-core: 6.1.86
|
||||
|
||||
tough-cookie@5.1.2:
|
||||
dependencies:
|
||||
tldts: 6.1.86
|
||||
|
||||
tr46@5.1.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
troika-three-text@0.52.4(three@0.184.0):
|
||||
dependencies:
|
||||
bidi-js: 1.0.3
|
||||
|
|
@ -2903,39 +2597,16 @@ snapshots:
|
|||
fsevents: 2.3.3
|
||||
sass: 1.100.0
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
dependencies:
|
||||
xml-name-validator: 5.0.0
|
||||
|
||||
webgl-constants@1.1.1: {}
|
||||
|
||||
webgl-sdf-generator@1.1.1: {}
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
|
||||
whatwg-mimetype@4.0.0: {}
|
||||
|
||||
whatwg-url@14.2.0:
|
||||
dependencies:
|
||||
tr46: 5.1.1
|
||||
webidl-conversions: 7.0.0
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
ws@8.21.0: {}
|
||||
|
||||
xml-name-validator@5.0.0: {}
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
|
|
|||
35
src/App.tsx
35
src/App.tsx
|
|
@ -2,23 +2,20 @@ import { BrowserRouter, Link, Outlet, Route, Routes, useNavigate, useParams } fr
|
|||
import { useEffect } from 'react';
|
||||
import { reaction } from 'mobx';
|
||||
import { ThreeView } from './components/ThreeView';
|
||||
import { Panel } from './components/Panel';
|
||||
import { state } from './state';
|
||||
import './App.scss';
|
||||
import { LeftPanel } from './components/LeftPanel';
|
||||
import { Panels } from './components/Panels';
|
||||
|
||||
function StateToUrlSync() {
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => reaction(
|
||||
() => ({
|
||||
isGame: state.isGamePlaying,
|
||||
selectedObjectId: state.worldEditor.selection?.type === 'object' && state.worldEditor.selection.id,
|
||||
selectedObjectTypeId: state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection.id,
|
||||
selectedId: state.worldEditor.selectedObjectId,
|
||||
}),
|
||||
({ isGame, selectedObjectId, selectedObjectTypeId }) => {
|
||||
({ isGame, selectedId }) => {
|
||||
if (isGame) navigate('/game');
|
||||
else if (selectedObjectId) navigate(`/editor/object/${selectedObjectId}`);
|
||||
else if (selectedObjectTypeId) navigate(`/editor/objectType/${selectedObjectTypeId}`);
|
||||
else if (selectedId) navigate(`/editor/object/${selectedId}`);
|
||||
else navigate('/editor');
|
||||
},
|
||||
), []);
|
||||
|
|
@ -28,7 +25,7 @@ function StateToUrlSync() {
|
|||
function EditorRoute() {
|
||||
useEffect(() => {
|
||||
if (state.isGamePlaying) state.stopGame();
|
||||
state.worldEditor.resetSelection();
|
||||
state.worldEditor.resetSelectedObject();
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -38,23 +35,10 @@ function EditorObjectRoute() {
|
|||
useEffect(() => {
|
||||
if (!id) return;
|
||||
if (state.isGamePlaying) state.stopGame();
|
||||
const editMode = state.worldEditor.selection?.type === 'object' && state.worldEditor.selection.id === id
|
||||
? state.worldEditor.selection.editMode ?? 'translate'
|
||||
const mode = state.worldEditor.selectedObjectId === id
|
||||
? state.worldEditor.selectedObjectMode ?? 'translate'
|
||||
: 'translate';
|
||||
state.worldEditor.setSelectedObject({ id, editMode });
|
||||
}, [id]);
|
||||
return null;
|
||||
}
|
||||
|
||||
function EditorObjectTypeRoute() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
if (state.isGamePlaying) state.stopGame();
|
||||
const editMode = state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection.id === id
|
||||
? state.worldEditor.selection.editMode ?? 'scripts'
|
||||
: 'scripts';
|
||||
state.worldEditor.setSelectedObjectType({ id, editMode });
|
||||
state.worldEditor.setSelectedObject(id, mode);
|
||||
}, [id]);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -75,7 +59,7 @@ function EditorLayout() {
|
|||
<>
|
||||
<Outlet />
|
||||
<ThreeView />
|
||||
<Panels />
|
||||
<Panel side="left" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -89,7 +73,6 @@ export const App = function () {
|
|||
<Route element={<EditorLayout />}>
|
||||
<Route path="/editor" element={<EditorRoute />} />
|
||||
<Route path="/editor/object/:id" element={<EditorObjectRoute />} />
|
||||
<Route path="/editor/objectType/:id" element={<EditorObjectTypeRoute />} />
|
||||
<Route path="/game" element={<GameRoute />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
|
|
|||
|
|
@ -6,19 +6,13 @@ import { useEffect, useRef } from "react";
|
|||
import { Euler, Quaternion, Vector3 } from "three";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useKeyboardControls } from "@react-three/drei";
|
||||
import { useRapier, useBeforePhysicsStep, type RapierRigidBody, type RapierCollider, RoundCuboidCollider } from "@react-three/rapier";
|
||||
import { joystickValues } from "../joystickInput";
|
||||
import type { RapierRigidBody } from "@react-three/rapier";
|
||||
|
||||
const SPEED = 5;
|
||||
const JUMP_SPEED = 8;
|
||||
const GRAVITY = 20;
|
||||
const SENSITIVITY = 0.002;
|
||||
// Shoulder offset in character-local space: right, up, behind
|
||||
const SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5);
|
||||
const LOOK_RATE = 2000;
|
||||
|
||||
// recreate private types
|
||||
type RapierWorldCreateCharacterControllerFunction = ReturnType<typeof useRapier>['world']['createCharacterController'];
|
||||
type KinematicCharacterController = ReturnType<RapierWorldCreateCharacterControllerFunction>;
|
||||
|
||||
const _q = new Quaternion();
|
||||
const _e = new Euler(0, 0, 0, 'YXZ');
|
||||
|
|
@ -34,32 +28,19 @@ type CharacterViewProps = {
|
|||
export const CharacterView = observer(function ({ character, editMode }: CharacterViewProps) {
|
||||
const pos = character.transform.position;
|
||||
const rbRef = useRef<RapierRigidBody>(null);
|
||||
const colliderRef = useRef<RapierCollider>(null);
|
||||
const [, get] = useKeyboardControls();
|
||||
const { gl } = useThree();
|
||||
const { world } = useRapier();
|
||||
const groundContacts = useRef(0);
|
||||
|
||||
const yawRef = useRef(0);
|
||||
const pitchRef = useRef(0);
|
||||
const mouseRef = useRef({ x: 0, y: 0 });
|
||||
const jumpPressedRef = useRef(false);
|
||||
const vyRef = useRef(0);
|
||||
const controllerRef = useRef<KinematicCharacterController | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = world.createCharacterController(0.01);
|
||||
controller.setUp({ x: 0, y: 1, z: 0 });
|
||||
controller.setMaxSlopeClimbAngle(45 * Math.PI / 180);
|
||||
controller.setMinSlopeSlideAngle(30 * Math.PI / 180);
|
||||
controller.enableAutostep(0.5, 0.2, true);
|
||||
controller.enableSnapToGround(0.5);
|
||||
controller.setApplyImpulsesToDynamicBodies(true);
|
||||
controllerRef.current = controller;
|
||||
return () => { world.removeCharacterController(controller); };
|
||||
}, [world]);
|
||||
if (editMode)
|
||||
return;
|
||||
|
||||
useEffect(() => {
|
||||
if (editMode) return;
|
||||
const canvas = gl.domElement;
|
||||
const onClick = () => canvas.requestPointerLock();
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
|
|
@ -75,81 +56,72 @@ export const CharacterView = observer(function ({ character, editMode }: Charact
|
|||
};
|
||||
}, [gl, editMode]);
|
||||
|
||||
useBeforePhysicsStep((world) => {
|
||||
if (editMode) return;
|
||||
const rb = rbRef.current;
|
||||
const collider = colliderRef.current;
|
||||
const controller = controllerRef.current;
|
||||
if (!rb || !collider || !controller) return;
|
||||
if (state.game?.isPaused) return;
|
||||
useFrame(({ camera }) => {
|
||||
if (editMode)
|
||||
return;
|
||||
|
||||
const dt = world.timestep;
|
||||
const yaw = yawRef.current;
|
||||
const { forward, backward, left, right, jump } = get();
|
||||
const fwdX = -Math.sin(yaw);
|
||||
const fwdZ = -Math.cos(yaw);
|
||||
const fwdScale = Math.max(-1, Math.min(1, (forward ? 1 : 0) - (backward ? 1 : 0) + joystickValues.move.y));
|
||||
const rightScale = Math.max(-1, Math.min(1, (right ? 1 : 0) - (left ? 1 : 0) + joystickValues.move.x));
|
||||
const vx = (fwdX * fwdScale - fwdZ * rightScale) * SPEED;
|
||||
const vz = (fwdZ * fwdScale + fwdX * rightScale) * SPEED;
|
||||
|
||||
const jumpInput = jump || joystickValues.jump;
|
||||
const isGrounded = controller.computedGrounded();
|
||||
if (jumpInput && !jumpPressedRef.current && isGrounded) {
|
||||
vyRef.current = JUMP_SPEED;
|
||||
} else if (!isGrounded) {
|
||||
vyRef.current -= GRAVITY * dt;
|
||||
} else {
|
||||
vyRef.current = 0;
|
||||
}
|
||||
jumpPressedRef.current = jumpInput;
|
||||
|
||||
controller.computeColliderMovement(collider, { x: vx * dt, y: vyRef.current * dt, z: vz * dt });
|
||||
const corrected = controller.computedMovement();
|
||||
const t = rb.translation();
|
||||
rb.setNextKinematicTranslation({
|
||||
x: t.x + corrected.x,
|
||||
y: t.y + corrected.y,
|
||||
z: t.z + corrected.z,
|
||||
});
|
||||
|
||||
_q.setFromEuler(_e.set(0, yaw, 0));
|
||||
rb.setNextKinematicRotation({ x: _q.x, y: _q.y, z: _q.z, w: _q.w });
|
||||
});
|
||||
|
||||
useFrame(({ camera }, delta) => {
|
||||
if (editMode) return;
|
||||
const rb = rbRef.current;
|
||||
if (!rb) return;
|
||||
|
||||
mouseRef.current.x += joystickValues.look.x * LOOK_RATE * delta;
|
||||
mouseRef.current.y += -joystickValues.look.y * LOOK_RATE * delta;
|
||||
if (!rbRef.current)
|
||||
return;
|
||||
|
||||
yawRef.current -= mouseRef.current.x * SENSITIVITY;
|
||||
pitchRef.current = Math.max(-0.85, Math.min(0.5, pitchRef.current - mouseRef.current.y * SENSITIVITY));
|
||||
pitchRef.current = Math.max(-0.4, Math.min(0.6, pitchRef.current - mouseRef.current.y * SENSITIVITY));
|
||||
mouseRef.current.x = 0;
|
||||
mouseRef.current.y = 0;
|
||||
|
||||
const t = rb.translation();
|
||||
const yaw = yawRef.current;
|
||||
|
||||
// Rotate character body on Y axis only
|
||||
_q.setFromEuler(_e.set(0, yaw, 0));
|
||||
rbRef.current.setRotation({ x: _q.x, y: _q.y, z: _q.z, w: _q.w }, true);
|
||||
|
||||
// Pin camera to shoulder offset, rotated by yaw + pitch
|
||||
const t = rbRef.current.translation();
|
||||
_charPos.set(t.x, t.y, t.z);
|
||||
_offset.copy(SHOULDER_OFFSET).applyEuler(_e.set(pitchRef.current, yawRef.current, 0, 'YXZ'));
|
||||
_offset.copy(SHOULDER_OFFSET).applyEuler(_e.set(pitchRef.current, yaw, 0, 'YXZ'));
|
||||
camera.position.copy(_charPos).add(_offset);
|
||||
_lookAt.set(t.x, t.y + 1, t.z);
|
||||
camera.lookAt(_lookAt);
|
||||
|
||||
// Movement relative to character facing direction
|
||||
if (!state.game?.isPaused && !editMode) {
|
||||
const { forward, backward, left, right, jump } = get();
|
||||
const fwdX = -Math.sin(yaw);
|
||||
const fwdZ = -Math.cos(yaw);
|
||||
const fwdScale = (forward ? 1 : 0) - (backward ? 1 : 0);
|
||||
const rightScale = (right ? 1 : 0) - (left ? 1 : 0);
|
||||
// right vector = cross(fwd, up) = (-fwdZ, 0, fwdX)
|
||||
const vx = (fwdX * fwdScale - fwdZ * rightScale) * SPEED;
|
||||
const vz = (fwdZ * fwdScale + fwdX * rightScale) * SPEED;
|
||||
const cur = rbRef.current.linvel();
|
||||
|
||||
const isGrounded = groundContacts.current > 0;
|
||||
|
||||
const vy = cur.y +
|
||||
(jump && !jumpPressedRef.current && isGrounded
|
||||
? JUMP_SPEED
|
||||
: 0);
|
||||
jumpPressedRef.current = jump;
|
||||
|
||||
rbRef.current.setLinvel({ x: vx, y: vy, z: vz }, true);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<SyncRigidBody
|
||||
ref={rbRef}
|
||||
type="kinematicPosition"
|
||||
colliders={false}
|
||||
colliders="cuboid"
|
||||
lockRotations
|
||||
position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]}
|
||||
onSync={() => { }}
|
||||
onCollisionEnter={() => { groundContacts.current++; }}
|
||||
onCollisionExit={() => { groundContacts.current--; }}
|
||||
onSync={(data) => {
|
||||
// state.game?.setCharacterTransform(
|
||||
// { position: data.position, look: character.transform.look },
|
||||
// data.linearVelocity,
|
||||
// undefined,
|
||||
// );
|
||||
}}
|
||||
>
|
||||
{/* <BallCollider ref={colliderRef} args={[0.55]} /> */}
|
||||
{/* <CapsuleCollider ref={colliderRef} args={[0.4, 0.5]} /> */}
|
||||
{/* <CuboidCollider ref={colliderRef} args={[0.55, 0.4, 0.55]} /> */}
|
||||
<RoundCuboidCollider ref={colliderRef} args={[0.4, 0.4, 0.4, 0.1]} />
|
||||
<group>
|
||||
<mesh rotation={[-Math.PI / 2, -Math.PI / 4, 0]}>
|
||||
<coneGeometry args={[0.55, 0.8, 4]} />
|
||||
|
|
|
|||
|
|
@ -1,143 +0,0 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { create } from "nipplejs";
|
||||
import { joystickValues } from "../joystickInput";
|
||||
|
||||
// const isTouch = navigator.maxTouchPoints > 0;
|
||||
const isTouch = true; // debug
|
||||
|
||||
export function JoystickView() {
|
||||
const moveZoneRef = useRef<HTMLDivElement>(null);
|
||||
const jumpBtnRef = useRef<HTMLDivElement>(null);
|
||||
const lookZoneRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (!isTouch)
|
||||
return;
|
||||
|
||||
const moveZone = moveZoneRef.current;
|
||||
const jumpBtn = jumpBtnRef.current;
|
||||
const lookZone = lookZoneRef.current;
|
||||
|
||||
if (!moveZone || !jumpBtn || !lookZone)
|
||||
return;
|
||||
|
||||
let moveJoystick: ReturnType<typeof create>;
|
||||
let lookJoystick: ReturnType<typeof create>;
|
||||
|
||||
const setup = () => {
|
||||
moveJoystick = create({
|
||||
zone: moveZone,
|
||||
mode: 'static',
|
||||
position: { left: '80px', bottom: '80px' },
|
||||
color: 'white',
|
||||
shape: 'circle',
|
||||
size: 100,
|
||||
});
|
||||
|
||||
lookJoystick = create({
|
||||
zone: lookZone,
|
||||
mode: 'dynamic',
|
||||
dataOnly: true,
|
||||
});
|
||||
|
||||
moveJoystick.on('move', (evt) => {
|
||||
joystickValues.move.x = evt.data.vector.x;
|
||||
joystickValues.move.y = evt.data.vector.y;
|
||||
});
|
||||
moveJoystick.on('end', () => {
|
||||
joystickValues.move = { x: 0, y: 0 };
|
||||
});
|
||||
|
||||
lookJoystick.on('move', (evt) => {
|
||||
joystickValues.look = evt.data.vector;
|
||||
});
|
||||
lookJoystick.on('end', () => {
|
||||
joystickValues.look = { x: 0, y: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const teardown = () => {
|
||||
moveJoystick?.destroy();
|
||||
lookJoystick?.destroy();
|
||||
joystickValues.move = { x: 0, y: 0 };
|
||||
joystickValues.look = { x: 0, y: 0 };
|
||||
joystickValues.jump = false;
|
||||
};
|
||||
|
||||
setup();
|
||||
|
||||
const onJumpDown = () => { joystickValues.jump = true; };
|
||||
const onJumpUp = () => { joystickValues.jump = false; };
|
||||
|
||||
jumpBtn.addEventListener('pointerdown', onJumpDown);
|
||||
jumpBtn.addEventListener('pointerup', onJumpUp);
|
||||
jumpBtn.addEventListener('pointercancel', onJumpUp);
|
||||
jumpBtn.addEventListener('pointerleave', onJumpUp);
|
||||
|
||||
// When the OS interrupts a touch (app switch, notification, etc.) the browser
|
||||
// fires pointercancel instead of pointerup, leaving nipplejs with a stuck active
|
||||
// identifier. In dynamic mode this silently drops all subsequent touches on the
|
||||
// look zone. Destroying and recreating resets nipplejs's internal touch tracking.
|
||||
const onVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
teardown();
|
||||
} else {
|
||||
setup();
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
|
||||
return () => {
|
||||
teardown();
|
||||
jumpBtn.removeEventListener('pointerdown', onJumpDown);
|
||||
jumpBtn.removeEventListener('pointerup', onJumpUp);
|
||||
jumpBtn.removeEventListener('pointercancel', onJumpUp);
|
||||
jumpBtn.removeEventListener('pointerleave', onJumpUp);
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isTouch)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={moveZoneRef}
|
||||
id="move-zone"
|
||||
style={{ position: 'absolute', left: 0, bottom: 0, width: '30vw', height: '30vw', touchAction: 'none' }}
|
||||
/>
|
||||
<div
|
||||
ref={lookZoneRef}
|
||||
id="look-zone"
|
||||
style={{ position: 'absolute', right: 0, bottom: 0, width: '100%', height: '100%', touchAction: 'none' }}
|
||||
/>
|
||||
<div
|
||||
ref={jumpBtnRef}
|
||||
id="jump-zone"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 30,
|
||||
bottom: 30,
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.25)',
|
||||
border: '2px solid rgba(255,255,255,0.6)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
touchAction: 'none',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
>
|
||||
JUMP
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { RenderInfoView } from "./RenderInfoView";
|
||||
import { MenuView } from "./MenuView";
|
||||
import { state } from "../state";
|
||||
import { Panel } from "./Panel";
|
||||
|
||||
export const LeftPanel = observer(function () {
|
||||
const isGame = state.game && !state.game.isPaused;
|
||||
|
||||
if (isGame)
|
||||
return null;
|
||||
|
||||
// function handleLoadWorld(): void {
|
||||
// state.world.load();
|
||||
// }
|
||||
|
||||
function handleLoadMockWorld(): void {
|
||||
state.world.loadMock();
|
||||
}
|
||||
|
||||
return <Panel side='left'>
|
||||
<MenuView />
|
||||
<div className="gap" />
|
||||
{/* <button onClick={handleLoadWorld}>Load</button> */}
|
||||
<button onClick={handleLoadMockWorld}>Load mock world</button>
|
||||
<div className="debug"><RenderInfoView /></div>
|
||||
</Panel>
|
||||
});
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { state } from "../state";
|
||||
import { Panel } from "./Panel";
|
||||
import { ScriptEditorView } from "./scriptEditor/ScriptEditorView";
|
||||
|
||||
export const MainPanel = observer(function () {
|
||||
const isGame = state.game && !state.game.isPaused;
|
||||
if (isGame)
|
||||
return null;
|
||||
|
||||
const selectedObjectTypeId = state.worldEditor.selection?.type === 'objectType'
|
||||
? state.worldEditor.selection.id
|
||||
: undefined;
|
||||
if (!selectedObjectTypeId)
|
||||
return null;
|
||||
|
||||
const objectType = state.world.getObjectTypeById(selectedObjectTypeId);
|
||||
if (!objectType)
|
||||
return null;
|
||||
|
||||
const objects = Object.values(state.worldEditor.scene.objects)
|
||||
.filter((o) => o.typeId === objectType.id);
|
||||
|
||||
return <Panel side='main'>
|
||||
<header>
|
||||
<div><span className="title">{objectType?.name}</span> ({objects.length} object{objects.length > 1 ? 's' : ''})</div>
|
||||
</header>
|
||||
<ScriptEditorView objectType={objectType} />
|
||||
</Panel>
|
||||
});
|
||||
|
|
@ -1,37 +1,40 @@
|
|||
.menu {
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
& details {
|
||||
&>summary {
|
||||
list-style: none;
|
||||
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
display: block;
|
||||
padding: 8px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
& details {
|
||||
padding-left: 12px;
|
||||
}
|
||||
.menu-node {
|
||||
> summary {
|
||||
list-style: none;
|
||||
&::-webkit-details-marker { display: none; }
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: block;
|
||||
padding: 8px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-node .menu-node > summary {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.menu-node .menu-node .menu-leaf {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,33 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { state } from '../state';
|
||||
import './MenuView.scss';
|
||||
import type { MenuNode } from '../state/menuState';
|
||||
|
||||
export const MenuNodeView = observer(function ({ node }: { node: MenuNode }) {
|
||||
const forceOpen = state.menu.nodeContainsSelected(node);
|
||||
|
||||
const ref = useRef<HTMLDetailsElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (forceOpen && ref.current)
|
||||
ref.current.open = true;
|
||||
}, [forceOpen]);
|
||||
|
||||
return (
|
||||
<details ref={ref}>
|
||||
<summary
|
||||
className={node.selected?.() ? 'selected' : ''}
|
||||
onClick={() => node.onClick?.()}
|
||||
>
|
||||
{node.title}
|
||||
</summary>
|
||||
{node.children?.map((child) => <MenuNodeView key={child.id} node={child} />)}
|
||||
</details>
|
||||
);
|
||||
});
|
||||
|
||||
export const MenuView = observer(function () {
|
||||
const objectIds = Object.keys(state.worldEditor.scene.objects);
|
||||
const selectedId = state.worldEditor.selectedObjectId;
|
||||
|
||||
return (
|
||||
<nav className="menu">
|
||||
{state.menu.nodes.map((node) => <MenuNodeView key={node.id} node={node} />)}
|
||||
<details open className="menu-node">
|
||||
<summary className="menu-item">Editor</summary>
|
||||
<details open className="menu-node">
|
||||
<summary
|
||||
className={`menu-item${selectedId == null ? ' active' : ''}`}
|
||||
onClick={() => state.worldEditor.resetSelectedObject()}
|
||||
>
|
||||
Objects
|
||||
</summary>
|
||||
{objectIds.map(id => (
|
||||
<div
|
||||
key={id}
|
||||
className={`menu-item menu-leaf${selectedId === id ? ' active' : ''}`}
|
||||
onClick={() => state.worldEditor.setSelectedObject(id, 'translate')}
|
||||
>
|
||||
{id}
|
||||
</div>
|
||||
))}
|
||||
</details>
|
||||
</details>
|
||||
</nav>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { TransformControls, useHelper } from "@react-three/drei";
|
|||
import { BoxHelper } from "three";
|
||||
import type { ThreeEvent } from "@react-three/fiber";
|
||||
import { state } from "../state";
|
||||
import { nextObjectEditMode } from "../state/worldEditorState";
|
||||
import { nextSelectionEditMode } from "../state/worldEditorState";
|
||||
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
|
||||
|
||||
type ObjectEditorViewProps = {
|
||||
|
|
@ -23,12 +23,8 @@ type SelectionOverlayProps = {
|
|||
// re-render on selection change, not all N objects in the scene.
|
||||
const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: SelectionOverlayProps) {
|
||||
const isSelected = state.worldEditor.isEnabled &&
|
||||
state.worldEditor.selection?.type === 'object' &&
|
||||
state.worldEditor.selection.id === objectId;
|
||||
const selectionMode = isSelected &&
|
||||
state.worldEditor.selection?.type === 'object'
|
||||
? state.worldEditor.selection?.editMode
|
||||
: undefined;
|
||||
state.worldEditor.selectedObjectId === objectId;
|
||||
const selectionMode = isSelected ? state.worldEditor.selectedObjectMode : undefined;
|
||||
|
||||
// Stable virtual ref that reads through to the group inside the handle
|
||||
const groupRef = useMemo<RefObject<Group | null>>(
|
||||
|
|
@ -64,10 +60,10 @@ export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewP
|
|||
e.stopPropagation();
|
||||
// Reading selection state inside an event handler: not tracked by observer.
|
||||
const currentMode = state.worldEditor.isEnabled &&
|
||||
state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === object.id
|
||||
? state.worldEditor.selection?.editMode
|
||||
state.worldEditor.selectedObjectId === object.id
|
||||
? state.worldEditor.selectedObjectMode
|
||||
: undefined;
|
||||
state.worldEditor.setSelectedObject({ id: object.id, editMode: nextObjectEditMode(currentMode) });
|
||||
state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(currentMode));
|
||||
}
|
||||
|
||||
function handleTransformEnd() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
.panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 30%;
|
||||
height: 100vh;
|
||||
padding: 10px;
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
&.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&>.container {
|
||||
flex: 1;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(4px);
|
||||
padding: 4px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
|
||||
&>*:not(.debug):not(.gap) {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&>.gap {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&>.debug {
|
||||
border: 1px solid #ffffff20;
|
||||
padding: 2px;
|
||||
font-size: 75%;
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
|
||||
&>.chart {
|
||||
display: inline;
|
||||
|
||||
& div {
|
||||
display: inline;
|
||||
position: relative !important;
|
||||
z-index: unset !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&>.metrics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25em 0.5em;
|
||||
|
||||
&>* {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,35 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import type { ReactNode } from "react";
|
||||
import './Panel.scss';
|
||||
import { RenderInfoView } from "./RenderInfoView";
|
||||
import { MenuView } from "./MenuView";
|
||||
import { state } from "../state";
|
||||
|
||||
export type PanelProps = {
|
||||
side?: 'left' | 'right' | 'main';
|
||||
children?: ReactNode;
|
||||
side?: 'left' | 'right';
|
||||
}
|
||||
|
||||
export const Panel = observer(function ({ children, side = 'left' }: PanelProps) {
|
||||
export const Panel = observer(function ({ side = 'left' }: PanelProps) {
|
||||
|
||||
function handleCloneTest1Object(): void {
|
||||
state.worldEditor.addObjectCloneAtRandomPosition('test1');
|
||||
}
|
||||
|
||||
function handleLoadWorld(): void {
|
||||
state.world.load();
|
||||
}
|
||||
|
||||
function handleLoadMockWorld(): void {
|
||||
state.world.loadMock();
|
||||
}
|
||||
|
||||
return <div className={`panel ${side}`}>
|
||||
{children}
|
||||
<div className="container">
|
||||
<MenuView />
|
||||
<button onClick={handleLoadWorld}>Load</button>
|
||||
<button onClick={handleLoadMockWorld}>Load mock world</button>
|
||||
<button onClick={handleCloneTest1Object}>Clone test1</button>
|
||||
<div className="gap" />
|
||||
<div className="debug"><RenderInfoView /></div>
|
||||
</div>
|
||||
</div >
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
.overlay-panels {
|
||||
z-index: 15000;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
&>.panel {
|
||||
box-sizing: border-box;
|
||||
margin: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(4px);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: none !important;
|
||||
}
|
||||
|
||||
&.left,
|
||||
&.right {
|
||||
min-width: 30%;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
&.main {
|
||||
flex: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&>header {
|
||||
background: #00000040;
|
||||
margin: -4px;
|
||||
padding: 12px;
|
||||
|
||||
&>.title {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
&>*:not(.debug):not(.header):not(.gap) {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&>.gap {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&>.debug {
|
||||
// border: 1px solid #ffffff20;
|
||||
border-radius: 2px;
|
||||
background: #00000040;
|
||||
|
||||
margin: -4px;
|
||||
padding: 4px;
|
||||
font-size: 75%;
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
align-items: start;
|
||||
|
||||
&>.chart {
|
||||
display: inline;
|
||||
|
||||
& div {
|
||||
display: inline;
|
||||
position: relative !important;
|
||||
z-index: unset !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&>.metrics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25em 0.5em;
|
||||
|
||||
&>* {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { LeftPanel } from "./LeftPanel";
|
||||
import { MainPanel } from "./MainPanel";
|
||||
import './Panels.scss';
|
||||
|
||||
export const Panels = observer(function () {
|
||||
|
||||
return <div className="overlay-panels">
|
||||
<LeftPanel />
|
||||
<MainPanel />
|
||||
</div>
|
||||
});
|
||||
|
|
@ -24,7 +24,6 @@ import { observer } from 'mobx-react-lite';
|
|||
import { state } from '../state';
|
||||
import { GameView } from './GameView';
|
||||
import { SceneEditorView } from './SceneEditorView';
|
||||
import { JoystickView } from './JoystickView';
|
||||
import type { RefObject } from 'react';
|
||||
|
||||
const IconStop = () => <svg viewBox="0 0 14 14"><rect x="2" y="2" width="10" height="10" fill="currentColor" /></svg>;
|
||||
|
|
@ -44,13 +43,12 @@ export const ThreeView = observer(function () {
|
|||
<div style={{ position: 'fixed', inset: 0, overflow: 'hidden' }}>
|
||||
<Canvas
|
||||
// camera={state.world.character.camera}
|
||||
onPointerMissed={() => state.worldEditor.resetSelection()}
|
||||
onPointerMissed={() => state.worldEditor.resetSelectedObject()}
|
||||
>
|
||||
<Stats parent={chartRef as RefObject<HTMLElement>} />
|
||||
<RenderInfoUpdater />
|
||||
{isGame ? <GameView /> : <SceneEditorView />}
|
||||
</Canvas>
|
||||
{isGame && <JoystickView />}
|
||||
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4 }}>
|
||||
{
|
||||
state.game
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
.script-editor {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.blockly-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { runInAction } from "mobx";
|
||||
import * as Blockly from "blockly";
|
||||
import type { ObjectType } from "../../types";
|
||||
import './ScriptEditorView.scss';
|
||||
|
||||
const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
|
||||
kind: 'flyoutToolbox',
|
||||
contents: [
|
||||
{ kind: 'block', type: 'controls_if' },
|
||||
{ kind: 'block', type: 'controls_repeat_ext' },
|
||||
{ kind: 'block', type: 'logic_compare' },
|
||||
{ kind: 'block', type: 'math_number' },
|
||||
{ kind: 'block', type: 'math_arithmetic' },
|
||||
{ kind: 'block', type: 'text' },
|
||||
{ kind: 'block', type: 'text_print' },
|
||||
],
|
||||
};
|
||||
|
||||
type ScriptEditorViewProps = {
|
||||
objectType: ObjectType;
|
||||
}
|
||||
|
||||
export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorViewProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
|
||||
const isLoadingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const workspace = Blockly.inject(containerRef.current, { toolbox: TOOLBOX });
|
||||
workspaceRef.current = workspace;
|
||||
|
||||
if (objectType.program) {
|
||||
try {
|
||||
isLoadingRef.current = true;
|
||||
Blockly.serialization.workspaces.load(JSON.parse(objectType.program), workspace);
|
||||
} catch {
|
||||
// corrupt program, start fresh
|
||||
} finally {
|
||||
isLoadingRef.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
const listener = (e: Blockly.Events.Abstract) => {
|
||||
if (isLoadingRef.current || e.isUiEvent) return;
|
||||
const serialized = JSON.stringify(Blockly.serialization.workspaces.save(workspace));
|
||||
runInAction(() => { objectType.program = serialized; });
|
||||
};
|
||||
workspace.addChangeListener(listener);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => Blockly.svgResize(workspace));
|
||||
resizeObserver.observe(containerRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
workspace.removeChangeListener(listener);
|
||||
workspace.dispose();
|
||||
workspaceRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const workspace = workspaceRef.current;
|
||||
if (!workspace) return;
|
||||
|
||||
isLoadingRef.current = true;
|
||||
workspace.clear();
|
||||
if (objectType.program) {
|
||||
try {
|
||||
Blockly.serialization.workspaces.load(JSON.parse(objectType.program), workspace);
|
||||
} catch {
|
||||
// corrupt program, start fresh
|
||||
}
|
||||
}
|
||||
isLoadingRef.current = false;
|
||||
}, [objectType.id]);
|
||||
|
||||
return <div className="script-editor">
|
||||
<div ref={containerRef} className="blockly-container" />
|
||||
</div>;
|
||||
});
|
||||
|
|
@ -64,13 +64,13 @@ body {
|
|||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
// *:not(.panel.main) svg {
|
||||
// fill: currentColor;
|
||||
// display: inline-block;
|
||||
// height: 1em;
|
||||
// top: 0.125em;
|
||||
// vertical-align: bottom;
|
||||
// }
|
||||
svg {
|
||||
fill: currentColor;
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
top: 0.125em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
|
|
@ -78,7 +78,3 @@ select,
|
|||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export const DEFAULT_JOYSTICK_VALUES = {
|
||||
move: { x: 0, y: 0 },
|
||||
jump: false,
|
||||
look: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
export const joystickValues = DEFAULT_JOYSTICK_VALUES;
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
import { state } from "./rootState";
|
||||
|
||||
export type MenuNode = {
|
||||
id: string;
|
||||
title: string;
|
||||
onClick?: () => void;
|
||||
selected?: () => boolean;
|
||||
children?: MenuNode[];
|
||||
}
|
||||
|
||||
export class MenuState {
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
private get editorObjectTypesMenu(): MenuNode[] {
|
||||
return Object.values(state.world.data.objectTypes)
|
||||
.map((ot) => ({
|
||||
id: `ot-${ot.id}`,
|
||||
title: ot.name,
|
||||
onClick: () => { state.worldEditor.setSelectedObjectType({ id: ot.id, editMode: 'scripts' }) },
|
||||
selected: () => state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection?.id === ot.id,
|
||||
children: Object.values(state.worldEditor.scene.objects)
|
||||
.filter((o) => o.typeId === ot.id)
|
||||
.map((o) => ({
|
||||
id: `o-${o.id}`,
|
||||
title: o.id,
|
||||
onClick: () => { state.worldEditor.setSelectedObject({ id: o.id, editMode: 'translate' }) },
|
||||
selected: () => state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === o.id,
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
private get editorMenu(): MenuNode[] {
|
||||
return [
|
||||
{
|
||||
id: 'editor-objects-menu',
|
||||
title: 'Objects',
|
||||
onClick: () => { state.worldEditor.resetSelection() },
|
||||
selected: () => !state.worldEditor.selection,
|
||||
children: this.editorObjectTypesMenu,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
public get nodes(): MenuNode[] {
|
||||
return [
|
||||
...this.editorMenu,
|
||||
]
|
||||
}
|
||||
|
||||
public nodeContainsSelected(node: MenuNode): boolean {
|
||||
return !!(node.selected?.() || node.children?.some((child) => this.nodeContainsSelected(child)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { makeAutoObservable } from "mobx";
|
|||
import { WorldState } from "./worldState";
|
||||
import { WorldEditorState } from "./worldEditorState";
|
||||
import { GameState } from "./gameState";
|
||||
import { MenuState } from "./menuState";
|
||||
|
||||
export type RenderInfo = {
|
||||
calls: number,
|
||||
|
|
@ -17,7 +16,6 @@ export class RootState {
|
|||
public readonly worldEditor: WorldEditorState;
|
||||
public game: GameState | undefined;
|
||||
public renderInfo: RenderInfo | undefined;
|
||||
public readonly menu = new MenuState();
|
||||
|
||||
constructor() {
|
||||
this.worldEditor = new WorldEditorState(this.world);
|
||||
|
|
@ -26,7 +24,6 @@ export class RootState {
|
|||
this,
|
||||
{
|
||||
world: false,
|
||||
menu: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,54 +6,27 @@ import { randomId } from "../utils";
|
|||
import { state } from "./rootState";
|
||||
import { populateRuntimeObject } from "../utils/runtime";
|
||||
|
||||
export const ObjectEditModeEnum = [
|
||||
export const SelectionEditModeEnum = [
|
||||
'translate',
|
||||
'rotate',
|
||||
'scale',
|
||||
] as const;
|
||||
type ObjectEditModeTuple = typeof ObjectEditModeEnum;
|
||||
export type ObjectEditMode = ObjectEditModeTuple[number];
|
||||
type SelectionEditModeTuple = typeof SelectionEditModeEnum;
|
||||
export type SelectionEditMode = SelectionEditModeTuple[number];
|
||||
|
||||
export function nextObjectEditMode(mode: ObjectEditMode | undefined): ObjectEditMode {
|
||||
export function nextSelectionEditMode(mode: SelectionEditMode | undefined): SelectionEditMode {
|
||||
if (mode === undefined)
|
||||
return ObjectEditModeEnum[0];
|
||||
return 'translate';
|
||||
|
||||
const idx = ObjectEditModeEnum.indexOf(mode);
|
||||
return ObjectEditModeEnum[(idx + 1) % ObjectEditModeEnum.length];
|
||||
const idx = SelectionEditModeEnum.indexOf(mode);
|
||||
return SelectionEditModeEnum[(idx + 1) % SelectionEditModeEnum.length];
|
||||
}
|
||||
|
||||
export const ObjectTypeEditModeEnum = [
|
||||
'scripts',
|
||||
] as const;
|
||||
type ObjectTypeEditModeTuple = typeof ObjectTypeEditModeEnum;
|
||||
export type ObjectTypeEditMode = ObjectTypeEditModeTuple[number];
|
||||
|
||||
export function nextObjectTypeEditMode(mode: ObjectTypeEditMode | undefined): ObjectTypeEditMode {
|
||||
if (mode === undefined)
|
||||
return ObjectTypeEditModeEnum[0];
|
||||
|
||||
const idx = ObjectTypeEditModeEnum.indexOf(mode);
|
||||
return ObjectTypeEditModeEnum[(idx + 1) % ObjectTypeEditModeEnum.length];
|
||||
}
|
||||
|
||||
export type SelectedObject = {
|
||||
type: 'object',
|
||||
id: string;
|
||||
editMode: ObjectEditMode;
|
||||
}
|
||||
|
||||
export type SelectedObjectType = {
|
||||
type: 'objectType',
|
||||
id: string;
|
||||
editMode: ObjectTypeEditMode;
|
||||
}
|
||||
|
||||
export type Selection = SelectedObject | SelectedObjectType;
|
||||
|
||||
export class WorldEditorState {
|
||||
private readonly world: WorldState;
|
||||
|
||||
public selection: Selection | undefined;
|
||||
public selectedObjectId: string | undefined;
|
||||
public selectedObjectMode: SelectionEditMode | undefined;
|
||||
|
||||
constructor(world: WorldState) {
|
||||
this.world = world;
|
||||
|
|
@ -68,33 +41,14 @@ export class WorldEditorState {
|
|||
this.world.data.editorCamera = value;
|
||||
}
|
||||
|
||||
public setSelection(value: Selection | undefined): void {
|
||||
this.selection = value;
|
||||
console.log(JSON.stringify(this.selection));
|
||||
public setSelectedObject(id: string, mode: SelectionEditMode): void {
|
||||
this.selectedObjectId = id;
|
||||
this.selectedObjectMode = mode;
|
||||
}
|
||||
|
||||
public setSelectedObject(value: Omit<SelectedObject, 'type'> | undefined): void {
|
||||
this.setSelection(value
|
||||
? {
|
||||
type: 'object',
|
||||
...value,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
public setSelectedObjectType(value: Omit<SelectedObjectType, 'type'> | undefined): void {
|
||||
this.setSelection(value
|
||||
? {
|
||||
type: 'objectType',
|
||||
...value,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
public resetSelection() {
|
||||
this.setSelection(undefined);
|
||||
public resetSelectedObject(): void {
|
||||
this.selectedObjectId = undefined;
|
||||
this.selectedObjectMode = undefined;
|
||||
}
|
||||
|
||||
public get scene(): RuntimeScene {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export class WorldState {
|
|||
this.withoutAutoSave(() => {
|
||||
this.data = WorldFactory.create();
|
||||
});
|
||||
state.worldEditor.resetSelection();
|
||||
state.worldEditor.resetSelectedObject();
|
||||
}
|
||||
|
||||
public loadMock() {
|
||||
|
|
@ -97,7 +97,7 @@ export class WorldState {
|
|||
});
|
||||
|
||||
console.log(objects);
|
||||
state.worldEditor.resetSelection();
|
||||
state.worldEditor.resetSelectedObject();
|
||||
}
|
||||
|
||||
public load() {
|
||||
|
|
@ -105,7 +105,7 @@ export class WorldState {
|
|||
this.withoutAutoSave(() => {
|
||||
this.data = WorldFactory.load() ?? WorldFactory.create();
|
||||
});
|
||||
state.worldEditor.resetSelection();
|
||||
state.worldEditor.resetSelectedObject();
|
||||
}
|
||||
|
||||
private saveData(data: World): void {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ export type ObjectType = {
|
|||
id: string;
|
||||
name: string;
|
||||
voxels: Voxel[];
|
||||
program?: string;
|
||||
}
|
||||
|
||||
export type ObjectInstance = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue