Compare commits
21 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
253e094c74 | ||
|
|
fef94f03fb | ||
|
|
e9f91f8ecd | ||
|
|
634df8d03d |
||
|
|
a88c8b5470 | ||
|
|
f51254d97a | ||
|
|
05cd560d6b | ||
|
|
b239636356 | ||
|
|
4f421ae45f | ||
|
|
3b94889bed |
||
|
|
636a7fdb54 |
||
|
|
c930365e32 | ||
|
|
852dd737ae | ||
|
|
06dc3cb033 | ||
|
|
c4097975bf | ||
|
|
1525fac2a1 | ||
|
|
f24cb49a87 | ||
|
|
0b1183f541 | ||
|
|
739a6fad24 | ||
|
|
7f7a14ac65 | ||
|
|
265d02d18d |
34 changed files with 1023 additions and 113 deletions
|
|
@ -78,6 +78,8 @@ There is a builtin proxy, but you can also host your one! Just clone the repo, r
|
|||
|
||||
[](https://app.koyeb.com/deploy?name=minecraft-web-client&type=git&repository=zardoy%2Fminecraft-web-client&branch=next&builder=dockerfile&env%5B%5D=&ports=8080%3Bhttp%3B%2F)
|
||||
|
||||
> **Note**: If you want to make **your own** Minecraft server accessible to web clients (without our proxies), you can use [mwc-proxy](https://github.com/zardoy/mwc-proxy) - a lightweight JS WebSocket proxy that runs on the same server as your Minecraft server, allowing players to connect directly via `wss://play.example.com`. `?client_mcraft` is added to the URL, so the proxy will know that it's this client.
|
||||
|
||||
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
|
||||
|
||||
```mermaid
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
{
|
||||
"ip": "wss://play.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play.webmc.fun",
|
||||
"name": "WebMC"
|
||||
},
|
||||
{
|
||||
"ip": "wss://ws.fuchsmc.net"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
"jszip": "^3.10.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mcraft-fun-mineflayer": "^0.1.23",
|
||||
"minecraft-data": "3.92.0",
|
||||
"minecraft-data": "3.98.0",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
|
||||
"mojangson": "^2.0.4",
|
||||
|
|
@ -205,7 +205,7 @@
|
|||
"diamond-square": "github:zardoy/diamond-square",
|
||||
"prismarine-block": "github:zardoy/prismarine-block#next-era",
|
||||
"prismarine-world": "github:zardoy/prismarine-world#next-era",
|
||||
"minecraft-data": "3.92.0",
|
||||
"minecraft-data": "3.98.0",
|
||||
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
|
||||
"prismarine-physics": "github:zardoy/prismarine-physics",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
|
|
|
|||
110
pnpm-lock.yaml
generated
110
pnpm-lock.yaml
generated
|
|
@ -13,7 +13,7 @@ overrides:
|
|||
diamond-square: github:zardoy/diamond-square
|
||||
prismarine-block: github:zardoy/prismarine-block#next-era
|
||||
prismarine-world: github:zardoy/prismarine-world#next-era
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-data: 3.98.0
|
||||
prismarine-provider-anvil: github:zardoy/prismarine-provider-anvil#everything
|
||||
prismarine-physics: github:zardoy/prismarine-physics
|
||||
minecraft-protocol: github:PrismarineJS/node-minecraft-protocol#master
|
||||
|
|
@ -140,13 +140,13 @@ importers:
|
|||
version: 4.17.21
|
||||
mcraft-fun-mineflayer:
|
||||
specifier: ^0.1.23
|
||||
version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13))
|
||||
version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13))
|
||||
minecraft-data:
|
||||
specifier: 3.92.0
|
||||
version: 3.92.0
|
||||
specifier: 3.98.0
|
||||
version: 3.98.0
|
||||
minecraft-protocol:
|
||||
specifier: github:PrismarineJS/node-minecraft-protocol#master
|
||||
version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
|
||||
version: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
|
||||
mineflayer-item-map-downloader:
|
||||
specifier: github:zardoy/mineflayer-item-map-downloader
|
||||
version: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13)
|
||||
|
|
@ -170,7 +170,7 @@ importers:
|
|||
version: 6.1.1
|
||||
prismarine-provider-anvil:
|
||||
specifier: github:zardoy/prismarine-provider-anvil#everything
|
||||
version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0)
|
||||
version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0)
|
||||
prosemirror-example-setup:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.3
|
||||
|
|
@ -345,7 +345,7 @@ importers:
|
|||
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1)
|
||||
mineflayer:
|
||||
specifier: github:zardoy/mineflayer#gen-the-master
|
||||
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)
|
||||
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
|
||||
mineflayer-mouse:
|
||||
specifier: ^0.1.21
|
||||
version: 0.1.21
|
||||
|
|
@ -436,7 +436,7 @@ importers:
|
|||
version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
|
||||
prismarine-chunk:
|
||||
specifier: github:zardoy/prismarine-chunk#master
|
||||
version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
|
||||
version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
|
||||
prismarine-schematic:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.3
|
||||
|
|
@ -6448,7 +6448,7 @@ packages:
|
|||
resolution: {integrity: sha512-H9jPt2xEU77itC27dSz3qazHYqN9qVsv4HgMPozg7RqQ1uwgXmEa+ojKIlDtXf/TLJsG6Kv4EbzGa8a1Wh72uA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
peerDependencies:
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-data: 3.98.0
|
||||
|
||||
mcraft-fun-mineflayer@0.1.23:
|
||||
resolution: {integrity: sha512-qmI1rQQ0Ro5zJdi99rClWLF+mS9JZffgNX2vyWWesS3Hsk3Xblp/8swYTJKHSaFpNgzkVfXV92fEIrBqeH6iKA==}
|
||||
|
|
@ -6658,8 +6658,8 @@ packages:
|
|||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
minecraft-data@3.92.0:
|
||||
resolution: {integrity: sha512-CGfO50svzm+pSRa4Mbq4owsmRKbPCNkSZ3MCOyH+epC7yNjh+PUhPQFHWq72O51qsY7pAB5qM/bJn1ncwG1J5g==}
|
||||
minecraft-data@3.98.0:
|
||||
resolution: {integrity: sha512-JAPqJ/TZoxMUlAPPdWUh1v5wdqvYGFSZ4rW9bUtmaKBkGpomDSjw4V02ocBqbxKJvcTtmc5nM/LfN9/0DDqHrQ==}
|
||||
|
||||
minecraft-folder-path@1.2.0:
|
||||
resolution: {integrity: sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==}
|
||||
|
|
@ -6668,9 +6668,9 @@ packages:
|
|||
resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41}
|
||||
version: 1.0.1
|
||||
|
||||
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074:
|
||||
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074}
|
||||
version: 1.61.0
|
||||
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9:
|
||||
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9}
|
||||
version: 1.62.0
|
||||
engines: {node: '>=22'}
|
||||
|
||||
minecraft-wrap@1.6.0:
|
||||
|
|
@ -6688,8 +6688,8 @@ packages:
|
|||
resolution: {integrity: sha512-1XTVuw3twIrEcqQ1QRSB8NcStIUEZ+tbxiAG6rOrN/9M4thhtlS5PTJzFdmdrcYgWEBLvuOdJszaKE5zFfiXhg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8}
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659}
|
||||
version: 8.0.0
|
||||
engines: {node: '>=22'}
|
||||
|
||||
|
|
@ -7387,7 +7387,7 @@ packages:
|
|||
prismarine-biome@1.3.0:
|
||||
resolution: {integrity: sha512-GY6nZxq93mTErT7jD7jt8YS1aPrOakbJHh39seYsJFXvueIOdHAmW16kYQVrTVMW5MlWLQVxV/EquRwOgr4MnQ==}
|
||||
peerDependencies:
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-data: 3.98.0
|
||||
prismarine-registry: ^1.1.0
|
||||
|
||||
prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9:
|
||||
|
|
@ -11343,8 +11343,8 @@ snapshots:
|
|||
'@nxg-org/mineflayer-trajectories@1.2.0(encoding@0.1.13)':
|
||||
dependencies:
|
||||
'@nxg-org/mineflayer-util-plugin': 1.8.4
|
||||
minecraft-data: 3.92.0
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)
|
||||
minecraft-data: 3.98.0
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
|
||||
prismarine-item: 1.17.0
|
||||
prismarine-physics: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b
|
||||
|
|
@ -13104,18 +13104,18 @@ snapshots:
|
|||
exit-hook: 2.2.1
|
||||
flatmap: 0.0.3
|
||||
long: 5.3.1
|
||||
mc-bridge: 0.1.3(minecraft-data@3.92.0)
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
|
||||
mc-bridge: 0.1.3(minecraft-data@3.98.0)
|
||||
minecraft-data: 3.98.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
|
||||
mkdirp: 2.1.6
|
||||
node-gzip: 1.1.2
|
||||
node-rsa: 1.1.1
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
|
||||
prismarine-entity: 2.5.0
|
||||
prismarine-item: 1.17.0
|
||||
prismarine-nbt: 2.7.0
|
||||
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0)
|
||||
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0)
|
||||
prismarine-windows: 2.9.0
|
||||
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
|
||||
rambda: 9.4.2
|
||||
|
|
@ -13142,16 +13142,16 @@ snapshots:
|
|||
exit-hook: 2.2.1
|
||||
flatmap: 0.0.3
|
||||
long: 5.3.1
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
|
||||
minecraft-data: 3.98.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
|
||||
mkdirp: 2.1.6
|
||||
node-gzip: 1.1.2
|
||||
node-rsa: 1.1.1
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
|
||||
prismarine-entity: 2.5.0
|
||||
prismarine-item: 1.17.0
|
||||
prismarine-nbt: 2.7.0
|
||||
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0)
|
||||
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0)
|
||||
prismarine-windows: 2.9.0
|
||||
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
|
||||
rambda: 9.4.2
|
||||
|
|
@ -14542,8 +14542,8 @@ snapshots:
|
|||
|
||||
diamond-square@https://codeload.github.com/zardoy/diamond-square/tar.gz/cfaad2d1d5909fdfa63c8cc7bc05fb5e87782d71:
|
||||
dependencies:
|
||||
minecraft-data: 3.92.0
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
|
||||
minecraft-data: 3.98.0
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
|
||||
prismarine-registry: 1.11.0
|
||||
random-seed: 0.3.0
|
||||
vec3: 0.1.10
|
||||
|
|
@ -16986,16 +16986,16 @@ snapshots:
|
|||
maxrects-packer: '@zardoy/maxrects-packer@2.7.4'
|
||||
zod: 3.24.2
|
||||
|
||||
mc-bridge@0.1.3(minecraft-data@3.92.0):
|
||||
mc-bridge@0.1.3(minecraft-data@3.98.0):
|
||||
dependencies:
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-data: 3.98.0
|
||||
|
||||
mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)):
|
||||
mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)):
|
||||
dependencies:
|
||||
'@zardoy/flying-squid': 0.0.49(encoding@0.1.13)
|
||||
exit-hook: 2.2.1
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
|
||||
prismarine-item: 1.17.0
|
||||
ws: 8.18.1
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -17302,7 +17302,7 @@ snapshots:
|
|||
|
||||
min-indent@1.0.1: {}
|
||||
|
||||
minecraft-data@3.92.0: {}
|
||||
minecraft-data@3.98.0: {}
|
||||
|
||||
minecraft-folder-path@1.2.0: {}
|
||||
|
||||
|
|
@ -17313,7 +17313,7 @@ snapshots:
|
|||
- '@types/react'
|
||||
- react
|
||||
|
||||
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13):
|
||||
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@types/node-rsa': 1.1.4
|
||||
'@types/readable-stream': 4.0.18
|
||||
|
|
@ -17322,7 +17322,7 @@ snapshots:
|
|||
debug: 4.4.0(supports-color@8.1.1)
|
||||
endian-toggle: 0.0.0
|
||||
lodash.merge: 4.6.2
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-data: 3.98.0
|
||||
minecraft-folder-path: 1.2.0
|
||||
node-fetch: 2.7.0(encoding@0.1.13)
|
||||
node-rsa: 0.4.2
|
||||
|
|
@ -17365,7 +17365,7 @@ snapshots:
|
|||
|
||||
mineflayer-item-map-downloader@https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824(patch_hash=a731ebbace2d8790c973ab3a5ba33494a6e9658533a9710dd8ba36f86db061ad)(encoding@0.1.13):
|
||||
dependencies:
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13)
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13)
|
||||
sharp: 0.30.7
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
|
@ -17380,15 +17380,15 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/86e65631e79c490021afc63c80091a7bb6019fa8(encoding@0.1.13):
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/dd3b1ff38506d6f72d90e8444186e4e75fe82659(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@nxg-org/mineflayer-physics-util': 1.8.10
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/c561917bf7e7966911321512c2a6895a3f9da074(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0)
|
||||
minecraft-data: 3.98.0
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/bf89f7e86526c54d8c43f555d8f6dfa4948fd2d9(patch_hash=4ebdae314c68d01ce7879445c0b8bde5f90373abba8b66ed00d42e7a5f542f8b)(encoding@0.1.13)
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0)
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
|
||||
prismarine-chat: 1.11.0
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
|
||||
prismarine-entity: 2.5.0
|
||||
prismarine-item: 1.17.0
|
||||
prismarine-nbt: 2.7.0
|
||||
|
|
@ -18174,15 +18174,15 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
prismarine-biome@1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0):
|
||||
prismarine-biome@1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0):
|
||||
dependencies:
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-data: 3.98.0
|
||||
prismarine-registry: 1.11.0
|
||||
|
||||
prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9:
|
||||
dependencies:
|
||||
minecraft-data: 3.92.0
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0)
|
||||
minecraft-data: 3.98.0
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0)
|
||||
prismarine-chat: 1.11.0
|
||||
prismarine-item: 1.17.0
|
||||
prismarine-nbt: 2.7.0
|
||||
|
|
@ -18194,9 +18194,9 @@ snapshots:
|
|||
prismarine-nbt: 2.7.0
|
||||
prismarine-registry: 1.11.0
|
||||
|
||||
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0):
|
||||
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0):
|
||||
dependencies:
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.92.0)(prismarine-registry@1.11.0)
|
||||
prismarine-biome: 1.3.0(minecraft-data@3.98.0)(prismarine-registry@1.11.0)
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
|
||||
prismarine-nbt: 2.7.0
|
||||
prismarine-registry: 1.11.0
|
||||
|
|
@ -18225,14 +18225,14 @@ snapshots:
|
|||
|
||||
prismarine-physics@https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b:
|
||||
dependencies:
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-data: 3.98.0
|
||||
prismarine-nbt: 2.7.0
|
||||
vec3: 0.1.10
|
||||
|
||||
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.92.0):
|
||||
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.98.0):
|
||||
dependencies:
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.92.0)
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/c5feac83b61d95feb4d4f22c063dacfb8c192a9f(minecraft-data@3.98.0)
|
||||
prismarine-nbt: 2.7.0
|
||||
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
|
||||
uint4: 0.1.2
|
||||
|
|
@ -18254,13 +18254,13 @@ snapshots:
|
|||
|
||||
prismarine-registry@1.11.0:
|
||||
dependencies:
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-data: 3.98.0
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
|
||||
prismarine-nbt: 2.7.0
|
||||
|
||||
prismarine-schematic@1.2.3:
|
||||
dependencies:
|
||||
minecraft-data: 3.92.0
|
||||
minecraft-data: 3.98.0
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9
|
||||
prismarine-nbt: 2.7.0
|
||||
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
|
||||
|
|
|
|||
55
renderer/viewer/lib/createPlayerObject.ts
Normal file
55
renderer/viewer/lib/createPlayerObject.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { PlayerObject, PlayerAnimation } from 'skinview3d'
|
||||
import * as THREE from 'three'
|
||||
import { WalkingGeneralSwing } from '../three/entity/animations'
|
||||
import { loadSkinImage, stevePngUrl } from './utils/skins'
|
||||
|
||||
export type PlayerObjectType = PlayerObject & {
|
||||
animation?: PlayerAnimation
|
||||
realPlayerUuid: string
|
||||
realUsername: string
|
||||
}
|
||||
|
||||
export function createPlayerObject (options: {
|
||||
username?: string
|
||||
uuid?: string
|
||||
scale?: number
|
||||
}): {
|
||||
playerObject: PlayerObjectType
|
||||
wrapper: THREE.Group
|
||||
} {
|
||||
const wrapper = new THREE.Group()
|
||||
const playerObject = new PlayerObject() as PlayerObjectType
|
||||
|
||||
playerObject.realPlayerUuid = options.uuid ?? ''
|
||||
playerObject.realUsername = options.username ?? ''
|
||||
playerObject.position.set(0, 16, 0)
|
||||
|
||||
// fix issues with starfield
|
||||
playerObject.traverse((obj) => {
|
||||
if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) {
|
||||
obj.material.transparent = true
|
||||
}
|
||||
})
|
||||
|
||||
wrapper.add(playerObject as any)
|
||||
const scale = options.scale ?? (1 / 16)
|
||||
wrapper.scale.set(scale, scale, scale)
|
||||
wrapper.rotation.set(0, Math.PI, 0)
|
||||
|
||||
// Set up animation
|
||||
playerObject.animation = new WalkingGeneralSwing()
|
||||
;(playerObject.animation as WalkingGeneralSwing).isMoving = false
|
||||
playerObject.animation.update(playerObject, 0)
|
||||
|
||||
return { playerObject, wrapper }
|
||||
}
|
||||
|
||||
export const applySkinToPlayerObject = async (playerObject: PlayerObjectType, skinUrl: string) => {
|
||||
return loadSkinImage(skinUrl || stevePngUrl).then(({ canvas }) => {
|
||||
const skinTexture = new THREE.CanvasTexture(canvas)
|
||||
skinTexture.magFilter = THREE.NearestFilter
|
||||
skinTexture.minFilter = THREE.NearestFilter
|
||||
skinTexture.needsUpdate = true
|
||||
playerObject.skin.map = skinTexture as any
|
||||
}).catch(console.error)
|
||||
}
|
||||
|
|
@ -47,6 +47,7 @@ export const defaultWorldRendererConfig = {
|
|||
smoothLighting: true,
|
||||
enableLighting: true,
|
||||
starfield: true,
|
||||
defaultSkybox: true,
|
||||
renderEntities: true,
|
||||
extraBlockRenderers: true,
|
||||
foreground: true,
|
||||
|
|
|
|||
|
|
@ -80,8 +80,12 @@ export class CameraShake {
|
|||
camera.setRotationFromQuaternion(yawQuat)
|
||||
} else {
|
||||
// For regular camera, apply all rotations
|
||||
const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.basePitch)
|
||||
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.baseYaw)
|
||||
// Add tiny offsets to prevent z-fighting at ideal angles (90, 180, 270 degrees)
|
||||
const pitchOffset = this.addAntiZfightingOffset(this.basePitch)
|
||||
const yawOffset = this.addAntiZfightingOffset(this.baseYaw)
|
||||
|
||||
const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitchOffset)
|
||||
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawOffset)
|
||||
const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle))
|
||||
// Combine rotations in the correct order: pitch -> yaw -> roll
|
||||
const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat)
|
||||
|
|
@ -96,4 +100,21 @@ export class CameraShake {
|
|||
private easeInOut (t: number): number {
|
||||
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2
|
||||
}
|
||||
|
||||
private addAntiZfightingOffset (angle: number): number {
|
||||
const offset = 0.001 // Very small offset in radians (about 0.057 degrees)
|
||||
|
||||
// Check if the angle is close to ideal angles (0, π/2, π, 3π/2)
|
||||
const normalizedAngle = ((angle % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2)
|
||||
const tolerance = 0.01 // Tolerance for considering an angle "ideal"
|
||||
|
||||
if (Math.abs(normalizedAngle) < tolerance ||
|
||||
Math.abs(normalizedAngle - Math.PI / 2) < tolerance ||
|
||||
Math.abs(normalizedAngle - Math.PI) < tolerance ||
|
||||
Math.abs(normalizedAngle - 3 * Math.PI / 2) < tolerance) {
|
||||
return angle + offset
|
||||
}
|
||||
|
||||
return angle
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { ItemSpecificContextProperties } from '../lib/basePlayerState'
|
|||
import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins'
|
||||
import { renderComponent } from '../sign-renderer'
|
||||
import { createCanvas } from '../lib/utils'
|
||||
import { PlayerObjectType } from '../lib/createPlayerObject'
|
||||
import { getBlockMeshFromModel } from './holdingBlock'
|
||||
import { createItemMesh } from './itemMesh'
|
||||
import * as Entity from './entity/EntityMesh'
|
||||
|
|
@ -33,12 +34,6 @@ export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl)
|
|||
|
||||
export const TWEEN_DURATION = 120
|
||||
|
||||
type PlayerObjectType = PlayerObject & {
|
||||
animation?: PlayerAnimation
|
||||
realPlayerUuid: string
|
||||
realUsername: string
|
||||
}
|
||||
|
||||
function convert2sComplementToHex (complement: number) {
|
||||
if (complement < 0) {
|
||||
complement = (0xFF_FF_FF_FF + complement + 1) >>> 0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as THREE from 'three'
|
||||
import { DebugGui } from '../lib/DebugGui'
|
||||
|
||||
export const DEFAULT_TEMPERATURE = 0.75
|
||||
|
||||
|
|
@ -17,11 +18,33 @@ export class SkyboxRenderer {
|
|||
private waterBreathing = false
|
||||
private fogBrightness = 0
|
||||
private prevFogBrightness = 0
|
||||
private readonly fogOrangeness = 0 // Debug property to control sky color orangeness
|
||||
private readonly distanceFactor = 2.7
|
||||
|
||||
private readonly brightnessAtPosition = 1
|
||||
debugGui: DebugGui
|
||||
|
||||
constructor (private readonly scene: THREE.Scene, public defaultSkybox: boolean, public initialImage: string | null) {
|
||||
this.debugGui = new DebugGui('skybox_renderer', this, [
|
||||
'temperature',
|
||||
'worldTime',
|
||||
'inWater',
|
||||
'waterBreathing',
|
||||
'fogOrangeness',
|
||||
'brightnessAtPosition',
|
||||
'distanceFactor'
|
||||
], {
|
||||
brightnessAtPosition: { min: 0, max: 1, step: 0.01 },
|
||||
temperature: { min: 0, max: 1, step: 0.01 },
|
||||
worldTime: { min: 0, max: 24_000, step: 1 },
|
||||
fogOrangeness: { min: -1, max: 1, step: 0.01 },
|
||||
distanceFactor: { min: 0, max: 5, step: 0.01 },
|
||||
})
|
||||
|
||||
constructor (private readonly scene: THREE.Scene, public initialImage: string | null) {
|
||||
if (!initialImage) {
|
||||
this.createGradientSky()
|
||||
}
|
||||
// this.debugGui.activate()
|
||||
}
|
||||
|
||||
async init () {
|
||||
|
|
@ -95,6 +118,7 @@ export class SkyboxRenderer {
|
|||
|
||||
// Update world time
|
||||
updateTime (timeOfDay: number, partialTicks = 0) {
|
||||
if (this.debugGui.visible) return
|
||||
this.worldTime = timeOfDay
|
||||
this.partialTicks = partialTicks
|
||||
this.updateSkyColors()
|
||||
|
|
@ -108,17 +132,26 @@ export class SkyboxRenderer {
|
|||
|
||||
// Update temperature (for biome support)
|
||||
updateTemperature (temperature: number) {
|
||||
if (this.debugGui.visible) return
|
||||
this.temperature = temperature
|
||||
this.updateSkyColors()
|
||||
}
|
||||
|
||||
// Update water state
|
||||
updateWaterState (inWater: boolean, waterBreathing: boolean) {
|
||||
if (this.debugGui.visible) return
|
||||
this.inWater = inWater
|
||||
this.waterBreathing = waterBreathing
|
||||
this.updateSkyColors()
|
||||
}
|
||||
|
||||
// Update default skybox setting
|
||||
updateDefaultSkybox (defaultSkybox: boolean) {
|
||||
if (this.debugGui.visible) return
|
||||
this.defaultSkybox = defaultSkybox
|
||||
this.updateSkyColors()
|
||||
}
|
||||
|
||||
private createGradientSky () {
|
||||
const size = 64
|
||||
const scale = 256 / size + 2
|
||||
|
|
@ -223,8 +256,15 @@ export class SkyboxRenderer {
|
|||
if (temperature < -1) temperature = -1
|
||||
if (temperature > 1) temperature = 1
|
||||
|
||||
const hue = 0.622_222_2 - temperature * 0.05
|
||||
const saturation = 0.5 + temperature * 0.1
|
||||
// Apply debug fog orangeness to hue - positive values make it more orange, negative make it less orange
|
||||
const baseHue = 0.622_222_2 - temperature * 0.05
|
||||
// Orange is around hue 0.08-0.15, so we need to shift from blue-purple (0.62) toward orange
|
||||
// Use a more dramatic shift and also increase saturation for more noticeable effect
|
||||
const orangeHue = 0.12 // Orange hue value
|
||||
const hue = this.fogOrangeness > 0
|
||||
? baseHue + (orangeHue - baseHue) * this.fogOrangeness * 0.8 // Blend toward orange
|
||||
: baseHue + this.fogOrangeness * 0.1 // Subtle shift for negative values
|
||||
const saturation = 0.5 + temperature * 0.1 + Math.abs(this.fogOrangeness) * 0.3 // Increase saturation with orangeness
|
||||
const brightness = 1
|
||||
|
||||
return this.hsbToRgb(hue, saturation, brightness)
|
||||
|
|
@ -279,11 +319,27 @@ export class SkyboxRenderer {
|
|||
private updateSkyColors () {
|
||||
if (!this.skyMesh || !this.voidMesh) return
|
||||
|
||||
// If default skybox is disabled, hide the skybox meshes
|
||||
if (!this.defaultSkybox) {
|
||||
this.skyMesh.visible = false
|
||||
this.voidMesh.visible = false
|
||||
if (this.mesh) {
|
||||
this.mesh.visible = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Show skybox meshes when default skybox is enabled
|
||||
this.skyMesh.visible = true
|
||||
this.voidMesh.visible = true
|
||||
if (this.mesh) {
|
||||
this.mesh.visible = true
|
||||
}
|
||||
|
||||
// Update fog brightness with smooth transition
|
||||
this.prevFogBrightness = this.fogBrightness
|
||||
const renderDistance = this.viewDistance / 32
|
||||
const brightnessAtPosition = 1 // Could be affected by light level in future
|
||||
const targetBrightness = brightnessAtPosition * (1 - renderDistance) + renderDistance
|
||||
const targetBrightness = this.brightnessAtPosition * (1 - renderDistance) + renderDistance
|
||||
this.fogBrightness += (targetBrightness - this.fogBrightness) * 0.1
|
||||
|
||||
// Handle water fog
|
||||
|
|
@ -317,7 +373,7 @@ export class SkyboxRenderer {
|
|||
const blue = (fogColor.z + (skyColor.z - fogColor.z) * viewFactor) * clampedBrightness * interpolatedBrightness
|
||||
|
||||
this.scene.background = new THREE.Color(red, green, blue)
|
||||
this.scene.fog = new THREE.Fog(new THREE.Color(red, green, blue), 0.0025, viewDistance * 2)
|
||||
this.scene.fog = new THREE.Fog(new THREE.Color(red, green, blue), 0.0025, viewDistance * this.distanceFactor)
|
||||
|
||||
;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(skyColor.x, skyColor.y, skyColor.z))
|
||||
;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const WAYPOINT_CONFIG = {
|
|||
CANVAS_SCALE: 2,
|
||||
ARROW: {
|
||||
enabledDefault: false,
|
||||
pixelSize: 30,
|
||||
pixelSize: 50,
|
||||
paddingPx: 50,
|
||||
},
|
||||
}
|
||||
|
|
@ -50,6 +50,7 @@ export function createWaypointSprite (options: {
|
|||
depthTest?: boolean,
|
||||
// Y offset in world units used by updateScaleWorld only (screen-pixel API ignores this)
|
||||
labelYOffset?: number,
|
||||
metadata?: any,
|
||||
}): WaypointSprite {
|
||||
const color = options.color ?? 0xFF_00_00
|
||||
const depthTest = options.depthTest ?? false
|
||||
|
|
@ -131,16 +132,22 @@ export function createWaypointSprite (options: {
|
|||
canvas.height = size
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.clearRect(0, 0, size, size)
|
||||
|
||||
// Draw arrow shape
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(size * 0.2, size * 0.5)
|
||||
ctx.lineTo(size * 0.8, size * 0.5)
|
||||
ctx.lineTo(size * 0.5, size * 0.2)
|
||||
ctx.moveTo(size * 0.15, size * 0.5)
|
||||
ctx.lineTo(size * 0.85, size * 0.5)
|
||||
ctx.lineTo(size * 0.5, size * 0.15)
|
||||
ctx.closePath()
|
||||
ctx.lineWidth = 4
|
||||
|
||||
// Use waypoint color for arrow
|
||||
const colorHex = `#${color.toString(16).padStart(6, '0')}`
|
||||
ctx.lineWidth = 6
|
||||
ctx.strokeStyle = 'black'
|
||||
ctx.stroke()
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fillStyle = colorHex
|
||||
ctx.fill()
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas)
|
||||
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false })
|
||||
arrowSprite = new THREE.Sprite(material)
|
||||
|
|
@ -169,6 +176,9 @@ export function createWaypointSprite (options: {
|
|||
ensureArrow()
|
||||
if (!arrowSprite) return true
|
||||
|
||||
// Check if onlyLeftRight is enabled in metadata
|
||||
const onlyLeftRight = options.metadata?.onlyLeftRight === true
|
||||
|
||||
// Build camera basis using camera.up to respect custom orientations
|
||||
const forward = new THREE.Vector3()
|
||||
camera.getWorldDirection(forward) // camera look direction
|
||||
|
|
@ -213,6 +223,20 @@ export function createWaypointSprite (options: {
|
|||
}
|
||||
}
|
||||
|
||||
// Apply onlyLeftRight logic - restrict arrows to left/right edges only
|
||||
if (onlyLeftRight) {
|
||||
// Force the arrow to appear only on left or right edges
|
||||
if (Math.abs(rx) > Math.abs(ry)) {
|
||||
// Horizontal direction is dominant, keep it
|
||||
ry = 0
|
||||
} else {
|
||||
// Vertical direction is dominant, but we want only left/right
|
||||
// So choose left or right based on the sign of rx
|
||||
rx = rx >= 0 ? 1 : -1
|
||||
ry = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Place on the rectangle border [-1,1]x[-1,1]
|
||||
const s = Math.max(Math.abs(rx), Math.abs(ry)) || 1
|
||||
let ndcX = rx / s
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ interface WaypointOptions {
|
|||
color?: number
|
||||
label?: string
|
||||
minDistance?: number
|
||||
metadata?: any
|
||||
}
|
||||
|
||||
export class WaypointsRenderer {
|
||||
|
|
@ -71,13 +72,14 @@ export class WaypointsRenderer {
|
|||
this.removeWaypoint(id)
|
||||
|
||||
const color = options.color ?? 0xFF_00_00
|
||||
const { label } = options
|
||||
const { label, metadata } = options
|
||||
const minDistance = options.minDistance ?? 0
|
||||
|
||||
const sprite = createWaypointSprite({
|
||||
position: new THREE.Vector3(x, y, z),
|
||||
color,
|
||||
label: (label || id),
|
||||
metadata,
|
||||
})
|
||||
sprite.enableOffscreenArrow(true)
|
||||
sprite.setArrowParent(this.waypointScene)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export class CursorBlock {
|
|||
}
|
||||
|
||||
cursorLineMaterial: LineMaterial
|
||||
interactionLines: null | { blockPos: Vec3, mesh: THREE.Group } = null
|
||||
interactionLines: null | { blockPos: Vec3, mesh: THREE.Group, shapePositions: BlocksShapes | undefined } = null
|
||||
prevColor: string | undefined
|
||||
blockBreakMesh: THREE.Mesh
|
||||
breakTextures: THREE.Texture[] = []
|
||||
|
|
@ -62,6 +62,13 @@ export class CursorBlock {
|
|||
this.worldRenderer.onReactivePlayerStateUpdated('gameMode', () => {
|
||||
this.updateLineMaterial()
|
||||
})
|
||||
// todo figure out why otherwise fog from skybox breaks it
|
||||
setTimeout(() => {
|
||||
this.updateLineMaterial()
|
||||
if (this.interactionLines) {
|
||||
this.setHighlightCursorBlock(this.interactionLines.blockPos, this.interactionLines.shapePositions, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Update functions
|
||||
|
|
@ -69,6 +76,9 @@ export class CursorBlock {
|
|||
const inCreative = this.worldRenderer.playerStateReactive.gameMode === 'creative'
|
||||
const pixelRatio = this.worldRenderer.renderer.getPixelRatio()
|
||||
|
||||
if (this.cursorLineMaterial) {
|
||||
this.cursorLineMaterial.dispose()
|
||||
}
|
||||
this.cursorLineMaterial = new LineMaterial({
|
||||
color: (() => {
|
||||
switch (this.worldRenderer.worldRendererConfig.highlightBlockColor) {
|
||||
|
|
@ -115,8 +125,8 @@ export class CursorBlock {
|
|||
}
|
||||
}
|
||||
|
||||
setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: BlocksShapes): void {
|
||||
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) {
|
||||
setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: BlocksShapes, force = false): void {
|
||||
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos) && !force) {
|
||||
return
|
||||
}
|
||||
if (this.interactionLines !== null) {
|
||||
|
|
@ -140,7 +150,7 @@ export class CursorBlock {
|
|||
}
|
||||
this.worldRenderer.scene.add(group)
|
||||
group.visible = !this.cursorLinesHidden
|
||||
this.interactionLines = { blockPos, mesh: group }
|
||||
this.interactionLines = { blockPos, mesh: group, shapePositions }
|
||||
}
|
||||
|
||||
render () {
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
this.holdingBlockLeft = new HoldingBlock(this, true)
|
||||
|
||||
// Initialize skybox renderer
|
||||
this.skyboxRenderer = new SkyboxRenderer(this.scene, null)
|
||||
this.skyboxRenderer = new SkyboxRenderer(this.scene, this.worldRendererConfig.defaultSkybox, null)
|
||||
void this.skyboxRenderer.init()
|
||||
|
||||
this.addDebugOverlay()
|
||||
|
|
@ -206,6 +206,9 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
this.onReactiveConfigUpdated('showChunkBorders', (value) => {
|
||||
this.updateShowChunksBorder(value)
|
||||
})
|
||||
this.onReactiveConfigUpdated('defaultSkybox', (value) => {
|
||||
this.skyboxRenderer.updateDefaultSkybox(value)
|
||||
})
|
||||
}
|
||||
|
||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@ console.log('size', fs.lstatSync(filePath).size / 1000 / 1000, gzipSizeFromFileS
|
|||
|
||||
const { defaultVersion } = MCProtocol
|
||||
const data = MinecraftData(defaultVersion)
|
||||
console.log('defaultVersion', defaultVersion, !!data)
|
||||
const initialMcData = {
|
||||
[defaultVersion]: {
|
||||
version: data.version,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export type AppConfig = {
|
|||
// defaultVersion?: string
|
||||
peerJsServer?: string
|
||||
peerJsServerFallback?: string
|
||||
promoteServers?: Array<{ ip, description, version? }>
|
||||
promoteServers?: Array<{ ip, description, name?, version?, }>
|
||||
mapsProvider?: string
|
||||
|
||||
appParams?: Record<string, any> // query string params
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ let audioContext: AudioContext
|
|||
const sounds: Record<string, any> = {}
|
||||
|
||||
// Track currently playing sounds and their gain nodes
|
||||
const activeSounds: Array<{ source: AudioBufferSourceNode; gainNode: GainNode; volumeMultiplier: number }> = []
|
||||
const activeSounds: Array<{
|
||||
source: AudioBufferSourceNode;
|
||||
gainNode: GainNode;
|
||||
volumeMultiplier: number;
|
||||
isMusic: boolean;
|
||||
}> = []
|
||||
window.activeSounds = activeSounds
|
||||
|
||||
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
|
||||
|
|
@ -43,7 +48,7 @@ export async function loadSound (path: string, contents = path) {
|
|||
}
|
||||
}
|
||||
|
||||
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false) => {
|
||||
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false, isMusic = false) => {
|
||||
const soundBuffer = sounds[url]
|
||||
if (!soundBuffer) {
|
||||
const start = Date.now()
|
||||
|
|
@ -51,11 +56,11 @@ export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = option
|
|||
if (cancelled || Date.now() - start > loadTimeout) return
|
||||
}
|
||||
|
||||
return playSound(url, soundVolume, loop)
|
||||
return playSound(url, soundVolume, loop, isMusic)
|
||||
}
|
||||
|
||||
export async function playSound (url, soundVolume = 1, loop = false) {
|
||||
const volume = soundVolume * (options.volume / 100)
|
||||
export async function playSound (url, soundVolume = 1, loop = false, isMusic = false) {
|
||||
const volume = soundVolume * (options.volume / 100) * (isMusic ? options.musicVolume / 100 : 1)
|
||||
|
||||
if (!volume) return
|
||||
|
||||
|
|
@ -82,7 +87,7 @@ export async function playSound (url, soundVolume = 1, loop = false) {
|
|||
source.start(0)
|
||||
|
||||
// Add to active sounds
|
||||
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume })
|
||||
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume, isMusic })
|
||||
|
||||
const callbacks = [] as Array<() => void>
|
||||
source.onended = () => {
|
||||
|
|
@ -110,6 +115,7 @@ export async function playSound (url, soundVolume = 1, loop = false) {
|
|||
console.warn('Failed to stop sound:', err)
|
||||
}
|
||||
},
|
||||
gainNode,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,11 +143,11 @@ export function stopSound (url: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
|
||||
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number, newMusicVolume: number) {
|
||||
const normalizedVolume = newVolume / 100
|
||||
for (const { gainNode, volumeMultiplier } of activeSounds) {
|
||||
for (const { gainNode, volumeMultiplier, isMusic } of activeSounds) {
|
||||
try {
|
||||
gainNode.gain.value = normalizedVolume * volumeMultiplier
|
||||
gainNode.gain.value = normalizedVolume * volumeMultiplier * (isMusic ? newMusicVolume / 100 : 1)
|
||||
} catch (err) {
|
||||
console.warn('Failed to change sound volume:', err)
|
||||
}
|
||||
|
|
@ -149,5 +155,9 @@ export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
|
|||
}
|
||||
|
||||
subscribeKey(options, 'volume', () => {
|
||||
changeVolumeOfCurrentlyPlayingSounds(options.volume)
|
||||
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume)
|
||||
})
|
||||
|
||||
subscribeKey(options, 'musicVolume', () => {
|
||||
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -82,15 +82,30 @@ const registerWaypointChannels = () => {
|
|||
{
|
||||
name: 'color',
|
||||
type: 'i32'
|
||||
},
|
||||
{
|
||||
name: 'metadataJson',
|
||||
type: ['pstring', { countType: 'i16' }]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
registerChannel('minecraft-web-client:waypoint-add', packetStructure, (data) => {
|
||||
// Parse metadata if provided
|
||||
let metadata: any = {}
|
||||
if (data.metadataJson && data.metadataJson.trim() !== '') {
|
||||
try {
|
||||
metadata = JSON.parse(data.metadataJson)
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse waypoint metadataJson:', error)
|
||||
}
|
||||
}
|
||||
|
||||
getThreeJsRendererMethods()?.addWaypoint(data.id, data.x, data.y, data.z, {
|
||||
minDistance: data.minDistance,
|
||||
label: data.label || undefined,
|
||||
color: data.color || undefined
|
||||
color: data.color || undefined,
|
||||
metadata
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ export const defaultOptions = {
|
|||
chatOpacityOpened: 100,
|
||||
messagesLimit: 200,
|
||||
volume: 50,
|
||||
enableMusic: false,
|
||||
enableMusic: true,
|
||||
musicVolume: 50,
|
||||
// fov: 70,
|
||||
fov: 75,
|
||||
defaultPerspective: 'first_person' as 'first_person' | 'third_person_back' | 'third_person_front',
|
||||
|
|
@ -41,6 +42,7 @@ export const defaultOptions = {
|
|||
renderEars: true,
|
||||
lowMemoryMode: false,
|
||||
starfieldRendering: true,
|
||||
defaultSkybox: true,
|
||||
enabledResourcepack: null as string | null,
|
||||
useVersionsTextures: 'latest',
|
||||
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
|
||||
|
|
@ -83,6 +85,7 @@ export const defaultOptions = {
|
|||
localServerOptions: {
|
||||
gameMode: 1
|
||||
} as any,
|
||||
saveLoginPassword: 'prompt' as 'prompt' | 'never' | 'always',
|
||||
preferLoadReadonly: false,
|
||||
experimentalClientSelfReload: false,
|
||||
remoteSoundsSupport: false,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,17 @@ import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree'
|
|||
import { enable, disable, enabled } from 'debug'
|
||||
import { Vec3 } from 'vec3'
|
||||
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
window.debugServerPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toClient.types).map(name => {
|
||||
name = name.replace('packet_', '')
|
||||
return [name, name]
|
||||
}))
|
||||
window.debugClientPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toServer.types).map(name => {
|
||||
name = name.replace('packet_', '')
|
||||
return [name, name]
|
||||
}))
|
||||
})
|
||||
|
||||
window.Vec3 = Vec3
|
||||
window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
|
||||
const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z)
|
||||
|
|
|
|||
|
|
@ -246,22 +246,29 @@ customEvents.on('gameLoaded', () => {
|
|||
}
|
||||
}
|
||||
// even if not found, still record to cache
|
||||
void getThreeJsRendererMethods()?.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
|
||||
void getThreeJsRendererMethods()!.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
|
||||
} catch (err) {
|
||||
console.error('Error decoding player texture:', err)
|
||||
reportError(new Error('Error applying skin texture:', { cause: err }))
|
||||
}
|
||||
}
|
||||
|
||||
bot.on('playerJoined', updateSkin)
|
||||
bot.on('playerUpdated', updateSkin)
|
||||
for (const entity of Object.values(bot.players)) {
|
||||
updateSkin(entity)
|
||||
}
|
||||
|
||||
bot.on('teamUpdated', (team: Team) => {
|
||||
const teamUpdated = (team: Team) => {
|
||||
for (const entity of Object.values(bot.entities)) {
|
||||
if (entity.type === 'player' && entity.username && team.members.includes(entity.username) || entity.uuid && team.members.includes(entity.uuid)) {
|
||||
bot.emit('entityUpdate', entity)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
bot.on('teamUpdated', teamUpdated)
|
||||
for (const team of Object.values(bot.teams)) {
|
||||
teamUpdated(team)
|
||||
}
|
||||
|
||||
const updateEntityNameTags = (team: Team) => {
|
||||
for (const entity of Object.values(bot.entities)) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import PrismarineChatLoader from 'prismarine-chat'
|
|||
import * as nbt from 'prismarine-nbt'
|
||||
import { BlockModel } from 'mc-assets'
|
||||
import { renderSlot } from 'renderer/viewer/three/renderSlot'
|
||||
import { loadSkinFromUsername } from 'renderer/viewer/lib/utils/skins'
|
||||
import Generic95 from '../assets/generic_95.png'
|
||||
import { appReplacableResources } from './generated/resources'
|
||||
import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState'
|
||||
|
|
@ -23,6 +24,7 @@ import { getItemDescription } from './itemsDescriptions'
|
|||
import { MessageFormatPart } from './chatUtils'
|
||||
import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items'
|
||||
import { playerState } from './mineflayer/playerState'
|
||||
import { modelViewerState } from './react/OverlayModelViewer'
|
||||
|
||||
const loadedImagesCache = new Map<string, HTMLImageElement | ImageBitmap>()
|
||||
const cleanLoadedImagesCache = () => {
|
||||
|
|
@ -40,6 +42,34 @@ export const jeiCustomCategories = proxy({
|
|||
value: [] as Array<{ id: string, categoryTitle: string, items: any[] }>
|
||||
})
|
||||
|
||||
let remotePlayerSkin: string | undefined | Promise<string>
|
||||
|
||||
export const showInventoryPlayer = () => {
|
||||
modelViewerState.model = {
|
||||
positioning: {
|
||||
windowWidth: 176,
|
||||
windowHeight: 166,
|
||||
x: 25,
|
||||
y: 8,
|
||||
width: 50,
|
||||
height: 70,
|
||||
scaled: true,
|
||||
onlyInitialScale: true,
|
||||
followCursor: true,
|
||||
},
|
||||
// models: ['https://bucket.mcraft.fun/sitarbuckss.glb'],
|
||||
// debug: true,
|
||||
steveModelSkin: appViewer.playerState.reactive.playerSkin ?? (typeof remotePlayerSkin === 'string' ? remotePlayerSkin : ''),
|
||||
}
|
||||
if (remotePlayerSkin === undefined && !appViewer.playerState.reactive.playerSkin) {
|
||||
remotePlayerSkin = loadSkinFromUsername(bot.username, 'skin').then(a => {
|
||||
setTimeout(() => { showInventoryPlayer() }, 0) // todo patch instead and make reactive
|
||||
remotePlayerSkin = a ?? ''
|
||||
return remotePlayerSkin
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const onGameLoad = () => {
|
||||
version = bot.version
|
||||
|
||||
|
|
@ -392,7 +422,12 @@ const openWindow = (type: string | undefined, title: string | any = undefined) =
|
|||
miscUiState.displaySearchInput = false
|
||||
destroyFn()
|
||||
skipClosePacketSending = false
|
||||
|
||||
modelViewerState.model = undefined
|
||||
})
|
||||
if (type === undefined) {
|
||||
showInventoryPlayer()
|
||||
}
|
||||
cleanLoadedImagesCache()
|
||||
const inv = openItemsCanvas(type)
|
||||
inv.canvasManager.children[0].mobileHelpers = miscUiState.currentTouch
|
||||
|
|
@ -435,6 +470,7 @@ const openWindow = (type: string | undefined, title: string | any = undefined) =
|
|||
const isRightClick = type === 'rightclick'
|
||||
const isLeftClick = type === 'leftclick'
|
||||
if (isLeftClick || isRightClick) {
|
||||
modelViewerState.model = undefined
|
||||
inv.canvasManager.children[0].showRecipesOrUsages(isLeftClick, item)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -466,6 +502,7 @@ const openWindow = (type: string | undefined, title: string | any = undefined) =
|
|||
if (freeSlot === null) return
|
||||
void bot.creative.setInventorySlot(freeSlot, item)
|
||||
} else {
|
||||
modelViewerState.model = undefined
|
||||
inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item], true)[0])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,12 @@ class CustomDuplex extends Duplex {
|
|||
}
|
||||
|
||||
export const getWebsocketStream = async (host: string) => {
|
||||
const baseProtocol = location.protocol === 'https:' ? 'wss' : host.startsWith('ws://') ? 'ws' : 'wss'
|
||||
const baseProtocol = host.startsWith('ws://') ? 'ws' : 'wss'
|
||||
const hostClean = host.replace('ws://', '').replace('wss://', '')
|
||||
const ws = new WebSocket(`${baseProtocol}://${hostClean}`)
|
||||
const hostURL = new URL(`${baseProtocol}://${hostClean}`)
|
||||
const hostParams = hostURL.searchParams
|
||||
hostParams.append('client_mcraft', '')
|
||||
const ws = new WebSocket(`${baseProtocol}://${hostURL.host}${hostURL.pathname}?${hostParams.toString()}`)
|
||||
const clientDuplex = new CustomDuplex(undefined, data => {
|
||||
ws.send(data)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -480,6 +480,24 @@ export const guiOptionsScheme: {
|
|||
],
|
||||
sound: [
|
||||
{ volume: {} },
|
||||
{
|
||||
custom () {
|
||||
return <OptionSlider
|
||||
valueOverride={options.enableMusic ? undefined : 0}
|
||||
onChange={(value) => {
|
||||
options.musicVolume = value
|
||||
}}
|
||||
item={{
|
||||
type: 'slider',
|
||||
id: 'musicVolume',
|
||||
text: 'Music Volume',
|
||||
min: 0,
|
||||
max: 100,
|
||||
unit: '%',
|
||||
}}
|
||||
/>
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button label='Sound Muffler' onClick={() => showModal({ reactType: 'sound-muffler' })} inScreen />
|
||||
|
|
@ -550,6 +568,16 @@ export const guiOptionsScheme: {
|
|||
return <Category>Server Connection</Category>
|
||||
},
|
||||
},
|
||||
{
|
||||
saveLoginPassword: {
|
||||
tooltip: 'Controls whether to save login passwords for servers in this browser memory.',
|
||||
values: [
|
||||
'prompt',
|
||||
'always',
|
||||
'never'
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
const { serversAutoVersionSelect } = useSnapshot(options)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export const startLocalReplayServer = (contents: string) => {
|
|||
const server = createServer({
|
||||
Server: LocalServer as any,
|
||||
version: header.minecraftVersion,
|
||||
keepAlive: false,
|
||||
'online-mode': false
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
}
|
||||
|
||||
const displayConnectButton = qsParamIp
|
||||
const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg']
|
||||
const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg', 'wss://play.webmc.fun']
|
||||
// pick random example
|
||||
const example = serverExamples[Math.floor(Math.random() * serverExamples.length)]
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,9 @@ export default ({
|
|||
const chatInput = useRef<HTMLInputElement>(null!)
|
||||
const chatMessages = useRef<HTMLDivElement>(null)
|
||||
const chatHistoryPos = useRef(sendHistoryRef.current.length)
|
||||
const commandHistoryPos = useRef(0)
|
||||
const inputCurrentlyEnteredValue = useRef('')
|
||||
const commandHistoryRef = useRef(sendHistoryRef.current.filter((msg: string) => msg.startsWith('/')))
|
||||
|
||||
const { scrollToBottom, isAtBottom, wasAtBottom, currentlyAtBottom } = useScrollBehavior(chatMessages, { messages, opened })
|
||||
const [rightNowAtBottom, setRightNowAtBottom] = useState(false)
|
||||
|
|
@ -142,6 +144,9 @@ export default ({
|
|||
sendHistoryRef.current = newHistory
|
||||
window.sessionStorage.chatHistory = JSON.stringify(newHistory)
|
||||
chatHistoryPos.current = newHistory.length
|
||||
// Update command history (only messages starting with /)
|
||||
commandHistoryRef.current = newHistory.filter((msg: string) => msg.startsWith('/'))
|
||||
commandHistoryPos.current = commandHistoryRef.current.length
|
||||
}
|
||||
|
||||
const acceptComplete = (item: string) => {
|
||||
|
|
@ -180,6 +185,21 @@ export default ({
|
|||
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
|
||||
}
|
||||
|
||||
const handleCommandArrowUp = () => {
|
||||
if (commandHistoryPos.current === 0 || commandHistoryRef.current.length === 0) return
|
||||
if (commandHistoryPos.current === commandHistoryRef.current.length) { // started navigating command history
|
||||
inputCurrentlyEnteredValue.current = chatInput.current.value
|
||||
}
|
||||
commandHistoryPos.current--
|
||||
updateInputValue(commandHistoryRef.current[commandHistoryPos.current] || '')
|
||||
}
|
||||
|
||||
const handleCommandArrowDown = () => {
|
||||
if (commandHistoryPos.current === commandHistoryRef.current.length) return
|
||||
commandHistoryPos.current++
|
||||
updateInputValue(commandHistoryRef.current[commandHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
|
||||
}
|
||||
|
||||
const auxInputFocus = (direction: 'up' | 'down') => {
|
||||
chatInput.current.focus()
|
||||
if (direction === 'up') {
|
||||
|
|
@ -203,6 +223,7 @@ export default ({
|
|||
updateInputValue(chatInputValueGlobal.value)
|
||||
chatInputValueGlobal.value = ''
|
||||
chatHistoryPos.current = sendHistoryRef.current.length
|
||||
commandHistoryPos.current = commandHistoryRef.current.length
|
||||
if (!usingTouch) {
|
||||
chatInput.current.focus()
|
||||
}
|
||||
|
|
@ -524,9 +545,19 @@ export default ({
|
|||
onBlur={() => setIsInputFocused(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code === 'ArrowUp') {
|
||||
handleArrowUp()
|
||||
if (e.altKey) {
|
||||
handleCommandArrowUp()
|
||||
e.preventDefault()
|
||||
} else {
|
||||
handleArrowUp()
|
||||
}
|
||||
} else if (e.code === 'ArrowDown') {
|
||||
handleArrowDown()
|
||||
if (e.altKey) {
|
||||
handleCommandArrowDown()
|
||||
e.preventDefault()
|
||||
} else {
|
||||
handleArrowDown()
|
||||
}
|
||||
}
|
||||
if (e.code === 'Tab') {
|
||||
if (completionItemsSource.length) {
|
||||
|
|
|
|||
|
|
@ -73,16 +73,28 @@ export default () => {
|
|||
}
|
||||
|
||||
const builtinHandled = tryHandleBuiltinCommand(message)
|
||||
if (getServerIndex() !== undefined && (message.startsWith('/login') || message.startsWith('/register'))) {
|
||||
showNotification('Click here to save your password in browser for auto-login', undefined, false, undefined, () => {
|
||||
if (getServerIndex() !== undefined && (message.startsWith('/login') || message.startsWith('/register')) && options.saveLoginPassword !== 'never') {
|
||||
const savePassword = () => {
|
||||
let hadPassword = false
|
||||
updateLoadedServerData((server) => {
|
||||
server.autoLogin ??= {}
|
||||
const password = message.split(' ')[1]
|
||||
hadPassword = !!server.autoLogin[bot.username]
|
||||
server.autoLogin[bot.username] = password
|
||||
return { ...server }
|
||||
})
|
||||
hideNotification()
|
||||
})
|
||||
if (options.saveLoginPassword === 'always') {
|
||||
const message = hadPassword ? 'Password updated in browser for auto-login' : 'Password saved in browser for auto-login'
|
||||
showNotification(message, undefined, false, undefined)
|
||||
} else {
|
||||
hideNotification()
|
||||
}
|
||||
}
|
||||
if (options.saveLoginPassword === 'prompt') {
|
||||
showNotification('Click here to save your password in browser for auto-login', undefined, false, undefined, savePassword)
|
||||
} else {
|
||||
savePassword()
|
||||
}
|
||||
notificationProxy.id = 'auto-login'
|
||||
const listener = () => {
|
||||
hideNotification()
|
||||
|
|
|
|||
|
|
@ -161,7 +161,15 @@ export const OptionButton = ({ item, onClick, valueText, cacheKey }: {
|
|||
/>
|
||||
}
|
||||
|
||||
export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slider' }> }) => {
|
||||
export const OptionSlider = ({
|
||||
item,
|
||||
onChange,
|
||||
valueOverride
|
||||
}: {
|
||||
item: Extract<OptionMeta, { type: 'slider' }>
|
||||
onChange?: (value: number) => void
|
||||
valueOverride?: number
|
||||
}) => {
|
||||
const { disabledBecauseOfSetting } = useCommonComponentsProps(item)
|
||||
|
||||
const optionValue = useSnapshot(options)[item.id!]
|
||||
|
|
@ -174,7 +182,7 @@ export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slid
|
|||
return (
|
||||
<Slider
|
||||
label={item.text!}
|
||||
value={options[item.id!]}
|
||||
value={valueOverride ?? options[item.id!]}
|
||||
data-setting={item.id}
|
||||
disabledReason={isLocked(item) ? 'qs' : disabledBecauseOfSetting ? `Disabled because ${item.disableIf![0]} is ${item.disableIf![1]}` : item.disabledReason}
|
||||
min={item.min}
|
||||
|
|
@ -184,6 +192,7 @@ export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slid
|
|||
updateOnDragEnd={item.delayApply}
|
||||
updateValue={(value) => {
|
||||
options[item.id!] = value
|
||||
onChange?.(value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
554
src/react/OverlayModelViewer.tsx
Normal file
554
src/react/OverlayModelViewer.tsx
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
import { proxy, useSnapshot, subscribe } from 'valtio'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { applySkinToPlayerObject, createPlayerObject, PlayerObjectType } from '../../renderer/viewer/lib/createPlayerObject'
|
||||
import { currentScaling } from '../scaleInterface'
|
||||
import { activeModalStack } from '../globalState'
|
||||
|
||||
THREE.ColorManagement.enabled = false
|
||||
|
||||
export const modelViewerState = proxy({
|
||||
model: undefined as undefined | {
|
||||
models?: string[] // Array of model URLs (URL itself is the cache key)
|
||||
steveModelSkin?: string
|
||||
debug?: boolean
|
||||
// absolute positioning
|
||||
positioning: {
|
||||
windowWidth: number
|
||||
windowHeight: number
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
scaled?: boolean
|
||||
onlyInitialScale?: boolean
|
||||
followCursor?: boolean
|
||||
}
|
||||
modelCustomization?: { [modelUrl: string]: { color?: string, opacity?: number, metalness?: number, roughness?: number } }
|
||||
resetRotationOnReleae?: boolean
|
||||
continiousRender?: boolean
|
||||
alwaysRender?: boolean
|
||||
}
|
||||
})
|
||||
globalThis.modelViewerState = modelViewerState
|
||||
|
||||
// Global debug function to get camera and model values
|
||||
globalThis.getModelViewerValues = () => {
|
||||
const scene = globalThis.sceneRef?.current
|
||||
if (!scene) return null
|
||||
|
||||
const { camera, playerObject } = scene
|
||||
if (!playerObject) return null
|
||||
|
||||
const wrapper = playerObject.parent
|
||||
if (!wrapper) return null
|
||||
|
||||
const box = new THREE.Box3().setFromObject(wrapper)
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
|
||||
return {
|
||||
camera: {
|
||||
position: camera.position.clone(),
|
||||
fov: camera.fov,
|
||||
aspect: camera.aspect
|
||||
},
|
||||
model: {
|
||||
position: wrapper.position.clone(),
|
||||
rotation: wrapper.rotation.clone(),
|
||||
scale: wrapper.scale.clone(),
|
||||
size,
|
||||
center
|
||||
},
|
||||
cursor: {
|
||||
position: globalThis.cursorPosition || { x: 0, y: 0 },
|
||||
normalized: globalThis.cursorPosition ? {
|
||||
x: globalThis.cursorPosition.x * 2 - 1,
|
||||
y: globalThis.cursorPosition.y * 2 - 1
|
||||
} : { x: 0, y: 0 }
|
||||
},
|
||||
visibleArea: {
|
||||
height: 2 * Math.tan(camera.fov * Math.PI / 180 / 2) * camera.position.z,
|
||||
width: 2 * Math.tan(camera.fov * Math.PI / 180 / 2) * camera.position.z * camera.aspect
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(activeModalStack, () => {
|
||||
if (!modelViewerState.model || !modelViewerState.model?.alwaysRender) {
|
||||
return
|
||||
}
|
||||
if (activeModalStack.length === 0) {
|
||||
modelViewerState.model = undefined
|
||||
}
|
||||
})
|
||||
|
||||
export default () => {
|
||||
const { model } = useSnapshot(modelViewerState)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const sceneRef = useRef<{
|
||||
scene: THREE.Scene
|
||||
camera: THREE.PerspectiveCamera
|
||||
renderer: THREE.WebGLRenderer
|
||||
controls: OrbitControls
|
||||
playerObject?: PlayerObjectType
|
||||
dispose: () => void
|
||||
}>()
|
||||
const initialScale = useMemo(() => {
|
||||
return currentScaling.scale
|
||||
}, [])
|
||||
globalThis.sceneRef = sceneRef
|
||||
|
||||
// Cursor following state
|
||||
const cursorPosition = useRef({ x: 0, y: 0 })
|
||||
const isFollowingCursor = useRef(false)
|
||||
|
||||
// Model management state
|
||||
const loadedModels = useRef<Map<string, THREE.Object3D>>(new Map())
|
||||
const modelLoaders = useRef<Map<string, GLTFLoader | OBJLoader>>(new Map())
|
||||
|
||||
// Model management functions
|
||||
const loadModel = (modelUrl: string) => {
|
||||
if (loadedModels.current.has(modelUrl)) return // Already loaded
|
||||
|
||||
const isGLTF = modelUrl.toLowerCase().endsWith('.gltf') || modelUrl.toLowerCase().endsWith('.glb')
|
||||
const loader = isGLTF ? new GLTFLoader() : new OBJLoader()
|
||||
modelLoaders.current.set(modelUrl, loader)
|
||||
|
||||
const onLoad = (object: THREE.Object3D) => {
|
||||
// Apply customization if available and enable shadows
|
||||
const customization = model?.modelCustomization?.[modelUrl]
|
||||
object.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
// Enable shadow casting and receiving for all meshes
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
|
||||
if (child.material && customization) {
|
||||
const material = child.material as THREE.MeshStandardMaterial
|
||||
if (customization.color) {
|
||||
material.color.setHex(parseInt(customization.color.replace('#', ''), 16))
|
||||
}
|
||||
if (customization.opacity !== undefined) {
|
||||
material.opacity = customization.opacity
|
||||
material.transparent = customization.opacity < 1
|
||||
}
|
||||
if (customization.metalness !== undefined) {
|
||||
material.metalness = customization.metalness
|
||||
}
|
||||
if (customization.roughness !== undefined) {
|
||||
material.roughness = customization.roughness
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Center and scale model
|
||||
const box = new THREE.Box3().setFromObject(object)
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
const maxDim = Math.max(size.x, size.y, size.z)
|
||||
const scale = 2 / maxDim
|
||||
object.scale.setScalar(scale)
|
||||
object.position.sub(center.multiplyScalar(scale))
|
||||
|
||||
// Store the model using URL as key
|
||||
loadedModels.current.set(modelUrl, object)
|
||||
sceneRef.current?.scene.add(object)
|
||||
|
||||
// Trigger render
|
||||
if (sceneRef.current) {
|
||||
setTimeout(() => {
|
||||
const render = () => sceneRef.current?.renderer.render(sceneRef.current.scene, sceneRef.current.camera)
|
||||
render()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
if (isGLTF) {
|
||||
(loader as GLTFLoader).load(modelUrl, (gltf) => {
|
||||
onLoad(gltf.scene)
|
||||
})
|
||||
} else {
|
||||
(loader as OBJLoader).load(modelUrl, onLoad)
|
||||
}
|
||||
}
|
||||
|
||||
const removeModel = (modelUrl: string) => {
|
||||
const model = loadedModels.current.get(modelUrl)
|
||||
if (model) {
|
||||
sceneRef.current?.scene.remove(model)
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (child.material) {
|
||||
if (Array.isArray(child.material)) {
|
||||
for (const mat of child.material) {
|
||||
mat.dispose()
|
||||
}
|
||||
} else {
|
||||
child.material.dispose()
|
||||
}
|
||||
}
|
||||
if (child.geometry) {
|
||||
child.geometry.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
loadedModels.current.delete(modelUrl)
|
||||
}
|
||||
modelLoaders.current.delete(modelUrl)
|
||||
}
|
||||
|
||||
// Subscribe to model changes
|
||||
useEffect(() => {
|
||||
if (!modelViewerState.model?.models) return
|
||||
|
||||
const modelsChanged = () => {
|
||||
const currentModels = modelViewerState.model?.models || []
|
||||
const currentModelUrls = new Set(currentModels)
|
||||
const loadedModelUrls = new Set(loadedModels.current.keys())
|
||||
|
||||
// Remove models that are no longer in the state
|
||||
for (const modelUrl of loadedModelUrls) {
|
||||
if (!currentModelUrls.has(modelUrl)) {
|
||||
removeModel(modelUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// Add new models
|
||||
for (const modelUrl of currentModels) {
|
||||
if (!loadedModelUrls.has(modelUrl)) {
|
||||
loadModel(modelUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
const unsubscribe = subscribe(modelViewerState.model.models, modelsChanged)
|
||||
|
||||
let unmounted = false
|
||||
setTimeout(() => {
|
||||
if (unmounted) return
|
||||
modelsChanged()
|
||||
})
|
||||
|
||||
return () => {
|
||||
unmounted = true
|
||||
unsubscribe?.()
|
||||
}
|
||||
}, [model?.models])
|
||||
|
||||
useEffect(() => {
|
||||
if (!model || !containerRef.current) return
|
||||
|
||||
// Setup scene
|
||||
const scene = new THREE.Scene()
|
||||
scene.background = null // Transparent background
|
||||
|
||||
// Setup camera with optimal settings for player model viewing
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
50, // Reduced FOV for better model viewing
|
||||
model.positioning.width / model.positioning.height,
|
||||
0.1,
|
||||
1000
|
||||
)
|
||||
camera.position.set(0, 0, 3) // Position camera to view player model optimally
|
||||
|
||||
// Setup renderer with pixel density awareness
|
||||
const renderer = new THREE.WebGLRenderer({ alpha: true })
|
||||
let scale = window.devicePixelRatio || 1
|
||||
if (modelViewerState.model?.positioning.scaled) {
|
||||
scale *= currentScaling.scale
|
||||
}
|
||||
renderer.setPixelRatio(scale)
|
||||
renderer.setSize(model.positioning.width, model.positioning.height)
|
||||
|
||||
// Enable shadow rendering for depth and realism
|
||||
renderer.shadowMap.enabled = true
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap // Soft shadows for better quality
|
||||
renderer.shadowMap.autoUpdate = true
|
||||
|
||||
containerRef.current.appendChild(renderer.domElement)
|
||||
|
||||
// Setup controls
|
||||
const controls = new OrbitControls(camera, renderer.domElement)
|
||||
// controls.enableZoom = false
|
||||
// controls.enablePan = false
|
||||
controls.minPolarAngle = Math.PI / 2 // Lock vertical rotation
|
||||
controls.maxPolarAngle = Math.PI / 2
|
||||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.05
|
||||
|
||||
// Add ambient light for overall illumination
|
||||
const ambientLight = new THREE.AmbientLight(0xff_ff_ff, 0.4) // Reduced intensity to allow shadows
|
||||
scene.add(ambientLight)
|
||||
|
||||
// Add directional light for shadows and depth (similar to Minecraft inventory lighting)
|
||||
const directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.6)
|
||||
directionalLight.position.set(2, 2, 2) // Position light from top-right-front
|
||||
directionalLight.target.position.set(0, 0, 0) // Point towards center of scene
|
||||
|
||||
// Configure shadow properties for optimal quality
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.shadow.mapSize.width = 2048 // High resolution shadow map
|
||||
directionalLight.shadow.mapSize.height = 2048
|
||||
directionalLight.shadow.camera.near = 0.1
|
||||
directionalLight.shadow.camera.far = 10
|
||||
directionalLight.shadow.camera.left = -3
|
||||
directionalLight.shadow.camera.right = 3
|
||||
directionalLight.shadow.camera.top = 3
|
||||
directionalLight.shadow.camera.bottom = -3
|
||||
directionalLight.shadow.bias = -0.0001 // Reduce shadow acne
|
||||
|
||||
scene.add(directionalLight)
|
||||
scene.add(directionalLight.target)
|
||||
|
||||
// Cursor following function
|
||||
const updatePlayerLookAt = () => {
|
||||
if (!isFollowingCursor.current || !sceneRef.current?.playerObject) return
|
||||
|
||||
const { playerObject } = sceneRef.current
|
||||
const { x, y } = cursorPosition.current
|
||||
|
||||
// Convert 0-1 cursor position to normalized coordinates (-1 to 1)
|
||||
const normalizedX = x * 2 - 1
|
||||
const normalizedY = y * 2 - 1 // Inverted: top of screen = negative pitch, bottom = positive pitch
|
||||
|
||||
// Calculate head rotation based on cursor position
|
||||
// Limit head movement to realistic angles
|
||||
const maxHeadYaw = Math.PI / 3 // 60 degrees
|
||||
const maxHeadPitch = Math.PI / 4 // 45 degrees
|
||||
|
||||
const headYaw = normalizedX * maxHeadYaw
|
||||
const headPitch = normalizedY * maxHeadPitch
|
||||
|
||||
// Apply head rotation with smooth interpolation
|
||||
const lerpFactor = 0.1 // Smooth interpolation factor
|
||||
playerObject.skin.head.rotation.y = THREE.MathUtils.lerp(
|
||||
playerObject.skin.head.rotation.y,
|
||||
headYaw,
|
||||
lerpFactor
|
||||
)
|
||||
playerObject.skin.head.rotation.x = THREE.MathUtils.lerp(
|
||||
playerObject.skin.head.rotation.x,
|
||||
headPitch,
|
||||
lerpFactor
|
||||
)
|
||||
|
||||
// Apply slight body rotation for more natural movement
|
||||
const bodyYaw = headYaw * 0.3 // Body follows head but with less rotation
|
||||
playerObject.rotation.y = THREE.MathUtils.lerp(
|
||||
playerObject.rotation.y,
|
||||
bodyYaw,
|
||||
lerpFactor * 0.5 // Slower body movement
|
||||
)
|
||||
|
||||
render()
|
||||
}
|
||||
|
||||
// Render function
|
||||
const render = () => {
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
// Setup animation/render strategy
|
||||
if (model.continiousRender) {
|
||||
// Continuous animation loop
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate)
|
||||
render()
|
||||
}
|
||||
animate()
|
||||
} else {
|
||||
// Render only on camera movement
|
||||
controls.addEventListener('change', render)
|
||||
// Initial render
|
||||
render()
|
||||
// Render after model loads
|
||||
if (model.steveModelSkin !== undefined) {
|
||||
// Create player model
|
||||
const { playerObject, wrapper } = createPlayerObject({
|
||||
scale: 1 // Start with base scale, will adjust below
|
||||
})
|
||||
|
||||
// Enable shadows for player object
|
||||
wrapper.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate proper scale and positioning for camera view
|
||||
const box = new THREE.Box3().setFromObject(wrapper)
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
|
||||
// Calculate scale to fit within camera view (considering FOV and distance)
|
||||
const cameraDistance = camera.position.z
|
||||
const fov = camera.fov * Math.PI / 180 // Convert to radians
|
||||
const visibleHeight = 2 * Math.tan(fov / 2) * cameraDistance
|
||||
const visibleWidth = visibleHeight * (model.positioning.width / model.positioning.height)
|
||||
|
||||
const scaleFactor = Math.min(
|
||||
(visibleHeight) / size.y,
|
||||
(visibleWidth) / size.x
|
||||
)
|
||||
|
||||
wrapper.scale.multiplyScalar(scaleFactor)
|
||||
|
||||
// Center the player object
|
||||
wrapper.position.sub(center.multiplyScalar(scaleFactor))
|
||||
|
||||
// Rotate to face camera (remove the default 180° rotation)
|
||||
wrapper.rotation.set(0, 0, 0)
|
||||
|
||||
scene.add(wrapper)
|
||||
sceneRef.current = {
|
||||
...sceneRef.current!,
|
||||
playerObject
|
||||
}
|
||||
|
||||
void applySkinToPlayerObject(playerObject, model.steveModelSkin).then(() => {
|
||||
setTimeout(render, 0)
|
||||
})
|
||||
|
||||
// Set up cursor following if enabled
|
||||
if (model.positioning.followCursor) {
|
||||
isFollowingCursor.current = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Window cursor tracking for followCursor
|
||||
let lastCursorUpdate = 0
|
||||
let waitingRender = false
|
||||
const handleWindowPointerMove = (event: PointerEvent) => {
|
||||
if (!model.positioning.followCursor) return
|
||||
|
||||
// Track cursor position as 0-1 across the entire window
|
||||
const newPosition = {
|
||||
x: event.clientX / window.innerWidth,
|
||||
y: event.clientY / window.innerHeight
|
||||
}
|
||||
cursorPosition.current = newPosition
|
||||
globalThis.cursorPosition = newPosition // Expose for debug
|
||||
lastCursorUpdate = Date.now()
|
||||
updatePlayerLookAt()
|
||||
if (!waitingRender) {
|
||||
requestAnimationFrame(() => {
|
||||
render()
|
||||
waitingRender = false
|
||||
})
|
||||
waitingRender = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add window event listeners
|
||||
if (model.positioning.followCursor) {
|
||||
window.addEventListener('pointermove', handleWindowPointerMove)
|
||||
isFollowingCursor.current = true
|
||||
}
|
||||
|
||||
// Store refs for cleanup
|
||||
sceneRef.current = {
|
||||
...sceneRef.current!,
|
||||
scene,
|
||||
camera,
|
||||
renderer,
|
||||
controls,
|
||||
dispose () {
|
||||
if (!model.continiousRender) {
|
||||
controls.removeEventListener('change', render)
|
||||
}
|
||||
if (model.positioning.followCursor) {
|
||||
window.removeEventListener('pointermove', handleWindowPointerMove)
|
||||
}
|
||||
|
||||
// Clean up loaded models
|
||||
for (const [modelUrl, model] of loadedModels.current) {
|
||||
scene.remove(model)
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (child.material) {
|
||||
if (Array.isArray(child.material)) {
|
||||
for (const mat of child.material) {
|
||||
mat.dispose()
|
||||
}
|
||||
} else {
|
||||
child.material.dispose()
|
||||
}
|
||||
}
|
||||
if (child.geometry) {
|
||||
child.geometry.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
loadedModels.current.clear()
|
||||
modelLoaders.current.clear()
|
||||
|
||||
const playerObject = sceneRef.current?.playerObject
|
||||
if (playerObject?.skin.map) {
|
||||
(playerObject.skin.map as unknown as THREE.Texture).dispose()
|
||||
}
|
||||
renderer.dispose()
|
||||
renderer.domElement?.remove()
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
sceneRef.current?.dispose()
|
||||
}
|
||||
}, [model])
|
||||
|
||||
if (!model) return null
|
||||
|
||||
const { x, y, width, height, scaled, onlyInitialScale } = model.positioning
|
||||
const { windowWidth } = model.positioning
|
||||
const { windowHeight } = model.positioning
|
||||
const scaleValue = onlyInitialScale ? initialScale : 'var(--guiScale)'
|
||||
|
||||
return (
|
||||
<div
|
||||
className='overlay-model-viewer-container'
|
||||
style={{
|
||||
zIndex: 100,
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
width: '100dvw',
|
||||
height: '100dvh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
transform: scaled ? `scale(${scaleValue})` : 'none',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='overlay-model-viewer-window'
|
||||
style={{
|
||||
width: windowWidth,
|
||||
height: windowHeight,
|
||||
position: 'relative',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='overlay-model-viewer'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: x,
|
||||
top: y,
|
||||
width,
|
||||
height,
|
||||
pointerEvents: 'auto',
|
||||
backgroundColor: model.debug ? 'red' : undefined,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -119,6 +119,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
...serversListProvided,
|
||||
...(customServersList ? [] : (miscUiState.appConfig?.promoteServers ?? [])).map((server): StoreServerItem => ({
|
||||
ip: server.ip,
|
||||
name: server.name,
|
||||
versionOverride: server.version,
|
||||
description: server.description,
|
||||
isRecommended: true
|
||||
|
|
@ -167,6 +168,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
console.log('pingResult.fullInfo.description', pingResult.fullInfo.description)
|
||||
data = {
|
||||
formattedText: pingResult.fullInfo.description,
|
||||
icon: pingResult.fullInfo.favicon,
|
||||
textNameRight: `ws ${pingResult.latency}ms`,
|
||||
textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`,
|
||||
offline: false
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ import FullscreenTime from './react/FullscreenTime'
|
|||
import StorageConflictModal from './react/StorageConflictModal'
|
||||
import FireRenderer from './react/FireRenderer'
|
||||
import MonacoEditor from './react/MonacoEditor'
|
||||
import OverlayModelViewer from './react/OverlayModelViewer'
|
||||
|
||||
const isFirefox = ua.getBrowser().name === 'Firefox'
|
||||
if (isFirefox) {
|
||||
|
|
@ -259,6 +260,7 @@ const App = () => {
|
|||
</div>
|
||||
<div />
|
||||
<DebugEdges />
|
||||
<OverlayModelViewer />
|
||||
<MonacoEditor />
|
||||
<DebugResponseTimeIndicator />
|
||||
</RobustPortal>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
z-index: 12;
|
||||
/* Account for GUI scaling */
|
||||
width: calc(100dvw / var(--guiScale, 1));
|
||||
height: calc(100dvh / var(--guiScale, 1));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ class MusicSystem {
|
|||
private currentMusic: string | null = null
|
||||
|
||||
async playMusic (url: string, musicVolume = 1) {
|
||||
if (!options.enableMusic || this.currentMusic) return
|
||||
if (!options.enableMusic || this.currentMusic || options.musicVolume === 0) return
|
||||
|
||||
try {
|
||||
const { onEnded } = await loadOrPlaySound(url, 0.5 * musicVolume, 5000) ?? {}
|
||||
const { onEnded } = await loadOrPlaySound(url, musicVolume, 5000, undefined, true) ?? {}
|
||||
|
||||
if (!onEnded) return
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { subscribeKey } from 'valtio/utils'
|
||||
import { isMobile } from 'renderer/viewer/lib/simpleUtils'
|
||||
import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter'
|
||||
import { setSkinsConfig } from 'renderer/viewer/lib/utils/skins'
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import { reloadChunks } from './utils'
|
||||
import { miscUiState } from './globalState'
|
||||
|
|
@ -97,6 +98,8 @@ export const watchOptionsAfterViewerInit = () => {
|
|||
appViewer.inWorldRenderingConfig.highlightBlockColor = o.highlightBlockColor
|
||||
appViewer.inWorldRenderingConfig._experimentalSmoothChunkLoading = o.rendererSharedOptions._experimentalSmoothChunkLoading
|
||||
appViewer.inWorldRenderingConfig._renderByChunks = o.rendererSharedOptions._renderByChunks
|
||||
|
||||
setSkinsConfig({ apiEnabled: o.loadPlayerSkins })
|
||||
})
|
||||
|
||||
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
|
||||
|
|
@ -116,6 +119,10 @@ export const watchOptionsAfterViewerInit = () => {
|
|||
appViewer.inWorldRenderingConfig.starfield = o.starfieldRendering
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
appViewer.inWorldRenderingConfig.defaultSkybox = o.defaultSkybox
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
// appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue