diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..bf554f516201293b4a31818d365cc979225dd190
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,5 @@
+# we dont want default config to be loaded in the dockerfile, but rather using a volume
+config.json
+# build stuff
+node_modules
+public
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..0d8ec3ae2959dda4e51f361375844a2a2d87e5ef 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
+assets/invsprite.png filter=lfs diff=lfs merge=lfs -text
+extra-textures/background/panorama_5.png filter=lfs diff=lfs merge=lfs -text
+extra-textures/loading.psd filter=lfs diff=lfs merge=lfs -text
+screenshot.png filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..13e47cf60ad64e41182ccab6c8551e1872994bff
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules/
+package-lock.json
+.vscode
+public
+.DS_Store
+.idea/
+*.iml
\ No newline at end of file
diff --git a/.gitpod b/.gitpod
new file mode 100644
index 0000000000000000000000000000000000000000..9ea6b0adb9c442e6446819dbf55fa28f34a8477f
--- /dev/null
+++ b/.gitpod
@@ -0,0 +1,2 @@
+tasks:
+- command: npm install
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000000000000000000000000000000000000..e1976198cbd14e5f2f25349df6d36f148d988094
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,3 @@
+node_modules/
+package-lock.json
+.vscode
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000000000000000000000000000000000000..094e38421d4085d75e1223c8c02d400bd687613c
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..70d9829f7c32ade1f56cb84b78f0b34b68edc083
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,9 @@
+FROM node:14-alpine
+# Without git installing the npm packages fails
+RUN apk add git
+RUN mkdir /app
+WORKDIR /app
+COPY . /app
+RUN npm install
+RUN npm run build
+ENTRYPOINT ["npm", "run", "prod-start"]
diff --git a/HISTORY.md b/HISTORY.md
new file mode 100644
index 0000000000000000000000000000000000000000..62529bbe8bbac20b0bb703dba8e291b1305dc0b2
--- /dev/null
+++ b/HISTORY.md
@@ -0,0 +1,43 @@
+## History
+
+### 1.6.0
+* [bump pviewer](https://github.com/PrismarineJS/prismarine-web-client/commit/901242b33151ad70d651e18416bd67b9949cc117) (thanks @rom1504)
+* [Remove incorrect signals + change server. (#361)](https://github.com/PrismarineJS/prismarine-web-client/commit/a243144a9802e569ef2ee31dce5379c2f2ba3951) (thanks @rom1504)
+* [Create commands.yml](https://github.com/PrismarineJS/prismarine-web-client/commit/17548409c1fc881a899dcad0a67a7a1a0ecf9157) (thanks @rom1504)
+* [avoid using @azure/msal-node (#358)](https://github.com/PrismarineJS/prismarine-web-client/commit/d441efaef504a22b31f7d5eca8af9a5591f22e32) (thanks @rom1504)
+* [bump mineflayer](https://github.com/PrismarineJS/prismarine-web-client/commit/d4c0c0c6e610cb2d1f836acb800cfa38777fc50e) (thanks @rom1504)
+* [Should show error screen on error (#350)](https://github.com/PrismarineJS/prismarine-web-client/commit/2fba863d662cb895339c3eda905a4f1edc8d5db6) (thanks @zardoy)
+* [[ImgBot] Optimize images (#348)](https://github.com/PrismarineJS/prismarine-web-client/commit/856639d586f1647bccef7abffe20a218ab232ddf) (thanks @Rahib777-7)
+
+### 1.5.0
+
+* Add BossBars @sverben (PrismarineJS#319)
+* save all play screen options @Fury-101 (PrismarineJS#326)
+
+### 1.4.0
+
+* so much
+
+### 1.3.0
+
+* a lot
+
+### 1.2.0
+
+* custom proxy selection (@Karang)
+* loading screen (@AwesomestCode)
+* better chat (@AwesomestCode)
+* default random name (@rom1504)
+* fix pixelated (@Karang)
+* placing and breaking (@ShrimpyStuff)
+* crosshairs (@AwesomestCode)
+
+### 1.1.0
+
+* chat
+* better font
+* pitch fix
+
+### 1.0.1
+
+* initial implementation
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..61ce9b1c4a5e7780c081b89732221cd85086239e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 PrismarineJS
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README_PT.md b/README_PT.md
new file mode 100644
index 0000000000000000000000000000000000000000..77c4fc523afa1109683827bafc99e6d84783408b
--- /dev/null
+++ b/README_PT.md
@@ -0,0 +1,104 @@
+# prismarine-web-client
+[](http://npmjs.com/package/prismarine-web-client)
+[](https://github.com/PrismarineJS/prismarine-web-client/actions?query=workflow%3A%22CI%22)
+[](https://discord.gg/GsEFRM8)
+[](https://gitpod.io/#https://github.com/PrismarineJS/prismarine-web-client)
+
+| 🇺🇸 [English](README.md) | 🇷🇺 [Russian](README_RU.md) | 🇵🇹 [Portuguese](README_PT.md) |
+| ----------------------- | -------------------------- | ---------------------------- |
+
+Um cliente de Minecraft a funcionar numa página web. **Demostração em https://prismarinejs.github.io/prismarine-web-client/**
+
+## Como funciona
+prismarine-web-client executa mineflayer e prismarine-viewer no teu navegador, que se conecta por WebSocket a uma proxy
+que traduz o conexão do WebSocket em TCP para poderes conectar-te a servidores normais do Minecraft. Prismarine-web-client é basiado em:
+* [prismarine-viewer](https://github.com/PrismarineJS/prismarine-viewer) para renderizar o mundo
+* [mineflayer](https://github.com/PrismarineJS/mineflayer) um API incrível do cliente de Minecraft
+
+Da uma olhada nestes módulos se quiseres entender mais sobre como isto funciona e poderes contribuir!
+
+## Captura de tela
+
+
+## Demostração ao vivo
+Clica neste endereço para o abrires no navegador, sem instalação necessária: https://prismarinejs.github.io/prismarine-web-client/
+
+*Testado no Chrome & Firefox para plataformas desktop.*
+
+## Uso
+Para hospedá-lo por si próprio, execute estes comandos no bash:
+```bash
+$ npm install -g prismarine-web-client
+$ prismarine-web-client
+```
+Finalmente, abra `http://localhost:8080` no seu navigador.
+
+## Conteúdos
+
+* Mostra criaturas e os jogadores
+* Mostra os blocos
+* Movimento (podes mover-te, e também ver entidades a mover-se em tempo real)
+* Colocar e destruir blocos
+
+## Planeamentos
+* Containers (inventário, baús, etc.)
+* Sons
+* Mais interações no mundo (atacar entidades, etc.)
+* Renderizar cosméticos (ciclo dia-noite, nevoeiro, etc.)
+
+## Desenvolvimentos
+
+Se estiveres a contribuir/fazer alterações, precisas intalá-lo de outra forma.
+
+Primeiro, clona o repositório.
+
+Depois, defina o seu diretório de trabalho para o do repositório. Por examplo:
+```bash
+$ cd ~/prismarine-web-client/
+```
+
+Finalmente, execute
+
+```bash
+$ npm install
+$ npm start
+```
+
+Isto vai começar o express e webpack no modo de desenvolvimento; quando salvares um arquivo, o build vai ser executado de novo (demora 5s),
+e podes atualizar a página para veres o novo resultado.
+
+Conecta em http://localhost:8080 no teu navegador.
+
+Poderás ter que desativar o auto salvar no teu IDE para evitar estar constantemente a reconstruir; see https://webpack.js.org/guides/development/#adjusting-your-text-editor.
+
+Para conferir a build de produção (vai demorar alguns minutos para terminar), podes executar `npm run build-start`.
+
+Se estiveres interessado em contribuir, podes dar uma vista de olhos nos projetos em https://github.com/PrismarineJS/prismarine-web-client/projects.
+
+Algumas variáveis estão expostas no objeto global ``window`` para depuração:
+* ``bot``
+* ``viewer``
+* ``mcData``
+* ``worldView``
+* ``Vec3``
+* ``pathfinder``
+* ``debugMenu``
+
+### Adicionar coisas no debugMenu
+
+debugMenu.customEntries['myKey'] = 'myValue'
+delete debugMenu.customEntries['myKey']
+
+### Alguns exemplos de depuração
+
+Na devtools do chrome:
+
+* `bot.chat('test')` permite usar o chat
+* `bot.chat(JSON.stringify(Object.values(bot.players).map(({username, ping}) => ({username, ping}))))` display the ping of everyone
+* `window.bot.entity.position.y += 5` saltar
+* `bot.chat(JSON.stringify(bot.findBlock({matching:(block) => block.name==='diamond_ore', maxDistance:256}).position))` encontrar a posição de um bloco de diamante
+* `bot.physics.stepHeight = 2` permite andar sobre os blocos
+* `bot.physics.sprintSpeed = 5` andar mais rápido
+* `bot.loadPlugin(pathfinder.pathfinder)` em seguida `bot.pathfinder.goto(new pathfinder.goals.GoalXZ(100, 100))` para ir para a posição 100, 100
+
+Para mais ideas de depuração, leia o documento [mineflayer](https://github.com/PrismarineJS/mineflayer).
diff --git a/README_RU.md b/README_RU.md
new file mode 100644
index 0000000000000000000000000000000000000000..bd632468a978a0a5961a8aee21b39979c4cdee5f
--- /dev/null
+++ b/README_RU.md
@@ -0,0 +1,105 @@
+# prismarine-web-client
+[](http://npmjs.com/package/prismarine-web-client)
+[](https://github.com/PrismarineJS/prismarine-web-client/actions?query=workflow%3A%22CI%22)
+[](https://discord.gg/GsEFRM8)
+[](https://gitpod.io/#https://github.com/PrismarineJS/prismarine-web-client)
+
+| 🇺🇸 [English](README.md) | 🇷🇺 [Russian](README_RU.md) | 🇵🇹 [Portuguese](README_PT.md) |
+| ----------------------- | -------------------------- | ---------------------------- |
+
+Клиент Minecraft, запущенный на веб-странице. **Демонстрация на https://prismarinejs.github.io/prismarine-web-client/**
+
+
+## Как это работает
+prismarine-web-client запускает mineflayer и prismarine-viewer в вашем браузере, которые подключаются к прокси через WebSocket
+который переводит соединение WebSocket в TCP для подключения к обычным серверам Minecraft. Prismarine-web-client основан на:
+* [prismarine-viewer](https://github.com/PrismarineJS/prismarine-viewer) для рендера мира
+* [mineflayer](https://github.com/PrismarineJS/mineflayer) для высокоуровневого API клиента minecraft
+
+Проверьте эти модули, если вы хотите больше понять, как это работает, и внести свой вклад!
+
+## Скриншот
+
+
+## Демонстация
+Нажмите на эту ссылку, чтобы открыть ее в вашем браузере, установка не требуется: https://prismarinejs.github.io/prismarine-web-client/
+
+*Протестировано в Chrome и Firefox для настольных платформ.*
+
+## Использование
+Чтобы хостить его самостоятельно, выполните эти команды в bash:
+```bash
+$ npm install -g prismarine-web-client
+$ prismarine-web-client
+```
+Наконец, откройте `http://localhost:8080` в вашем браузере.
+
+## Функции
+
+* Показывание мобов и игроков
+* Показывание блоков
+* Передвижение (Вы можете двигаться, и вы можете видеть передвижение других сущностей)
+* Установка и ломание блоков
+
+## Roadmap
+* Контейнеры (Инвентарь, сундуки, и так далее.)
+* Звуки
+* Больше взаимодействий с Миром (Атаковать мобов и других игроков, и так далее.)
+* Косметические особенности рендера (Цикл дня и ночи, fog, и так далее.)
+
+## Разработка
+
+Если вы ввносите изменения, вам нужно установить его по-другому.
+
+Во-первых, клонируйте репозиторий.
+
+Затем установите свой рабочий каталог в каталог репозитория. Например:
+```bash
+$ cd ~/prismarine-web-client/
+```
+
+Наконец, запустите.
+
+```bash
+$ npm install
+$ npm start
+```
+
+Это запустит express и webpack в режиме разработки: всякий раз, когда вы сохраняете файл, сборка будет переделана (это займет 5 секунд),
+и вы можете обновить страницу, чтобы получить новый результат.
+
+Для входа в Prismarine Web Client откройте http://localhost:8080 в вашем браузере.
+
+Если вы захотите отключить автоматическое сохранение в своем IDE, чтобы избежать постоянной пересборки Web Client'а, смотрите: https://webpack.js.org/guides/development/#adjusting-your-text-editor
+
+Чтобы проверить сборку Web Client'а (на сборку потребуется минута), вы можете запустить `npm run build-start`
+
+Если вы заинтересованы в участии, вы можете проверить проекты [тут](https://github.com/PrismarineJS/prismarine-web-client/projects)
+
+Некоторые переменные которые отображаются в окне для отладки:
+* bot
+* viewer
+* mcData
+* worldView
+* Vec3
+* pathfinder
+* debugMenu
+
+### Как добавить дополнительные элементы в меню отладки ?
+
+debugMenu.customEntries['myKey'] = 'myValue'
+delete debugMenu.customEntries['myKey']
+
+### Некоторые примеры отладки
+
+В инструментах chrome dev
+
+* `bot.chat('test')` позволяет вам использовать чат
+* `bot.chat(JSON.stringify(Object.values(bot.players).map(({username, ping}) => ({username, ping}))))` показывает пинг всех игроков
+* `window.bot.entity.position.y += 5` прыжок
+* `bot.chat(JSON.stringify(bot.findBlock({matching:(block) => block.name==='diamond_ore', maxDistance:256}).position))` ищет позицию алмазной руды
+* `bot.physics.stepHeight = 2` позволяет вам взбиратся по блокам
+* `bot.physics.sprintSpeed = 5` более быстрый бег
+* `bot.loadPlugin(pathfinder.pathfinder)` затем `bot.pathfinder.goto(new pathfinder.goals.GoalXZ(100, 100))` идет к координатам 100, 100
+
+Для получения дополнительных идей по отладке прочитайте [mineflayer](https://github.com/PrismarineJS/mineflayer) документацию
diff --git a/assets/click_stereo.ogg b/assets/click_stereo.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..b4889da63483512786c016c57463e04f2706540a
Binary files /dev/null and b/assets/click_stereo.ogg differ
diff --git a/assets/favicon.ico b/assets/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..27f48c13add5e5bf93921eff48f96a3b76af19bb
Binary files /dev/null and b/assets/favicon.ico differ
diff --git a/assets/favicon.png b/assets/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..0b46a03263713ccb0df3231f28c542761d0569c0
Binary files /dev/null and b/assets/favicon.png differ
diff --git a/assets/favicon.svg b/assets/favicon.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6b9f69bf53db2be442b774c82afa2d5cd762f8ac
--- /dev/null
+++ b/assets/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/invsprite.png b/assets/invsprite.png
new file mode 100644
index 0000000000000000000000000000000000000000..062e95fc5696dd99bb303e725ab512f74b9fd527
--- /dev/null
+++ b/assets/invsprite.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f1ac25723319eaf53490652f2255c7e82d3f06805946d558aca239ba843f76b6
+size 2845482
diff --git a/assets/manifest.json b/assets/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..5bb456583ab50383c9328ab76267136ff85be4f7
--- /dev/null
+++ b/assets/manifest.json
@@ -0,0 +1,13 @@
+{
+ "name": "Prismarine Web Client",
+ "short_name": "Prismarine Web Client",
+ "scope": "/",
+ "start_url": "/",
+ "icons": [{
+ "src": "favicon.png",
+ "sizes": "512x512"
+ }],
+ "background_color": "#349474",
+ "theme_color": "#349474",
+ "display": "standalone"
+}
\ No newline at end of file
diff --git a/assets/minecraftia.woff b/assets/minecraftia.woff
new file mode 100644
index 0000000000000000000000000000000000000000..457901322abaccaca0b07e27cb4d7952f16e5c06
Binary files /dev/null and b/assets/minecraftia.woff differ
diff --git a/assets/mojangles.ttf b/assets/mojangles.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..e7b87e9f56cf3459a0942a144142ba2556a15361
Binary files /dev/null and b/assets/mojangles.ttf differ
diff --git a/config.json b/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..3e943f8564cc76c8b1905739e3bce6620380341a
--- /dev/null
+++ b/config.json
@@ -0,0 +1,7 @@
+{
+ "defaultHost": "kaboom.pw",
+ "defaultHostPort": 25565,
+ "defaultProxy": "",
+ "defaultProxyPort": 0,
+ "defaultVersion": "1.18.2"
+}
diff --git a/extra-textures/background/panorama_0.png b/extra-textures/background/panorama_0.png
new file mode 100644
index 0000000000000000000000000000000000000000..185a3dda389c79f9e7ffee4602e23c5c8fea2210
Binary files /dev/null and b/extra-textures/background/panorama_0.png differ
diff --git a/extra-textures/background/panorama_1.png b/extra-textures/background/panorama_1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c5cbfd0d231ed7f7d16405e1b01a9b24f3962cd9
Binary files /dev/null and b/extra-textures/background/panorama_1.png differ
diff --git a/extra-textures/background/panorama_2.png b/extra-textures/background/panorama_2.png
new file mode 100644
index 0000000000000000000000000000000000000000..f8fb7f79f7f5b22be3209960c3574555b3606a21
Binary files /dev/null and b/extra-textures/background/panorama_2.png differ
diff --git a/extra-textures/background/panorama_3.png b/extra-textures/background/panorama_3.png
new file mode 100644
index 0000000000000000000000000000000000000000..c029be04998a6f3246b009dcbab816c8fdb017d0
Binary files /dev/null and b/extra-textures/background/panorama_3.png differ
diff --git a/extra-textures/background/panorama_4.png b/extra-textures/background/panorama_4.png
new file mode 100644
index 0000000000000000000000000000000000000000..bed2ae74a23573978716023e8238335306f71790
Binary files /dev/null and b/extra-textures/background/panorama_4.png differ
diff --git a/extra-textures/background/panorama_5.png b/extra-textures/background/panorama_5.png
new file mode 100644
index 0000000000000000000000000000000000000000..811b90794dacc471c411b48df980c38fc0b23b23
--- /dev/null
+++ b/extra-textures/background/panorama_5.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7888ab245c81f35c2e6c16d10872512e4c6fa7ab47a8f76063ef688213802007
+size 1122177
diff --git a/extra-textures/edition.png b/extra-textures/edition.png
new file mode 100644
index 0000000000000000000000000000000000000000..995cdb17aec44da1245de5edf462008259dd499f
Binary files /dev/null and b/extra-textures/edition.png differ
diff --git a/extra-textures/gui.png b/extra-textures/gui.png
new file mode 100644
index 0000000000000000000000000000000000000000..d583b6b4a5b70ccc90ec26ed5ff903179961d053
Binary files /dev/null and b/extra-textures/gui.png differ
diff --git a/extra-textures/loading.png b/extra-textures/loading.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f6a121accea52a2a81ad9beb0587f14bb4185db
Binary files /dev/null and b/extra-textures/loading.png differ
diff --git a/extra-textures/loading.psd b/extra-textures/loading.psd
new file mode 100644
index 0000000000000000000000000000000000000000..88d44fcad7c0f32f75fdc8c1326e4525c902b597
--- /dev/null
+++ b/extra-textures/loading.psd
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:35bb9df1485124bc896ba3d16033b7cb0dbb92b87c8aa98b0a8c3f1c0c1e051e
+size 2636661
diff --git a/index.html b/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..e6b2218d625894ff8ad4a18364b2188996da310c
--- /dev/null
+++ b/index.html
@@ -0,0 +1,30 @@
+
+
+
+ Prismarine Web Client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index.js b/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..23a5054a5316a66c019672e20b47dbd4819ac878
--- /dev/null
+++ b/index.js
@@ -0,0 +1,466 @@
+/* global THREE */
+require('./lib/chat')
+
+require('./lib/menus/components/button')
+require('./lib/menus/components/edit_box')
+require('./lib/menus/components/slider')
+require('./lib/menus/components/hotbar')
+require('./lib/menus/components/health_bar')
+require('./lib/menus/components/food_bar')
+require('./lib/menus/components/breath_bar')
+require('./lib/menus/components/debug_overlay')
+require('./lib/menus/components/playerlist_overlay')
+require('./lib/menus/components/bossbars_overlay')
+require('./lib/menus/hud')
+require('./lib/menus/play_screen')
+require('./lib/menus/pause_screen')
+require('./lib/menus/loading_screen')
+require('./lib/menus/keybinds_screen')
+require('./lib/menus/options_screen')
+require('./lib/menus/title_screen')
+
+const net = require('net')
+const Cursor = require('./lib/cursor')
+
+// Workaround for process.versions.node not existing in the browser
+process.versions.node = '14.0.0'
+
+const mineflayer = require('mineflayer')
+const { WorldView, Viewer } = require('prismarine-viewer/viewer')
+const pathfinder = require('mineflayer-pathfinder')
+const { Vec3 } = require('vec3')
+global.THREE = require('three')
+const { initVR } = require('./lib/vr')
+
+if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/service-worker.js').then(registration => {
+ console.log('SW registered: ', registration)
+ }).catch(registrationError => {
+ console.log('SW registration failed: ', registrationError)
+ })
+ })
+}
+
+const maxPitch = 0.5 * Math.PI
+const minPitch = -0.5 * Math.PI
+
+// Create three.js context, add to page
+const renderer = new THREE.WebGLRenderer()
+renderer.setPixelRatio(window.devicePixelRatio || 1)
+renderer.setSize(window.innerWidth, window.innerHeight)
+document.body.appendChild(renderer.domElement)
+
+// Create viewer
+const viewer = new Viewer(renderer)
+
+// Menu panorama background
+function addPanoramaCubeMap () {
+ let time = 0
+ viewer.camera = new THREE.PerspectiveCamera(85, window.innerWidth / window.innerHeight, 0.05, 1000)
+ viewer.camera.updateProjectionMatrix()
+ viewer.camera.position.set(0, 0, 0)
+ viewer.camera.rotation.set(0, 0, 0)
+ const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000)
+
+ const loader = new THREE.TextureLoader()
+ const panorMaterials = [
+ new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_1.png'), transparent: true, side: THREE.DoubleSide }), // WS
+ new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_3.png'), transparent: true, side: THREE.DoubleSide }), // ES
+ new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_4.png'), transparent: true, side: THREE.DoubleSide }), // Up
+ new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_5.png'), transparent: true, side: THREE.DoubleSide }), // Down
+ new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_0.png'), transparent: true, side: THREE.DoubleSide }), // NS
+ new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_2.png'), transparent: true, side: THREE.DoubleSide }) // SS
+ ]
+
+ const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials)
+
+ panoramaBox.onBeforeRender = () => {
+ time += 0.01
+ panoramaBox.rotation.y = Math.PI + time * 0.01
+ panoramaBox.rotation.z = Math.sin(-time * 0.001) * 0.001
+ }
+
+ const group = new THREE.Object3D()
+ group.add(panoramaBox)
+
+ const Entity = require('prismarine-viewer/viewer/lib/entity/Entity')
+ for (let i = 0; i < 42; i++) {
+ const m = new Entity('1.16.4', 'squid').mesh
+ m.position.set(Math.random() * 30 - 15, Math.random() * 20 - 10, Math.random() * 10 - 17)
+ m.rotation.set(0, Math.PI + Math.random(), -Math.PI / 4, 'ZYX')
+ const v = Math.random() * 0.01
+ m.children[0].onBeforeRender = () => {
+ m.rotation.y += v
+ m.rotation.z = Math.cos(panoramaBox.rotation.y * 3) * Math.PI / 4 - Math.PI / 2
+ }
+ group.add(m)
+ }
+
+ viewer.scene.add(group)
+ return group
+}
+
+const panoramaCubeMap = addPanoramaCubeMap()
+
+function removePanorama () {
+ viewer.camera = new THREE.PerspectiveCamera(document.getElementById('options-screen').fov, window.innerWidth / window.innerHeight, 0.1, 1000)
+ viewer.camera.updateProjectionMatrix()
+ viewer.scene.remove(panoramaCubeMap)
+}
+
+let animate = () => {
+ window.requestAnimationFrame(animate)
+ viewer.update()
+ renderer.render(viewer.scene, viewer.camera)
+}
+animate()
+
+window.addEventListener('resize', () => {
+ viewer.camera.aspect = window.innerWidth / window.innerHeight
+ viewer.camera.updateProjectionMatrix()
+ renderer.setSize(window.innerWidth, window.innerHeight)
+})
+
+const showEl = (str) => { document.getElementById(str).style = 'display:block' }
+async function main () {
+ const menu = document.getElementById('play-screen')
+
+ menu.addEventListener('connect', e => {
+ const options = e.detail
+ menu.style = 'display: none;'
+ showEl('loading-screen')
+ removePanorama()
+ connect(options)
+ })
+}
+
+async function connect (options) {
+ const loadingScreen = document.getElementById('loading-screen')
+
+ const hud = document.getElementById('hud')
+ const chat = hud.shadowRoot.querySelector('#chat')
+ const debugMenu = hud.shadowRoot.querySelector('#debug-overlay')
+ const optionsScrn = document.getElementById('options-screen')
+ const keyBindScrn = document.getElementById('keybinds-screen')
+ const gameMenu = document.getElementById('pause-screen')
+
+ const viewDistance = optionsScrn.renderDistance
+ const hostprompt = options.server
+ const proxyprompt = options.proxy
+ const username = options.username
+ const password = options.password
+
+ let host, port, proxy, proxyport
+ if (!hostprompt.includes(':')) {
+ host = hostprompt
+ port = 25565
+ } else {
+ [host, port] = hostprompt.split(':')
+ port = parseInt(port, 10)
+ }
+
+ if (!proxyprompt.includes(':')) {
+ proxy = proxyprompt
+ proxyport = undefined
+ } else {
+ [proxy, proxyport] = proxyprompt.split(':')
+ proxyport = parseInt(proxyport, 10)
+ }
+ console.log(`connecting to ${host} ${port} with ${username}`)
+
+ if (proxy) {
+ console.log(`using proxy ${proxy} ${proxyport}`)
+ net.setProxy({ hostname: proxy, port: proxyport })
+ }
+
+ loadingScreen.status = 'Logging in'
+
+ const errorAbortController = new AbortController()
+ window.addEventListener('unhandledrejection', (e) => {
+ handleError(e.reason)
+ })
+ window.addEventListener('error', (e) => {
+ handleError(e.message)
+ })
+ const bot = mineflayer.createBot({
+ host,
+ port,
+ version: options.botVersion === '' ? false : options.botVersion,
+ username,
+ password,
+ viewDistance: 'tiny',
+ checkTimeoutInterval: 240 * 1000,
+ noPongTimeout: 240 * 1000,
+ closeTimeout: 240 * 1000
+ })
+ hud.preload(bot)
+
+ const handleError = (err) => {
+ console.log('Encountered error!', err)
+ loadingScreen.status = `Error encountered. Error message: ${err}. Please reload the page`
+ loadingScreen.style = 'display: block;'
+ loadingScreen.hasError = true
+ }
+ bot.on('error', handleError)
+
+ bot.on('kicked', (kickReason) => {
+ console.log('User was kicked!', kickReason)
+ loadingScreen.status = `The Minecraft server kicked you. Kick reason: ${kickReason}. Please reload the page to rejoin`
+ loadingScreen.style = 'display: block;'
+ loadingScreen.hasError = true
+ })
+
+ bot.on('end', (endReason) => {
+ console.log('disconnected for', endReason)
+ loadingScreen.status = `You have been disconnected from the server. End reason: ${endReason}. Please reload the page to rejoin`
+ loadingScreen.style = 'display: block;'
+ loadingScreen.hasError = true
+ })
+
+ bot.once('login', () => {
+ loadingScreen.status = 'Loading world'
+ })
+
+ bot.once('spawn', () => {
+ const mcData = require('minecraft-data')(bot.version)
+
+ loadingScreen.status = 'Placing blocks (starting viewer)'
+
+ console.log('bot spawned - starting viewer')
+
+ const version = bot.version
+
+ const center = bot.entity.position
+
+ console.log(viewDistance)
+ const worldView = new WorldView(bot.world, viewDistance, center)
+
+ gameMenu.init(renderer)
+ optionsScrn.isInsideWorld = true
+ optionsScrn.addEventListener('fov_changed', (e) => {
+ viewer.camera.fov = e.detail.fov
+ viewer.camera.updateProjectionMatrix()
+ })
+
+ viewer.setVersion(version)
+
+ window.worldView = worldView
+ window.bot = bot
+ window.mcData = mcData
+ window.viewer = viewer
+ window.Vec3 = Vec3
+ window.pathfinder = pathfinder
+ window.debugMenu = debugMenu
+ window.settings = optionsScrn
+ window.renderer = renderer
+
+ initVR(bot, renderer, viewer)
+
+ const cursor = new Cursor(viewer, renderer, bot)
+ animate = () => {
+ window.requestAnimationFrame(animate)
+ viewer.update()
+ cursor.update(bot)
+ debugMenu.cursorBlock = cursor.cursorBlock
+ renderer.render(viewer.scene, viewer.camera)
+ }
+
+ // Link WorldView and Viewer
+ viewer.listen(worldView)
+ worldView.listenToBot(bot)
+ worldView.init(bot.entity.position)
+
+ // Bot position callback
+ function botPosition () {
+ viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
+ worldView.updatePosition(bot.entity.position)
+ }
+ bot.on('move', botPosition)
+ botPosition()
+
+ loadingScreen.status = 'Setting callbacks'
+
+ function moveCallback (e) {
+ bot.entity.pitch -= e.movementY * optionsScrn.mouseSensitivityY * 0.0001
+ bot.entity.pitch = Math.max(minPitch, Math.min(maxPitch, bot.entity.pitch))
+ bot.entity.yaw -= e.movementX * optionsScrn.mouseSensitivityX * 0.0001
+
+ viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch)
+ }
+
+ function changeCallback () {
+ if (document.pointerLockElement === renderer.domElement ||
+ document.mozPointerLockElement === renderer.domElement ||
+ document.webkitPointerLockElement === renderer.domElement) {
+ document.addEventListener('mousemove', moveCallback, false)
+ } else {
+ document.removeEventListener('mousemove', moveCallback, false)
+ }
+ }
+
+ document.addEventListener('pointerlockchange', changeCallback, false)
+ document.addEventListener('mozpointerlockchange', changeCallback, false)
+ document.addEventListener('webkitpointerlockchange', changeCallback, false)
+
+ let lastTouch
+ document.addEventListener('touchmove', (e) => {
+ window.scrollTo(0, 0)
+ e.preventDefault()
+ e.stopPropagation()
+ if (lastTouch !== undefined) {
+ moveCallback({ movementX: e.touches[0].pageX - lastTouch.pageX, movementY: e.touches[0].pageY - lastTouch.pageY })
+ }
+ lastTouch = e.touches[0]
+ }, { passive: false })
+
+ document.addEventListener('touchend', (e) => {
+ lastTouch = undefined
+ }, { passive: false })
+
+ renderer.domElement.requestPointerLock = renderer.domElement.requestPointerLock ||
+ renderer.domElement.mozRequestPointerLock ||
+ renderer.domElement.webkitRequestPointerLock
+ document.addEventListener('mousedown', (e) => {
+ if (!chat.inChat && !gameMenu.inMenu) {
+ renderer.domElement.requestPointerLock()
+ }
+ })
+
+ document.addEventListener('contextmenu', (e) => e.preventDefault(), false)
+
+ window.addEventListener('blur', (e) => {
+ bot.clearControlStates()
+ }, false)
+
+ document.addEventListener('keydown', (e) => {
+ if (chat.inChat) return
+ if (gameMenu.inMenu) return
+
+ keyBindScrn.keymaps.forEach(km => {
+ if (e.code === km.key) {
+ switch (km.defaultKey) {
+ case 'KeyQ':
+ if (bot.heldItem) bot.tossStack(bot.heldItem)
+ break
+ case 'ControlLeft':
+ bot.setControlState('sprint', true)
+ break
+ case 'ShiftLeft':
+ bot.setControlState('sneak', true)
+ break
+ case 'Space':
+ bot.setControlState('jump', true)
+ break
+ case 'KeyD':
+ bot.setControlState('left', true)
+ break
+ case 'KeyA':
+ bot.setControlState('right', true)
+ break
+ case 'KeyS':
+ bot.setControlState('back', true)
+ break
+ case 'KeyW':
+ bot.setControlState('forward', true)
+ break
+ }
+ }
+ })
+ }, false)
+
+ document.addEventListener('keyup', (e) => {
+ keyBindScrn.keymaps.forEach(km => {
+ if (e.code === km.key) {
+ switch (km.defaultKey) {
+ case 'ControlLeft':
+ bot.setControlState('sprint', false)
+ break
+ case 'ShiftLeft':
+ bot.setControlState('sneak', false)
+ break
+ case 'Space':
+ bot.setControlState('jump', false)
+ break
+ case 'KeyD':
+ bot.setControlState('left', false)
+ break
+ case 'KeyA':
+ bot.setControlState('right', false)
+ break
+ case 'KeyS':
+ bot.setControlState('back', false)
+ break
+ case 'KeyW':
+ bot.setControlState('forward', false)
+ break
+ }
+ }
+ })
+ }, false)
+
+ loadingScreen.status = 'Done!'
+ console.log(loadingScreen.status) // only do that because it's read in index.html and npm run fix complains.
+
+ hud.init(renderer, bot, host)
+ hud.style.display = 'block'
+
+ setTimeout(function () {
+ // game in playable state show errors in console instead
+ errorAbortController.abort()
+ if (loadingScreen.hasError) return
+ // remove loading screen, wait a second to make sure a frame has properly rendered
+ loadingScreen.style = 'display: none;'
+ }, 2500)
+ })
+}
+
+/**
+ * @param {URLSearchParams} params
+ */
+async function fromTheOutside (params, addr) {
+ const opts = {}
+ const dfltConfig = await (await window.fetch('config.json')).json()
+
+ let server, port, proxy, proxyPort
+
+ if (address.includes(':')) {
+ const s = address.split(':')
+ server = s[0]
+ port = Number(s[1]) || 25565
+ } else {
+ server = address
+ port = Number(params.get('port')) || 25565
+ }
+
+ const proxyAddr = params.get('proxy')
+ if (proxyAddr) {
+ const s = proxyAddr.split(':')
+ proxy = s[0]
+ proxyPort = Number(s[1] ?? 'NaN') || 22
+ } else {
+ proxy = dfltConfig.defaultProxy
+ proxyPort = !dfltConfig.defaultProxy && !dfltConfig.defaultProxyPort ? '' : dfltConfig.defaultProxyPort ?? 443
+ }
+
+ opts.server = `${server}:${port}`
+ opts.proxy = `${proxy}:${proxyPort}`
+ opts.username = params.get('username') ?? `pviewer${Math.floor(Math.random() * 1000)}`
+ opts.password = params.get('password') ?? ''
+ opts.botVersion = params.get('version') ?? false
+
+ console.log(opts)
+
+ showEl('loading-screen')
+ removePanorama()
+ connect(opts)
+}
+
+const params = new URLSearchParams(window.location.search)
+let address
+if ((address = params.get('address'))) {
+ fromTheOutside(params, address)
+} else {
+ showEl('title-screen')
+ main()
+}
diff --git a/lib/chat.js b/lib/chat.js
new file mode 100644
index 0000000000000000000000000000000000000000..d584b1f56bfa4af30a5e640c2d61362bff4844e8
--- /dev/null
+++ b/lib/chat.js
@@ -0,0 +1,361 @@
+const { LitElement, html, css } = require('lit')
+const { isMobile } = require('./menus/components/common')
+
+const styles = {
+ black: 'color:#000000',
+ dark_blue: 'color:#0000AA',
+ dark_green: 'color:#00AA00',
+ dark_aqua: 'color:#00AAAA',
+ dark_red: 'color:#AA0000',
+ dark_purple: 'color:#AA00AA',
+ gold: 'color:#FFAA00',
+ gray: 'color:#AAAAAA',
+ dark_gray: 'color:#555555',
+ blue: 'color:#5555FF',
+ green: 'color:#55FF55',
+ aqua: 'color:#55FFFF',
+ red: 'color:#FF5555',
+ light_purple: 'color:#FF55FF',
+ yellow: 'color:#FFFF55',
+ white: 'color:#FFFFFF',
+ bold: 'font-weight:900',
+ strikethrough: 'text-decoration:line-through',
+ underlined: 'text-decoration:underline',
+ italic: 'font-style:italic'
+}
+
+function colorShadow (hex, dim = 0.25) {
+ const color = parseInt(hex.replace('#', ''), 16)
+
+ const r = (color >> 16 & 0xFF) * dim | 0
+ const g = (color >> 8 & 0xFF) * dim | 0
+ const b = (color & 0xFF) * dim | 0
+
+ const f = (c) => ('00' + c.toString(16)).substr(-2)
+ return `#${f(r)}${f(g)}${f(b)}`
+}
+
+class ChatBox extends LitElement {
+ static get styles () {
+ return css`
+ .chat-wrapper {
+ position: fixed;
+ z-index: 10;
+ }
+
+ .chat-display-wrapper {
+ bottom: 40px;
+ padding: 4px;
+ padding-left: 0;
+ max-height: var(--chatHeight);
+ width: var(--chatWidth);
+ transform-origin: bottom left;
+ transform: scale(var(--chatScale));
+ }
+
+ .chat-input-wrapper {
+ bottom: 1px;
+ width: calc(100% - 3px);
+ position: absolute;
+ left: 1px;
+ box-sizing: border-box;
+ overflow: hidden;
+ background-color: rgba(0, 0, 0, 0);
+ }
+
+ .input-mobile {
+ top: 1px;
+ }
+
+ .display-mobile {
+ top: 40px;
+ }
+
+ .chat {
+ overflow: hidden;
+ color: white;
+ font-size: 10px;
+ margin: 0px;
+ line-height: 100%;
+ text-shadow: 1px 1px 0px #3f3f3f;
+ font-family: mojangles, minecraft, monospace;
+ width: 100%;
+ max-height: var(--chatHeight);
+ }
+
+ input[type=text], #chatinput {
+ background-color: rgba(0, 0, 0, 0.5);
+ border: 1px solid rgba(0, 0, 0, 0);
+ display: none;
+ outline: none;
+ }
+
+ #chatinput:focus {
+ border-color: white;
+ }
+
+ .chat-message {
+ display: block;
+ padding-left: 4px;
+ background-color: rgba(0, 0, 0, 0.5);
+ }
+
+ .chat-message-fadeout {
+ opacity: 1;
+ transition: all 3s;
+ }
+
+ .chat-message-fade {
+ opacity: 0;
+ }
+
+ .chat-message-faded {
+ transition: none !important;
+ }
+
+ .chat-message-chat-opened {
+ opacity: 1 !important;
+ transition: none !important;
+ }
+ `
+ }
+
+ render () {
+ return html`
+
+
+
Welcome to prismarine-web-client! Chat appears here.
+
+
+
+ `
+ }
+
+ constructor () {
+ super()
+ this.chatHistoryPos = 0
+ this.chatHistory = []
+ }
+
+ enableChat (isCommand) {
+ const chat = this.shadowRoot.querySelector('#chat')
+ const chatInput = this.shadowRoot.querySelector('#chatinput')
+
+ this.shadowRoot.querySelector('#chat-wrapper2').classList.toggle('input-mobile', isMobile())
+ this.shadowRoot.querySelector('#chat-wrapper').classList.toggle('display-mobile', isMobile())
+
+ // Set inChat value
+ this.inChat = true
+ // Exit the pointer lock
+ document.exitPointerLock()
+ // Show chat input
+ chatInput.style.display = 'block'
+ // Show extended chat history
+ chat.style.maxHeight = 'var(--chatHeight)'
+ chat.scrollTop = chat.scrollHeight // Stay bottom of the list
+ if (isCommand) { // handle commands
+ chatInput.value = '/'
+ }
+ // Focus element
+ chatInput.focus()
+ this.chatHistoryPos = this.chatHistory.length
+ document.querySelector('#hud').shadowRoot.querySelector('#chat').shadowRoot.querySelectorAll('.chat-message').forEach(e => e.classList.add('chat-message-chat-opened'))
+ }
+
+ /**
+ * @param {globalThis.THREE.Renderer} client
+ * @param {import('minecraft-protocol').Client} renderer
+ */
+ init (client, renderer) {
+ this.inChat = false
+ const chat = this.shadowRoot.querySelector('#chat')
+ const gameMenu = document.getElementById('pause-screen')
+ const chatInput = this.shadowRoot.querySelector('#chatinput')
+
+ renderer.domElement.requestPointerLock = renderer.domElement.requestPointerLock ||
+ renderer.domElement.mozRequestPointerLock ||
+ renderer.domElement.webkitRequestPointerLock
+
+ // Show chat
+ chat.style.display = 'block'
+
+ // Esc event - Doesnt work with onkeypress?! - keypressed is deprecated uk
+ document.addEventListener('keydown', e => {
+ if (gameMenu.inMenu) return
+ if (!this.inChat) return
+ e = e || window.event
+ if (e.code === 'Escape') {
+ disableChat()
+ } else if (e.code === 'ArrowUp') {
+ if (this.chatHistoryPos === 0) return
+ chatInput.value = this.chatHistory[--this.chatHistoryPos] !== undefined ? this.chatHistory[this.chatHistoryPos] : ''
+ setTimeout(() => { chatInput.setSelectionRange(-1, -1) }, 0)
+ } else if (e.code === 'ArrowDown') {
+ if (this.chatHistoryPos === this.chatHistory.length) return
+ chatInput.value = this.chatHistory[++this.chatHistoryPos] !== undefined ? this.chatHistory[this.chatHistoryPos] : ''
+ setTimeout(() => { chatInput.setSelectionRange(-1, -1) }, 0)
+ }
+ })
+
+ const keyBindScrn = document.getElementById('keybinds-screen')
+
+ // Chat events
+ document.addEventListener('keypress', e => {
+ if (gameMenu.inMenu) return
+ e = e || window.event
+ if (this.inChat === false) {
+ keyBindScrn.keymaps.forEach(km => {
+ if (e.code === km.key) {
+ switch (km.defaultKey) {
+ case 'KeyT':
+ setTimeout(() => this.enableChat(false), 0)
+ break
+ case 'Slash':
+ setTimeout(() => this.enableChat(true), 0)
+ break
+ }
+ }
+ })
+
+ return false
+ }
+
+ if (!this.inChat) return
+
+ e.stopPropagation()
+
+ if (e.code === 'Enter') {
+ this.chatHistory.push(chatInput.value)
+ client.write('chat', { message: chatInput.value })
+ disableChat()
+ }
+ })
+
+ const disableChat = () => {
+ this.inChat = false
+ hideChat()
+ renderer.domElement.requestPointerLock()
+ }
+
+ const hideChat = () => {
+ // Clear chat input
+ chatInput.value = ''
+ // Unfocus it
+ chatInput.blur()
+ // Hide it
+ chatInput.style.display = 'none'
+ // Hide extended chat history
+ chat.style.maxHeight = 'var(--chatHeight)'
+ chat.scrollTop = chat.scrollHeight // Stay bottom of the list
+ document.querySelector('#hud').shadowRoot.querySelector('#chat').shadowRoot.querySelectorAll('.chat-message').forEach(e => e.classList.remove('chat-message-chat-opened'))
+ }
+
+ client.on('chat', (packet) => {
+ // Reading of chat message
+ const fullmessage = JSON.parse(packet.message.toString())
+ const msglist = []
+
+ const colorF = (color) => {
+ return color.trim().startsWith('#') ? `color:${color}` : styles[color] ?? undefined
+ }
+
+ const readMsg = (msglist, msg) => {
+ const styles = {
+ color: msg.color,
+ bold: !!msg.bold,
+ italic: !!msg.italic,
+ underlined: !!msg.underlined,
+ strikethrough: !!msg.strikethrough,
+ obfuscated: !!msg.obfuscated
+ }
+
+ if (msg.text) {
+ msglist.push({
+ text: msg.text,
+ ...styles
+ })
+ }
+
+ if (msg.translate) {
+ const tText = window.mcData.language[msg.translate] ?? msg.translate
+
+ if (msg.with) {
+ const splited = tText.split(/%s|%\d+\$s/g)
+
+ let i = 0
+ splited.forEach((spl, j, arr) => {
+ msglist.push({ text: spl, ...styles })
+
+ if (j + 1 < arr.length) {
+ if (msg.with[i]) {
+ if (typeof msg.with[i] === 'string') {
+ readMsg(msglist, {
+ ...styles,
+ text: msg.with[i]
+ })
+ } else {
+ readMsg(msglist, {
+ ...styles,
+ ...msg.with[i]
+ })
+ }
+ }
+ i++
+ }
+ })
+ } else {
+ msglist.push({
+ text: tText,
+ ...styles
+ })
+ }
+ }
+
+ if (msg.extra) {
+ msg.extra.forEach(ex => {
+ readMsg(msglist, { ...styles, ...ex })
+ })
+ }
+ }
+
+ readMsg(msglist, fullmessage)
+
+ const li = document.createElement('li')
+ msglist.forEach(msg => {
+ const span = document.createElement('span')
+ span.appendChild(document.createTextNode(msg.text))
+ span.setAttribute(
+ 'style',
+ `${msg.color ? colorF(msg.color.toLowerCase()) + `; text-shadow: 1px 1px 0px ${colorShadow(colorF(msg.color.toLowerCase()).replace('color:', ''))}` : styles.white}; ${
+ msg.bold ? styles.bold + ';' : ''
+ }${msg.italic ? styles.italic + ';' : ''}${
+ msg.strikethrough ? styles.strikethrough + ';' : ''
+ }${msg.underlined ? styles.underlined + ';' : ''}`
+ )
+ li.appendChild(span)
+ })
+ chat.appendChild(li)
+ chat.scrollTop = chat.scrollHeight // Stay bottom of the list
+ // fading
+ li.classList.add('chat-message')
+ if (this.inChat) {
+ li.classList.add('chat-message-chat-opened')
+ }
+ setTimeout(() => {
+ li.classList.add('chat-message-fadeout')
+ li.classList.add('chat-message-fade')
+ setTimeout(() => {
+ li.classList.add('chat-message-faded')
+ }, 3000)
+ }, 5000)
+ })
+
+ hideChat()
+ }
+}
+
+window.customElements.define('chat-box', ChatBox)
diff --git a/lib/cursor.js b/lib/cursor.js
new file mode 100644
index 0000000000000000000000000000000000000000..f9ee6c622c94836d0615e5509425befd69de8d54
--- /dev/null
+++ b/lib/cursor.js
@@ -0,0 +1,143 @@
+/* global THREE performance */
+
+const { Vec3 } = require('vec3')
+
+function getViewDirection (pitch, yaw) {
+ const csPitch = Math.cos(pitch)
+ const snPitch = Math.sin(pitch)
+ const csYaw = Math.cos(yaw)
+ const snYaw = Math.sin(yaw)
+ return new Vec3(-snYaw * csPitch, snPitch, -csYaw * csPitch)
+}
+
+class Cursor {
+ constructor (viewer, renderer, bot) {
+ // Init state
+ this.buttons = [false, false, false]
+ this.lastButtons = [false, false, false]
+ this.breakStartTime = 0
+ this.cursorBlock = null
+
+ // Setup graphics
+ const blockGeometry = new THREE.BoxGeometry(1.001, 1.001, 1.001)
+ this.cursorMesh = new THREE.LineSegments(new THREE.EdgesGeometry(blockGeometry), new THREE.LineBasicMaterial({ color: 0 }))
+ this.cursorMesh.visible = false
+ viewer.scene.add(this.cursorMesh)
+
+ const loader = new THREE.TextureLoader()
+ this.breakTextures = []
+ for (let i = 0; i < 10; i++) {
+ const texture = loader.load('textures/' + viewer.version + '/blocks/destroy_stage_' + i + '.png')
+ texture.magFilter = THREE.NearestFilter
+ texture.minFilter = THREE.NearestFilter
+ this.breakTextures.push(texture)
+ }
+ const breakMaterial = new THREE.MeshBasicMaterial({
+ transparent: true,
+ blending: THREE.MultiplyBlending
+ })
+ this.blockBreakMesh = new THREE.Mesh(blockGeometry, breakMaterial)
+ this.blockBreakMesh.visible = false
+ this.blockBreakMesh.renderOrder = 999
+ viewer.scene.add(this.blockBreakMesh)
+
+ // Setup events
+ document.addEventListener('mouseup', (e) => {
+ this.buttons[e.button] = false
+ })
+
+ document.addEventListener('mousedown', (e) => {
+ if (document.pointerLockElement !== renderer.domElement) return
+ this.buttons[e.button] = true
+
+ const entity = bot.nearestEntity((e) => {
+ if (e.position.distanceTo(bot.entity.position) <= (bot.player.gamemode === 1 ? 5 : 3)) {
+ const dir = getViewDirection(bot.entity.pitch, bot.entity.yaw)
+ const { width, height } = e
+ const { x: eX, y: eY, z: eZ } = e.position
+ const { x: bX, y: bY, z: bZ } = bot.entity.position
+ const box = new THREE.Box3(
+ new THREE.Vector3(eX - width / 2, eY, eZ - width / 2),
+ new THREE.Vector3(eX + width / 2, eY + height, eZ + width / 2)
+ )
+
+ const r = new THREE.Raycaster(
+ new THREE.Vector3(bX, bY + 1.52, bZ),
+ new THREE.Vector3(dir.x, dir.y, dir.z)
+ )
+ const int = r.ray.intersectBox(box, new THREE.Vector3(eX, eY, eZ))
+ return int !== null
+ }
+
+ return false
+ })
+
+ if (entity) {
+ bot.attack(entity)
+ }
+ })
+ this.lastPlaced = 4 // ticks since last placed
+ bot.on('physicsTick', () => { if (this.lastPlaced < 4) this.lastPlaced++ })
+ }
+
+ update (bot) {
+ let cursorBlock = bot.blockAtCursor(6)
+ if (!bot.canDigBlock(cursorBlock)) cursorBlock = null
+
+ let cursorChanged = !cursorBlock !== !this.cursorBlock
+ if (cursorBlock && this.cursorBlock) {
+ cursorChanged = !cursorBlock.position.equals(this.cursorBlock.position)
+ }
+
+ // Place
+ if (cursorBlock && this.buttons[2] && (!this.lastButtons[2] || cursorChanged) && this.lastPlaced >= 4) {
+ const vecArray = [new Vec3(0, -1, 0), new Vec3(0, 1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(1, 0, 0)]
+ const delta = cursorBlock.intersect.minus(cursorBlock.position)
+ bot._placeBlockWithOptions(cursorBlock, vecArray[cursorBlock.face], { delta, forceLook: 'ignore' })
+ bot.lastPlaced = 0
+ }
+
+ // Start break
+ if (cursorBlock && this.buttons[0] && (!this.lastButtons[0] || cursorChanged)) {
+ this.breakStartTime = performance.now()
+ try {
+ bot.dig(cursorBlock, 'ignore')
+ } catch (e) {} // we don't care if its aborted
+ }
+
+ // Stop break
+ if (!this.buttons[0] && this.lastButtons[0]) {
+ try {
+ bot.stopDigging() // this shouldnt throw anything...
+ } catch (e) {} // to be reworked in mineflayer, then remove the try here
+ }
+
+ // Show break animation
+ if (cursorBlock && this.buttons[0]) {
+ const elapsed = performance.now() - this.breakStartTime
+ const time = bot.digTime(cursorBlock)
+ const state = Math.floor((elapsed / time) * 10)
+ this.blockBreakMesh.position.set(cursorBlock.position.x + 0.5, cursorBlock.position.y + 0.5, cursorBlock.position.z + 0.5)
+ this.blockBreakMesh.material.map = this.breakTextures[state]
+ this.blockBreakMesh.visible = true
+ } else {
+ this.blockBreakMesh.visible = false
+ }
+
+ // Show cursor
+ if (!cursorBlock) {
+ this.cursorMesh.visible = false
+ } else {
+ this.cursorMesh.visible = true
+ this.cursorMesh.position.set(cursorBlock.position.x + 0.5, cursorBlock.position.y + 0.5, cursorBlock.position.z + 0.5)
+ }
+
+ // Update state
+ this.cursorBlock = cursorBlock
+ this.lastButtons[0] = this.buttons[0]
+ this.lastButtons[1] = this.buttons[1]
+ this.lastButtons[2] = this.buttons[2]
+ }
+}
+
+module.exports = Cursor
diff --git a/lib/dns.js b/lib/dns.js
new file mode 100644
index 0000000000000000000000000000000000000000..2f1dba6668746197dc1ae986eded4d9f80b7d0b8
--- /dev/null
+++ b/lib/dns.js
@@ -0,0 +1,39 @@
+/* global XMLHttpRequest */
+
+// Custom DNS resolver made by SiebeDW. Powered by google dns.
+// Supported: SRV (not all errors support)
+module.exports.resolveSrv = function (hostname, callback) {
+ const Http = new XMLHttpRequest()
+ const url = `https://dns.google.com/resolve?name=${hostname}&type=SRV`
+ Http.open('GET', url)
+ Http.responseType = 'json'
+ Http.send()
+
+ Http.onload = function () {
+ const response = Http.response
+ if (response.Status === 3) {
+ const err = new Error('querySrv ENOTFOUND')
+ err.code = 'ENOTFOUND'
+ callback(err)
+ return
+ }
+ if (!response.Answer || response.Answer.length < 1) {
+ const err = new Error('querySrv ENODATA')
+ err.code = 'ENODATA'
+ callback(err)
+ return
+ }
+ const willreturn = []
+ response.Answer.forEach(function (object) {
+ const data = object.data.split(' ')
+ willreturn.push({
+ priority: data[0],
+ weight: data[1],
+ port: data[2],
+ name: data[3]
+ })
+ })
+ console.log(willreturn)
+ callback(null, willreturn)
+ }
+}
diff --git a/lib/invsprite.json b/lib/invsprite.json
new file mode 100644
index 0000000000000000000000000000000000000000..e734e3c929354620498ecf93b8872c0a984cbb61
--- /dev/null
+++ b/lib/invsprite.json
@@ -0,0 +1,4812 @@
+{
+ "air": {
+ "name": "Air",
+ "x": 832,
+ "y": 2336
+ },
+ "stone": {
+ "name": "Stone",
+ "x": 224,
+ "y": 992
+ },
+ "granite": {
+ "name": "Granite",
+ "x": 96,
+ "y": 3360
+ },
+ "polished_granite": {
+ "name": "Polished Granite",
+ "x": 384,
+ "y": 896
+ },
+ "diorite": {
+ "name": "Diorite",
+ "x": 64,
+ "y": 3360
+ },
+ "polished_diorite": {
+ "name": "Polished Diorite",
+ "x": 352,
+ "y": 896
+ },
+ "andesite": {
+ "name": "Andesite",
+ "x": 832,
+ "y": 3328
+ },
+ "polished_andesite": {
+ "name": "Polished Andesite",
+ "x": 320,
+ "y": 896
+ },
+ "grass_block": {
+ "name": "Grass Block",
+ "x": 640,
+ "y": 960
+ },
+ "dirt": {
+ "name": "Dirt",
+ "x": 576,
+ "y": 960
+ },
+ "coarse_dirt": {
+ "name": "Coarse Dirt",
+ "x": 416,
+ "y": 960
+ },
+ "podzol": {
+ "name": "Podzol",
+ "x": 352,
+ "y": 3360
+ },
+ "crimson_nylium": {
+ "name": "Crimson Nylium",
+ "x": 640,
+ "y": 128
+ },
+ "warped_nylium": {
+ "name": "Warped Nylium",
+ "x": 768,
+ "y": 128
+ },
+ "cobblestone": {
+ "name": "Cobblestone",
+ "x": 960,
+ "y": 3328
+ },
+ "oak_planks": {
+ "name": "Oak Planks",
+ "x": 192,
+ "y": 896
+ },
+ "spruce_planks": {
+ "name": "Spruce Planks",
+ "x": 0,
+ "y": 928
+ },
+ "birch_planks": {
+ "name": "Birch Planks",
+ "x": 832,
+ "y": 832
+ },
+ "jungle_planks": {
+ "name": "Jungle Planks",
+ "x": 992,
+ "y": 864
+ },
+ "acacia_planks": {
+ "name": "Acacia Planks",
+ "x": 672,
+ "y": 832
+ },
+ "dark_oak_planks": {
+ "name": "Dark Oak Planks",
+ "x": 512,
+ "y": 864
+ },
+ "crimson_planks": {
+ "name": "Crimson Planks",
+ "x": 544,
+ "y": 128
+ },
+ "warped_planks": {
+ "name": "Warped Planks",
+ "x": 576,
+ "y": 128
+ },
+ "oak_sapling": {
+ "name": "Oak Sapling",
+ "x": 192,
+ "y": 3424
+ },
+ "spruce_sapling": {
+ "name": "Spruce Sapling",
+ "x": 608,
+ "y": 3424
+ },
+ "birch_sapling": {
+ "name": "Birch Sapling",
+ "x": 416,
+ "y": 3392
+ },
+ "jungle_sapling": {
+ "name": "Jungle Sapling",
+ "x": 960,
+ "y": 3392
+ },
+ "acacia_sapling": {
+ "name": "Acacia Sapling",
+ "x": 224,
+ "y": 3392
+ },
+ "dark_oak_sapling": {
+ "name": "Dark Oak Sapling",
+ "x": 768,
+ "y": 3392
+ },
+ "bedrock": {
+ "name": "Bedrock",
+ "x": 320,
+ "y": 960
+ },
+ "sand": {
+ "name": "Sand",
+ "x": 480,
+ "y": 3360
+ },
+ "red_sand": {
+ "name": "Red Sand",
+ "x": 416,
+ "y": 3360
+ },
+ "gravel": {
+ "name": "Gravel",
+ "x": 128,
+ "y": 3360
+ },
+ "gold_ore": {
+ "name": "Gold Ore",
+ "x": 32,
+ "y": 3392
+ },
+ "iron_ore": {
+ "name": "Iron Ore",
+ "x": 64,
+ "y": 3392
+ },
+ "coal_ore": {
+ "name": "Coal Ore",
+ "x": 960,
+ "y": 3360
+ },
+ "nether_gold_ore": {
+ "name": "Nether Gold Ore",
+ "x": 288,
+ "y": 192
+ },
+ "oak_log": {
+ "name": "Oak Log",
+ "x": 160,
+ "y": 3424
+ },
+ "spruce_log": {
+ "name": "Spruce Log",
+ "x": 576,
+ "y": 3424
+ },
+ "birch_log": {
+ "name": "Birch Log",
+ "x": 384,
+ "y": 3392
+ },
+ "jungle_log": {
+ "name": "Jungle Log",
+ "x": 928,
+ "y": 3392
+ },
+ "acacia_log": {
+ "name": "Acacia Log",
+ "x": 192,
+ "y": 3392
+ },
+ "dark_oak_log": {
+ "name": "Dark Oak Log",
+ "x": 736,
+ "y": 3392
+ },
+ "crimson_stem": {
+ "name": "Crimson Stem",
+ "x": 672,
+ "y": 128
+ },
+ "warped_stem": {
+ "name": "Warped Stem",
+ "x": 800,
+ "y": 128
+ },
+ "stripped_oak_log": {
+ "name": "Stripped Oak Log",
+ "x": 288,
+ "y": 1664
+ },
+ "stripped_spruce_log": {
+ "name": "Stripped Spruce Log",
+ "x": 320,
+ "y": 1664
+ },
+ "stripped_birch_log": {
+ "name": "Stripped Birch Log",
+ "x": 192,
+ "y": 1664
+ },
+ "stripped_jungle_log": {
+ "name": "Stripped Jungle Log",
+ "x": 256,
+ "y": 1664
+ },
+ "stripped_acacia_log": {
+ "name": "Stripped Acacia Log",
+ "x": 160,
+ "y": 1664
+ },
+ "stripped_dark_oak_log": {
+ "name": "Stripped Dark Oak Log",
+ "x": 224,
+ "y": 1664
+ },
+ "stripped_crimson_stem": {
+ "name": "Stripped Crimson Stem",
+ "x": 448,
+ "y": 160
+ },
+ "stripped_warped_stem": {
+ "name": "Stripped Warped Stem",
+ "x": 480,
+ "y": 160
+ },
+ "stripped_oak_wood": {
+ "name": "Stripped Oak Wood",
+ "x": 576,
+ "y": 2080
+ },
+ "stripped_spruce_wood": {
+ "name": "Stripped Spruce Wood",
+ "x": 608,
+ "y": 2080
+ },
+ "stripped_birch_wood": {
+ "name": "Stripped Birch Wood",
+ "x": 640,
+ "y": 2080
+ },
+ "stripped_jungle_wood": {
+ "name": "Stripped Jungle Wood",
+ "x": 672,
+ "y": 2080
+ },
+ "stripped_acacia_wood": {
+ "name": "Stripped Acacia Wood",
+ "x": 704,
+ "y": 2080
+ },
+ "stripped_dark_oak_wood": {
+ "name": "Stripped Dark Oak Wood",
+ "x": 736,
+ "y": 2080
+ },
+ "stripped_crimson_hyphae": {
+ "name": "Stripped Crimson Hyphae",
+ "x": 224,
+ "y": 192
+ },
+ "stripped_warped_hyphae": {
+ "name": "Stripped Warped Hyphae",
+ "x": 256,
+ "y": 192
+ },
+ "oak_wood": {
+ "name": "Oak Wood",
+ "x": 704,
+ "y": 3264
+ },
+ "spruce_wood": {
+ "name": "Spruce Wood",
+ "x": 736,
+ "y": 3296
+ },
+ "birch_wood": {
+ "name": "Birch Wood",
+ "x": 0,
+ "y": 3232
+ },
+ "jungle_wood": {
+ "name": "Jungle Wood",
+ "x": 192,
+ "y": 3264
+ },
+ "acacia_wood": {
+ "name": "Acacia Wood",
+ "x": 768,
+ "y": 3200
+ },
+ "dark_oak_wood": {
+ "name": "Dark Oak Wood",
+ "x": 704,
+ "y": 3232
+ },
+ "crimson_hyphae": {
+ "name": "Crimson Hyphae",
+ "x": 160,
+ "y": 192
+ },
+ "warped_hyphae": {
+ "name": "Warped Hyphae",
+ "x": 192,
+ "y": 192
+ },
+ "oak_leaves": {
+ "name": "Oak Leaves",
+ "x": 128,
+ "y": 3424
+ },
+ "spruce_leaves": {
+ "name": "Spruce Leaves",
+ "x": 544,
+ "y": 3424
+ },
+ "birch_leaves": {
+ "name": "Birch Leaves",
+ "x": 352,
+ "y": 3392
+ },
+ "jungle_leaves": {
+ "name": "Jungle Leaves",
+ "x": 896,
+ "y": 3392
+ },
+ "acacia_leaves": {
+ "name": "Acacia Leaves",
+ "x": 160,
+ "y": 3392
+ },
+ "dark_oak_leaves": {
+ "name": "Dark Oak Leaves",
+ "x": 704,
+ "y": 3392
+ },
+ "sponge": {
+ "name": "Sponge",
+ "x": 896,
+ "y": 3584
+ },
+ "wet_sponge": {
+ "name": "Wet Sponge",
+ "x": 0,
+ "y": 3616
+ },
+ "glass": {
+ "name": "Glass",
+ "x": 960,
+ "y": 3232
+ },
+ "lapis_ore": {
+ "name": "Lapis Lazuli Ore",
+ "x": 96,
+ "y": 3392
+ },
+ "lapis_block": {
+ "name": "Lapis Lazuli Block",
+ "x": 256,
+ "y": 3264
+ },
+ "dispenser": {
+ "name": "Dispenser",
+ "x": 576,
+ "y": 3584
+ },
+ "sandstone": {
+ "name": "Sandstone",
+ "x": 512,
+ "y": 3360
+ },
+ "chiseled_sandstone": {
+ "name": "Chiseled Sandstone",
+ "x": 384,
+ "y": 3232
+ },
+ "cut_sandstone": {
+ "name": "Cut Sandstone",
+ "x": 544,
+ "y": 3232
+ },
+ "note_block": {
+ "name": "Note Block",
+ "x": 832,
+ "y": 3584
+ },
+ "powered_rail": {
+ "name": "Powered Rail",
+ "x": 448,
+ "y": 3328
+ },
+ "detector_rail": {
+ "name": "Detector Rail",
+ "x": 160,
+ "y": 3328
+ },
+ "sticky_piston": {
+ "name": "Sticky Piston",
+ "x": 704,
+ "y": 3328
+ },
+ "cobweb": {
+ "name": "Cobweb",
+ "x": 992,
+ "y": 3328
+ },
+ "grass": {
+ "name": "Grass",
+ "x": 864,
+ "y": 3392
+ },
+ "fern": {
+ "name": "Fern",
+ "x": 832,
+ "y": 3392
+ },
+ "dead_bush": {
+ "name": "Dead Bush",
+ "x": 800,
+ "y": 3392
+ },
+ "seagrass": {
+ "name": "Seagrass",
+ "x": 384,
+ "y": 2016
+ },
+ "sea_pickle": {
+ "name": "Sea Pickle",
+ "x": 224,
+ "y": 1728
+ },
+ "piston": {
+ "name": "Piston",
+ "x": 416,
+ "y": 3328
+ },
+ "white_wool": {
+ "name": "White Wool",
+ "x": 0,
+ "y": 1536
+ },
+ "orange_wool": {
+ "name": "Orange Wool",
+ "x": 896,
+ "y": 1504
+ },
+ "magenta_wool": {
+ "name": "Magenta Wool",
+ "x": 864,
+ "y": 1504
+ },
+ "light_blue_wool": {
+ "name": "Light Blue Wool",
+ "x": 768,
+ "y": 1504
+ },
+ "yellow_wool": {
+ "name": "Yellow Wool",
+ "x": 32,
+ "y": 1536
+ },
+ "lime_wool": {
+ "name": "Lime Wool",
+ "x": 832,
+ "y": 1504
+ },
+ "pink_wool": {
+ "name": "Pink Wool",
+ "x": 928,
+ "y": 1504
+ },
+ "gray_wool": {
+ "name": "Gray Wool",
+ "x": 704,
+ "y": 1504
+ },
+ "light_gray_wool": {
+ "name": "Light Gray Wool",
+ "x": 800,
+ "y": 1504
+ },
+ "cyan_wool": {
+ "name": "Cyan Wool",
+ "x": 672,
+ "y": 1504
+ },
+ "purple_wool": {
+ "name": "Purple Wool",
+ "x": 960,
+ "y": 1504
+ },
+ "blue_wool": {
+ "name": "Blue Wool",
+ "x": 608,
+ "y": 1504
+ },
+ "brown_wool": {
+ "name": "Brown Wool",
+ "x": 640,
+ "y": 1504
+ },
+ "green_wool": {
+ "name": "Green Wool",
+ "x": 736,
+ "y": 1504
+ },
+ "red_wool": {
+ "name": "Red Wool",
+ "x": 992,
+ "y": 1504
+ },
+ "black_wool": {
+ "name": "Black Wool",
+ "x": 576,
+ "y": 1504
+ },
+ "dandelion": {
+ "name": "Dandelion",
+ "x": 672,
+ "y": 3392
+ },
+ "poppy": {
+ "name": "Poppy",
+ "x": 352,
+ "y": 3424
+ },
+ "blue_orchid": {
+ "name": "Blue Orchid",
+ "x": 448,
+ "y": 3392
+ },
+ "allium": {
+ "name": "Allium",
+ "x": 256,
+ "y": 3392
+ },
+ "azure_bluet": {
+ "name": "Azure Bluet",
+ "x": 288,
+ "y": 3392
+ },
+ "red_tulip": {
+ "name": "Red Tulip",
+ "x": 480,
+ "y": 3424
+ },
+ "orange_tulip": {
+ "name": "Orange Tulip",
+ "x": 224,
+ "y": 3424
+ },
+ "white_tulip": {
+ "name": "White Tulip",
+ "x": 768,
+ "y": 3424
+ },
+ "pink_tulip": {
+ "name": "Pink Tulip",
+ "x": 320,
+ "y": 3424
+ },
+ "oxeye_daisy": {
+ "name": "Oxeye Daisy",
+ "x": 256,
+ "y": 3424
+ },
+ "cornflower": {
+ "name": "Cornflower",
+ "x": 640,
+ "y": 2368
+ },
+ "lily_of_the_valley": {
+ "name": "Lily of the Valley",
+ "x": 672,
+ "y": 2368
+ },
+ "wither_rose": {
+ "name": "Wither Rose",
+ "x": 608,
+ "y": 2368
+ },
+ "brown_mushroom": {
+ "name": "Brown Mushroom",
+ "x": 512,
+ "y": 3392
+ },
+ "red_mushroom": {
+ "name": "Red Mushroom",
+ "x": 448,
+ "y": 3424
+ },
+ "crimson_fungus": {
+ "name": "Crimson Fungus",
+ "x": 928,
+ "y": 128
+ },
+ "warped_fungus": {
+ "name": "Warped Fungus",
+ "x": 960,
+ "y": 128
+ },
+ "crimson_roots": {
+ "name": "Crimson Roots",
+ "x": 928,
+ "y": 160
+ },
+ "warped_roots": {
+ "name": "Warped Roots",
+ "x": 992,
+ "y": 160
+ },
+ "nether_sprouts": {
+ "name": "Nether Sprouts",
+ "x": 928,
+ "y": 64
+ },
+ "weeping_vines": {
+ "name": "Weeping Vines",
+ "x": 992,
+ "y": 128
+ },
+ "twisting_vines": {
+ "name": "Twisting Vines",
+ "x": 320,
+ "y": 192
+ },
+ "sugar_cane": {
+ "name": "Sugar Cane",
+ "x": 640,
+ "y": 3424
+ },
+ "kelp": {
+ "name": "Kelp",
+ "x": 576,
+ "y": 1664
+ },
+ "bamboo": {
+ "name": "Bamboo",
+ "x": 128,
+ "y": 2368
+ },
+ "gold_block": {
+ "name": "Block of Gold",
+ "x": 128,
+ "y": 3232
+ },
+ "iron_block": {
+ "name": "Block of Iron",
+ "x": 160,
+ "y": 3232
+ },
+ "oak_slab": {
+ "name": "Oak Slab",
+ "x": 640,
+ "y": 3264
+ },
+ "spruce_slab": {
+ "name": "Spruce Slab",
+ "x": 672,
+ "y": 3296
+ },
+ "birch_slab": {
+ "name": "Birch Slab",
+ "x": 960,
+ "y": 3200
+ },
+ "jungle_slab": {
+ "name": "Jungle Slab",
+ "x": 128,
+ "y": 3264
+ },
+ "acacia_slab": {
+ "name": "Acacia Slab",
+ "x": 704,
+ "y": 3200
+ },
+ "dark_oak_slab": {
+ "name": "Dark Oak Slab",
+ "x": 640,
+ "y": 3232
+ },
+ "crimson_slab": {
+ "name": "Crimson Slab",
+ "x": 32,
+ "y": 160
+ },
+ "warped_slab": {
+ "name": "Warped Slab",
+ "x": 128,
+ "y": 160
+ },
+ "stone_slab": {
+ "name": "Stone Slab",
+ "x": 864,
+ "y": 3296
+ },
+ "smooth_stone_slab": {
+ "name": "Smooth Stone Slab",
+ "x": 544,
+ "y": 3296
+ },
+ "sandstone_slab": {
+ "name": "Sandstone Slab",
+ "x": 192,
+ "y": 3296
+ },
+ "cut_sandstone_slab": {
+ "name": "Cut Sandstone Slab",
+ "x": 64,
+ "y": 2464
+ },
+ "petrified_oak_slab": {
+ "name": "Petrified Oak Slab",
+ "x": 224,
+ "y": 896
+ },
+ "cobblestone_slab": {
+ "name": "Cobblestone Slab",
+ "x": 416,
+ "y": 3232
+ },
+ "brick_slab": {
+ "name": "Brick Slab",
+ "x": 256,
+ "y": 3232
+ },
+ "stone_brick_slab": {
+ "name": "Stone Brick Slab",
+ "x": 128,
+ "y": 928
+ },
+ "nether_brick_slab": {
+ "name": "Nether Brick Slab",
+ "x": 480,
+ "y": 3264
+ },
+ "quartz_slab": {
+ "name": "Quartz Slab",
+ "x": 960,
+ "y": 3264
+ },
+ "red_sandstone_slab": {
+ "name": "Red Sandstone Slab",
+ "x": 96,
+ "y": 3296
+ },
+ "cut_red_sandstone_slab": {
+ "name": "Cut Red Sandstone Slab",
+ "x": 32,
+ "y": 2464
+ },
+ "purpur_slab": {
+ "name": "Purpur Slab",
+ "x": 896,
+ "y": 3264
+ },
+ "prismarine_slab": {
+ "name": "Dark Prismarine Slab",
+ "x": 736,
+ "y": 3232
+ },
+ "prismarine_brick_slab": {
+ "name": "Prismarine Brick Slab",
+ "x": 864,
+ "y": 3264
+ },
+ "dark_prismarine_slab": {
+ "name": "Dark Prismarine Slab",
+ "x": 736,
+ "y": 3232
+ },
+ "smooth_quartz": {
+ "name": "Smooth Quartz Block",
+ "x": 64,
+ "y": 864
+ },
+ "smooth_red_sandstone": {
+ "name": "Smooth Red Sandstone",
+ "x": 416,
+ "y": 3296
+ },
+ "smooth_sandstone": {
+ "name": "Smooth Sandstone",
+ "x": 512,
+ "y": 3296
+ },
+ "smooth_stone": {
+ "name": "Smooth Stone",
+ "x": 832,
+ "y": 896
+ },
+ "bricks": {
+ "name": "Bricks",
+ "x": 416,
+ "y": 96
+ },
+ "tnt": {
+ "name": "TNT",
+ "x": 960,
+ "y": 3584
+ },
+ "bookshelf": {
+ "name": "Bookshelf",
+ "x": 224,
+ "y": 3232
+ },
+ "mossy_cobblestone": {
+ "name": "Mossy Cobblestone",
+ "x": 192,
+ "y": 3360
+ },
+ "obsidian": {
+ "name": "Obsidian",
+ "x": 288,
+ "y": 3360
+ },
+ "torch": {
+ "name": "Torch",
+ "x": 928,
+ "y": 3360
+ },
+ "end_rod": {
+ "name": "End Rod",
+ "x": 672,
+ "y": 3168
+ },
+ "chorus_plant": {
+ "name": "Chorus Plant",
+ "x": 640,
+ "y": 3392
+ },
+ "chorus_flower": {
+ "name": "Chorus Flower",
+ "x": 608,
+ "y": 3392
+ },
+ "purpur_block": {
+ "name": "Purpur Block",
+ "x": 416,
+ "y": 896
+ },
+ "purpur_pillar": {
+ "name": "Purpur Pillar",
+ "x": 448,
+ "y": 896
+ },
+ "purpur_stairs": {
+ "name": "Purpur Stairs",
+ "x": 928,
+ "y": 3264
+ },
+ "spawner": {
+ "name": "Spawner",
+ "x": 576,
+ "y": 3360
+ },
+ "oak_stairs": {
+ "name": "Oak Stairs",
+ "x": 672,
+ "y": 3264
+ },
+ "chest": {
+ "name": "Chest",
+ "x": 704,
+ "y": 1216
+ },
+ "diamond_ore": {
+ "name": "Diamond Ore",
+ "x": 992,
+ "y": 3360
+ },
+ "diamond_block": {
+ "name": "Block of Diamond",
+ "x": 64,
+ "y": 3232
+ },
+ "crafting_table": {
+ "name": "Crafting Table",
+ "x": 512,
+ "y": 3584
+ },
+ "farmland": {
+ "name": "Farmland",
+ "x": 672,
+ "y": 864
+ },
+ "furnace": {
+ "name": "Furnace",
+ "x": 704,
+ "y": 3584
+ },
+ "ladder": {
+ "name": "Ladder",
+ "x": 224,
+ "y": 3264
+ },
+ "rail": {
+ "name": "Rail",
+ "x": 480,
+ "y": 3328
+ },
+ "cobblestone_stairs": {
+ "name": "Cobblestone Stairs",
+ "x": 448,
+ "y": 3232
+ },
+ "lever": {
+ "name": "Lever",
+ "x": 288,
+ "y": 3328
+ },
+ "stone_pressure_plate": {
+ "name": "Stone Pressure Plate",
+ "x": 768,
+ "y": 3328
+ },
+ "oak_pressure_plate": {
+ "name": "Oak Pressure Plate",
+ "x": 384,
+ "y": 3328
+ },
+ "spruce_pressure_plate": {
+ "name": "Spruce Pressure Plate",
+ "x": 672,
+ "y": 3328
+ },
+ "birch_pressure_plate": {
+ "name": "Birch Pressure Plate",
+ "x": 32,
+ "y": 3328
+ },
+ "jungle_pressure_plate": {
+ "name": "Jungle Pressure Plate",
+ "x": 256,
+ "y": 3328
+ },
+ "acacia_pressure_plate": {
+ "name": "Acacia Pressure Plate",
+ "x": 960,
+ "y": 3296
+ },
+ "dark_oak_pressure_plate": {
+ "name": "Dark Oak Pressure Plate",
+ "x": 128,
+ "y": 3328
+ },
+ "crimson_pressure_plate": {
+ "name": "Crimson Pressure Plate",
+ "x": 256,
+ "y": 160
+ },
+ "warped_pressure_plate": {
+ "name": "Warped Pressure Plate",
+ "x": 384,
+ "y": 160
+ },
+ "polished_blackstone_pressure_plate": {
+ "name": "Polished Blackstone Pressure Plate",
+ "x": 928,
+ "y": 256
+ },
+ "redstone_ore": {
+ "name": "Redstone Ore",
+ "x": 128,
+ "y": 3392
+ },
+ "redstone_torch": {
+ "name": "Redstone Torch",
+ "x": 608,
+ "y": 3328
+ },
+ "snow": {
+ "name": "Snow",
+ "x": 544,
+ "y": 3360
+ },
+ "ice": {
+ "name": "Ice",
+ "x": 160,
+ "y": 3360
+ },
+ "snow_block": {
+ "name": "Snow Block",
+ "x": 576,
+ "y": 3296
+ },
+ "cactus": {
+ "name": "Cactus",
+ "x": 544,
+ "y": 3392
+ },
+ "clay": {
+ "name": "Clay",
+ "x": 928,
+ "y": 3328
+ },
+ "jukebox": {
+ "name": "Jukebox",
+ "x": 768,
+ "y": 3584
+ },
+ "oak_fence": {
+ "name": "Oak Fence",
+ "x": 608,
+ "y": 3264
+ },
+ "spruce_fence": {
+ "name": "Spruce Fence",
+ "x": 640,
+ "y": 3296
+ },
+ "birch_fence": {
+ "name": "Birch Fence",
+ "x": 928,
+ "y": 3200
+ },
+ "jungle_fence": {
+ "name": "Jungle Fence",
+ "x": 96,
+ "y": 3264
+ },
+ "acacia_fence": {
+ "name": "Acacia Fence",
+ "x": 672,
+ "y": 3200
+ },
+ "dark_oak_fence": {
+ "name": "Dark Oak Fence",
+ "x": 608,
+ "y": 3232
+ },
+ "crimson_fence": {
+ "name": "Crimson Fence",
+ "x": 0,
+ "y": 160
+ },
+ "warped_fence": {
+ "name": "Warped Fence",
+ "x": 96,
+ "y": 160
+ },
+ "pumpkin": {
+ "name": "Pumpkin",
+ "x": 384,
+ "y": 3424
+ },
+ "carved_pumpkin": {
+ "name": "Carved Pumpkin",
+ "x": 576,
+ "y": 3392
+ },
+ "netherrack": {
+ "name": "Netherrack",
+ "x": 832,
+ "y": 3360
+ },
+ "soul_sand": {
+ "name": "Soul Sand",
+ "x": 864,
+ "y": 3360
+ },
+ "soul_soil": {
+ "name": "Soul Soil",
+ "x": 736,
+ "y": 128
+ },
+ "basalt": {
+ "name": "Basalt",
+ "x": 608,
+ "y": 128
+ },
+ "polished_basalt": {
+ "name": "Polished Basalt",
+ "x": 416,
+ "y": 192
+ },
+ "soul_torch": {
+ "name": "Soul Torch",
+ "x": 864,
+ "y": 128
+ },
+ "glowstone": {
+ "name": "Glowstone",
+ "x": 672,
+ "y": 3360
+ },
+ "jack_o_lantern": {
+ "name": "Jack o'Lantern",
+ "x": 32,
+ "y": 3264
+ },
+ "oak_trapdoor": {
+ "name": "Oak Trapdoor",
+ "x": 928,
+ "y": 928
+ },
+ "spruce_trapdoor": {
+ "name": "Spruce Trapdoor",
+ "x": 448,
+ "y": 1632
+ },
+ "birch_trapdoor": {
+ "name": "Birch Trapdoor",
+ "x": 480,
+ "y": 1632
+ },
+ "jungle_trapdoor": {
+ "name": "Jungle Trapdoor",
+ "x": 512,
+ "y": 1632
+ },
+ "acacia_trapdoor": {
+ "name": "Acacia Trapdoor",
+ "x": 544,
+ "y": 1632
+ },
+ "dark_oak_trapdoor": {
+ "name": "Dark Oak Trapdoor",
+ "x": 576,
+ "y": 1632
+ },
+ "crimson_trapdoor": {
+ "name": "Crimson Trapdoor",
+ "x": 288,
+ "y": 160
+ },
+ "warped_trapdoor": {
+ "name": "Warped Trapdoor",
+ "x": 416,
+ "y": 160
+ },
+ "infested_stone": {
+ "name": "Infested Stone",
+ "x": 224,
+ "y": 992
+ },
+ "infested_cobblestone": {
+ "name": "Infested Cobblestone",
+ "x": 960,
+ "y": 3328
+ },
+ "infested_stone_bricks": {
+ "name": "Infested Stone Bricks",
+ "x": 608,
+ "y": 3360
+ },
+ "infested_mossy_stone_bricks": {
+ "name": "Infested Mossy Stone Bricks",
+ "x": 224,
+ "y": 3360
+ },
+ "infested_cracked_stone_bricks": {
+ "name": "Infested Cracked Stone Bricks",
+ "x": 0,
+ "y": 3360
+ },
+ "infested_chiseled_stone_bricks": {
+ "name": "Infested Chiseled Stone Bricks",
+ "x": 896,
+ "y": 3328
+ },
+ "stone_bricks": {
+ "name": "Stone Bricks",
+ "x": 608,
+ "y": 3360
+ },
+ "mossy_stone_bricks": {
+ "name": "Mossy Stone Bricks",
+ "x": 224,
+ "y": 3360
+ },
+ "cracked_stone_bricks": {
+ "name": "Cracked Stone Bricks",
+ "x": 0,
+ "y": 3360
+ },
+ "chiseled_stone_bricks": {
+ "name": "Chiseled Stone Bricks",
+ "x": 896,
+ "y": 3328
+ },
+ "brown_mushroom_block": {
+ "name": "Brown Mushroom Block",
+ "x": 480,
+ "y": 3392
+ },
+ "red_mushroom_block": {
+ "name": "Red Mushroom Block",
+ "x": 416,
+ "y": 3424
+ },
+ "mushroom_stem": {
+ "name": "Mushroom Stem",
+ "x": 96,
+ "y": 3424
+ },
+ "iron_bars": {
+ "name": "Iron Bars",
+ "x": 0,
+ "y": 3264
+ },
+ "chain": {
+ "name": "Chain",
+ "x": 32,
+ "y": 288
+ },
+ "glass_pane": {
+ "name": "Glass Pane",
+ "x": 928,
+ "y": 3232
+ },
+ "melon": {
+ "name": "Melon",
+ "x": 64,
+ "y": 3424
+ },
+ "vine": {
+ "name": "Vines",
+ "x": 736,
+ "y": 3424
+ },
+ "oak_fence_gate": {
+ "name": "Oak Fence Gate",
+ "x": 576,
+ "y": 3264
+ },
+ "spruce_fence_gate": {
+ "name": "Spruce Fence Gate",
+ "x": 608,
+ "y": 3296
+ },
+ "birch_fence_gate": {
+ "name": "Birch Fence Gate",
+ "x": 896,
+ "y": 3200
+ },
+ "jungle_fence_gate": {
+ "name": "Jungle Fence Gate",
+ "x": 64,
+ "y": 3264
+ },
+ "acacia_fence_gate": {
+ "name": "Acacia Fence Gate",
+ "x": 640,
+ "y": 3200
+ },
+ "dark_oak_fence_gate": {
+ "name": "Dark Oak Fence Gate",
+ "x": 576,
+ "y": 3232
+ },
+ "crimson_fence_gate": {
+ "name": "Crimson Fence Gate",
+ "x": 576,
+ "y": 160
+ },
+ "warped_fence_gate": {
+ "name": "Warped Fence Gate",
+ "x": 608,
+ "y": 160
+ },
+ "brick_stairs": {
+ "name": "Brick Stairs",
+ "x": 288,
+ "y": 3232
+ },
+ "stone_brick_stairs": {
+ "name": "Stone Brick Stairs",
+ "x": 800,
+ "y": 3296
+ },
+ "mycelium": {
+ "name": "Mycelium",
+ "x": 256,
+ "y": 3360
+ },
+ "lily_pad": {
+ "name": "Lily Pad",
+ "x": 32,
+ "y": 3424
+ },
+ "nether_bricks": {
+ "name": "Nether Bricks",
+ "x": 768,
+ "y": 3360
+ },
+ "cracked_nether_bricks": {
+ "name": "Cracked Nether Bricks",
+ "x": 448,
+ "y": 256
+ },
+ "chiseled_nether_bricks": {
+ "name": "Chiseled Nether Bricks",
+ "x": 384,
+ "y": 256
+ },
+ "nether_brick_fence": {
+ "name": "Nether Brick Fence",
+ "x": 704,
+ "y": 3360
+ },
+ "nether_brick_stairs": {
+ "name": "Nether Brick Stairs",
+ "x": 736,
+ "y": 3360
+ },
+ "enchanting_table": {
+ "name": "Enchanting Table",
+ "x": 640,
+ "y": 3584
+ },
+ "end_portal_frame": {
+ "name": "End Portal Frame",
+ "x": 640,
+ "y": 3168
+ },
+ "end_stone": {
+ "name": "End Stone",
+ "x": 704,
+ "y": 3168
+ },
+ "end_stone_bricks": {
+ "name": "End Stone Bricks",
+ "x": 896,
+ "y": 3232
+ },
+ "dragon_egg": {
+ "name": "Dragon Egg",
+ "x": 576,
+ "y": 3168
+ },
+ "redstone_lamp": {
+ "name": "Redstone Lamp",
+ "x": 544,
+ "y": 3328
+ },
+ "sandstone_stairs": {
+ "name": "Sandstone Stairs",
+ "x": 224,
+ "y": 3296
+ },
+ "emerald_ore": {
+ "name": "Emerald Ore",
+ "x": 0,
+ "y": 3392
+ },
+ "ender_chest": {
+ "name": "Ender Chest",
+ "x": 672,
+ "y": 3584
+ },
+ "tripwire_hook": {
+ "name": "Tripwire Hook",
+ "x": 800,
+ "y": 3328
+ },
+ "emerald_block": {
+ "name": "Block of Emerald",
+ "x": 96,
+ "y": 3232
+ },
+ "spruce_stairs": {
+ "name": "Spruce Stairs",
+ "x": 704,
+ "y": 3296
+ },
+ "birch_stairs": {
+ "name": "Birch Stairs",
+ "x": 992,
+ "y": 3200
+ },
+ "jungle_stairs": {
+ "name": "Jungle Stairs",
+ "x": 160,
+ "y": 3264
+ },
+ "crimson_stairs": {
+ "name": "Crimson Stairs",
+ "x": 64,
+ "y": 160
+ },
+ "warped_stairs": {
+ "name": "Warped Stairs",
+ "x": 160,
+ "y": 160
+ },
+ "command_block": {
+ "name": "Impulse Command Block Revision 1",
+ "x": 736,
+ "y": 1216
+ },
+ "beacon": {
+ "name": "Beacon",
+ "x": 352,
+ "y": 3584
+ },
+ "cobblestone_wall": {
+ "name": "Cobblestone Wall",
+ "x": 480,
+ "y": 3232
+ },
+ "mossy_cobblestone_wall": {
+ "name": "Mossy Cobblestone Wall",
+ "x": 352,
+ "y": 3264
+ },
+ "brick_wall": {
+ "name": "Brick Wall",
+ "x": 320,
+ "y": 3232
+ },
+ "prismarine_wall": {
+ "name": "Prismarine Wall",
+ "x": 0,
+ "y": 96
+ },
+ "red_sandstone_wall": {
+ "name": "Red Sandstone Wall",
+ "x": 160,
+ "y": 3296
+ },
+ "mossy_stone_brick_wall": {
+ "name": "Mossy Stone Brick Wall",
+ "x": 448,
+ "y": 3264
+ },
+ "granite_wall": {
+ "name": "Granite Wall",
+ "x": 32,
+ "y": 2400
+ },
+ "stone_brick_wall": {
+ "name": "Stone Brick Wall",
+ "x": 832,
+ "y": 3296
+ },
+ "nether_brick_wall": {
+ "name": "Nether Brick Wall",
+ "x": 512,
+ "y": 3264
+ },
+ "andesite_wall": {
+ "name": "Andesite Wall",
+ "x": 864,
+ "y": 3200
+ },
+ "red_nether_brick_wall": {
+ "name": "Red Nether Brick Wall",
+ "x": 64,
+ "y": 3296
+ },
+ "sandstone_wall": {
+ "name": "Sandstone Wall",
+ "x": 256,
+ "y": 3296
+ },
+ "end_stone_brick_wall": {
+ "name": "End Stone Brick Wall",
+ "x": 0,
+ "y": 2400
+ },
+ "diorite_wall": {
+ "name": "Diorite Wall",
+ "x": 864,
+ "y": 3232
+ },
+ "blackstone_wall": {
+ "name": "Blackstone Wall",
+ "x": 672,
+ "y": 256
+ },
+ "polished_blackstone_wall": {
+ "name": "Polished Blackstone Wall",
+ "x": 864,
+ "y": 256
+ },
+ "polished_blackstone_brick_wall": {
+ "name": "Polished Blackstone Brick Wall",
+ "x": 768,
+ "y": 256
+ },
+ "stone_button": {
+ "name": "Stone Button",
+ "x": 736,
+ "y": 3328
+ },
+ "oak_button": {
+ "name": "Oak Button",
+ "x": 352,
+ "y": 3328
+ },
+ "spruce_button": {
+ "name": "Spruce Button",
+ "x": 640,
+ "y": 3328
+ },
+ "birch_button": {
+ "name": "Birch Button",
+ "x": 0,
+ "y": 3328
+ },
+ "jungle_button": {
+ "name": "Jungle Button",
+ "x": 224,
+ "y": 3328
+ },
+ "acacia_button": {
+ "name": "Acacia Button",
+ "x": 928,
+ "y": 3296
+ },
+ "dark_oak_button": {
+ "name": "Dark Oak Button",
+ "x": 96,
+ "y": 3328
+ },
+ "crimson_button": {
+ "name": "Crimson Button",
+ "x": 192,
+ "y": 160
+ },
+ "warped_button": {
+ "name": "Warped Button",
+ "x": 320,
+ "y": 160
+ },
+ "polished_blackstone_button": {
+ "name": "Polished Blackstone Button",
+ "x": 896,
+ "y": 256
+ },
+ "anvil": {
+ "name": "Anvil",
+ "x": 288,
+ "y": 3584
+ },
+ "chipped_anvil": {
+ "name": "Chipped Anvil",
+ "x": 480,
+ "y": 3584
+ },
+ "damaged_anvil": {
+ "name": "Damaged Anvil",
+ "x": 544,
+ "y": 3584
+ },
+ "trapped_chest": {
+ "name": "Trapped Chest",
+ "x": 992,
+ "y": 3584
+ },
+ "light_weighted_pressure_plate": {
+ "name": "Light Weighted Pressure Plate",
+ "x": 320,
+ "y": 3328
+ },
+ "heavy_weighted_pressure_plate": {
+ "name": "Heavy Weighted Pressure Plate",
+ "x": 192,
+ "y": 3328
+ },
+ "daylight_detector": {
+ "name": "Daylight Detector",
+ "x": 800,
+ "y": 1216
+ },
+ "redstone_block": {
+ "name": "Block of Redstone",
+ "x": 64,
+ "y": 3328
+ },
+ "nether_quartz_ore": {
+ "name": "Nether Quartz Ore",
+ "x": 800,
+ "y": 3360
+ },
+ "hopper": {
+ "name": "Hopper",
+ "x": 736,
+ "y": 3584
+ },
+ "chiseled_quartz_block": {
+ "name": "Chiseled Quartz Block",
+ "x": 224,
+ "y": 864
+ },
+ "quartz_block": {
+ "name": "Block of Quartz",
+ "x": 192,
+ "y": 3232
+ },
+ "quartz_bricks": {
+ "name": "Quartz Bricks",
+ "x": 992,
+ "y": 256
+ },
+ "quartz_pillar": {
+ "name": "Quartz Pillar",
+ "x": 288,
+ "y": 896
+ },
+ "quartz_stairs": {
+ "name": "Quartz Stairs",
+ "x": 992,
+ "y": 3264
+ },
+ "activator_rail": {
+ "name": "Activator Rail",
+ "x": 992,
+ "y": 3296
+ },
+ "dropper": {
+ "name": "Dropper",
+ "x": 608,
+ "y": 3584
+ },
+ "white_terracotta": {
+ "name": "White Terracotta",
+ "x": 672,
+ "y": 768
+ },
+ "orange_terracotta": {
+ "name": "Orange Terracotta",
+ "x": 928,
+ "y": 736
+ },
+ "magenta_terracotta": {
+ "name": "Magenta Terracotta",
+ "x": 736,
+ "y": 736
+ },
+ "light_blue_terracotta": {
+ "name": "Light Blue Terracotta",
+ "x": 160,
+ "y": 736
+ },
+ "yellow_terracotta": {
+ "name": "Yellow Terracotta",
+ "x": 864,
+ "y": 768
+ },
+ "lime_terracotta": {
+ "name": "Lime Terracotta",
+ "x": 544,
+ "y": 736
+ },
+ "pink_terracotta": {
+ "name": "Pink Terracotta",
+ "x": 96,
+ "y": 768
+ },
+ "gray_terracotta": {
+ "name": "Gray Terracotta",
+ "x": 800,
+ "y": 704
+ },
+ "light_gray_terracotta": {
+ "name": "Light Gray Terracotta",
+ "x": 352,
+ "y": 736
+ },
+ "cyan_terracotta": {
+ "name": "Cyan Terracotta",
+ "x": 608,
+ "y": 704
+ },
+ "purple_terracotta": {
+ "name": "Purple Terracotta",
+ "x": 288,
+ "y": 768
+ },
+ "blue_terracotta": {
+ "name": "Blue Terracotta",
+ "x": 224,
+ "y": 704
+ },
+ "brown_terracotta": {
+ "name": "Brown Terracotta",
+ "x": 416,
+ "y": 704
+ },
+ "green_terracotta": {
+ "name": "Green Terracotta",
+ "x": 992,
+ "y": 704
+ },
+ "red_terracotta": {
+ "name": "Red Terracotta",
+ "x": 480,
+ "y": 768
+ },
+ "black_terracotta": {
+ "name": "Black Terracotta",
+ "x": 32,
+ "y": 704
+ },
+ "barrier": {
+ "name": "Barrier",
+ "x": 320,
+ "y": 3584
+ },
+ "iron_trapdoor": {
+ "name": "Iron Trapdoor",
+ "x": 416,
+ "y": 928
+ },
+ "hay_block": {
+ "name": "Hay Bale",
+ "x": 992,
+ "y": 3232
+ },
+ "white_carpet": {
+ "name": "White Carpet",
+ "x": 64,
+ "y": 1632
+ },
+ "orange_carpet": {
+ "name": "Orange Carpet",
+ "x": 960,
+ "y": 1600
+ },
+ "magenta_carpet": {
+ "name": "Magenta Carpet",
+ "x": 928,
+ "y": 1600
+ },
+ "light_blue_carpet": {
+ "name": "Light Blue Carpet",
+ "x": 832,
+ "y": 1600
+ },
+ "yellow_carpet": {
+ "name": "Yellow Carpet",
+ "x": 96,
+ "y": 1632
+ },
+ "lime_carpet": {
+ "name": "Lime Carpet",
+ "x": 896,
+ "y": 1600
+ },
+ "pink_carpet": {
+ "name": "Pink Carpet",
+ "x": 992,
+ "y": 1600
+ },
+ "gray_carpet": {
+ "name": "Gray Carpet",
+ "x": 768,
+ "y": 1600
+ },
+ "light_gray_carpet": {
+ "name": "Light Gray Carpet",
+ "x": 864,
+ "y": 1600
+ },
+ "cyan_carpet": {
+ "name": "Cyan Carpet",
+ "x": 736,
+ "y": 1600
+ },
+ "purple_carpet": {
+ "name": "Purple Carpet",
+ "x": 0,
+ "y": 1632
+ },
+ "blue_carpet": {
+ "name": "Blue Carpet",
+ "x": 672,
+ "y": 1600
+ },
+ "brown_carpet": {
+ "name": "Brown Carpet",
+ "x": 704,
+ "y": 1600
+ },
+ "green_carpet": {
+ "name": "Green Carpet",
+ "x": 800,
+ "y": 1600
+ },
+ "red_carpet": {
+ "name": "Red Carpet",
+ "x": 32,
+ "y": 1632
+ },
+ "black_carpet": {
+ "name": "Black Carpet",
+ "x": 640,
+ "y": 1600
+ },
+ "terracotta": {
+ "name": "Terracotta",
+ "x": 640,
+ "y": 3360
+ },
+ "coal_block": {
+ "name": "Block of Coal",
+ "x": 32,
+ "y": 3232
+ },
+ "packed_ice": {
+ "name": "Packed Ice",
+ "x": 320,
+ "y": 3360
+ },
+ "acacia_stairs": {
+ "name": "Acacia Stairs",
+ "x": 736,
+ "y": 3200
+ },
+ "dark_oak_stairs": {
+ "name": "Dark Oak Stairs",
+ "x": 672,
+ "y": 3232
+ },
+ "slime_block": {
+ "name": "Slime Block",
+ "x": 736,
+ "y": 896
+ },
+ "grass_path": {
+ "name": "Grass Path",
+ "x": 96,
+ "y": 1344
+ },
+ "sunflower": {
+ "name": "Sunflower",
+ "x": 672,
+ "y": 3424
+ },
+ "lilac": {
+ "name": "Lilac",
+ "x": 0,
+ "y": 3424
+ },
+ "rose_bush": {
+ "name": "Rose Bush",
+ "x": 512,
+ "y": 3424
+ },
+ "peony": {
+ "name": "Peony",
+ "x": 288,
+ "y": 3424
+ },
+ "tall_grass": {
+ "name": "Tall Grass",
+ "x": 704,
+ "y": 3424
+ },
+ "large_fern": {
+ "name": "Large Fern",
+ "x": 992,
+ "y": 3392
+ },
+ "white_stained_glass": {
+ "name": "White Stained Glass",
+ "x": 704,
+ "y": 768
+ },
+ "orange_stained_glass": {
+ "name": "Orange Stained Glass",
+ "x": 960,
+ "y": 736
+ },
+ "magenta_stained_glass": {
+ "name": "Magenta Stained Glass",
+ "x": 768,
+ "y": 736
+ },
+ "light_blue_stained_glass": {
+ "name": "Light Blue Stained Glass",
+ "x": 192,
+ "y": 736
+ },
+ "yellow_stained_glass": {
+ "name": "Yellow Stained Glass",
+ "x": 896,
+ "y": 768
+ },
+ "lime_stained_glass": {
+ "name": "Lime Stained Glass",
+ "x": 576,
+ "y": 736
+ },
+ "pink_stained_glass": {
+ "name": "Pink Stained Glass",
+ "x": 128,
+ "y": 768
+ },
+ "gray_stained_glass": {
+ "name": "Gray Stained Glass",
+ "x": 832,
+ "y": 704
+ },
+ "light_gray_stained_glass": {
+ "name": "Light Gray Stained Glass",
+ "x": 384,
+ "y": 736
+ },
+ "cyan_stained_glass": {
+ "name": "Cyan Stained Glass",
+ "x": 640,
+ "y": 704
+ },
+ "purple_stained_glass": {
+ "name": "Purple Stained Glass",
+ "x": 320,
+ "y": 768
+ },
+ "blue_stained_glass": {
+ "name": "Blue Stained Glass",
+ "x": 256,
+ "y": 704
+ },
+ "brown_stained_glass": {
+ "name": "Brown Stained Glass",
+ "x": 448,
+ "y": 704
+ },
+ "green_stained_glass": {
+ "name": "Green Stained Glass",
+ "x": 0,
+ "y": 736
+ },
+ "red_stained_glass": {
+ "name": "Red Stained Glass",
+ "x": 512,
+ "y": 768
+ },
+ "black_stained_glass": {
+ "name": "Black Stained Glass",
+ "x": 64,
+ "y": 704
+ },
+ "white_stained_glass_pane": {
+ "name": "White Stained Glass Pane",
+ "x": 736,
+ "y": 768
+ },
+ "orange_stained_glass_pane": {
+ "name": "Orange Stained Glass Pane",
+ "x": 992,
+ "y": 736
+ },
+ "magenta_stained_glass_pane": {
+ "name": "Magenta Stained Glass Pane",
+ "x": 800,
+ "y": 736
+ },
+ "light_blue_stained_glass_pane": {
+ "name": "Light Blue Stained Glass Pane",
+ "x": 224,
+ "y": 736
+ },
+ "yellow_stained_glass_pane": {
+ "name": "Yellow Stained Glass Pane",
+ "x": 928,
+ "y": 768
+ },
+ "lime_stained_glass_pane": {
+ "name": "Lime Stained Glass Pane",
+ "x": 608,
+ "y": 736
+ },
+ "pink_stained_glass_pane": {
+ "name": "Pink Stained Glass Pane",
+ "x": 160,
+ "y": 768
+ },
+ "gray_stained_glass_pane": {
+ "name": "Gray Stained Glass Pane",
+ "x": 864,
+ "y": 704
+ },
+ "light_gray_stained_glass_pane": {
+ "name": "Light Gray Stained Glass Pane",
+ "x": 416,
+ "y": 736
+ },
+ "cyan_stained_glass_pane": {
+ "name": "Cyan Stained Glass Pane",
+ "x": 672,
+ "y": 704
+ },
+ "purple_stained_glass_pane": {
+ "name": "Purple Stained Glass Pane",
+ "x": 352,
+ "y": 768
+ },
+ "blue_stained_glass_pane": {
+ "name": "Blue Stained Glass Pane",
+ "x": 288,
+ "y": 704
+ },
+ "brown_stained_glass_pane": {
+ "name": "Brown Stained Glass Pane",
+ "x": 480,
+ "y": 704
+ },
+ "green_stained_glass_pane": {
+ "name": "Green Stained Glass Pane",
+ "x": 32,
+ "y": 736
+ },
+ "red_stained_glass_pane": {
+ "name": "Red Stained Glass Pane",
+ "x": 544,
+ "y": 768
+ },
+ "black_stained_glass_pane": {
+ "name": "Black Stained Glass Pane",
+ "x": 96,
+ "y": 704
+ },
+ "prismarine": {
+ "name": "Prismarine Wall",
+ "x": 0,
+ "y": 96
+ },
+ "prismarine_bricks": {
+ "name": "Prismarine Bricks",
+ "x": 384,
+ "y": 3360
+ },
+ "dark_prismarine": {
+ "name": "Dark Prismarine",
+ "x": 32,
+ "y": 3360
+ },
+ "prismarine_stairs": {
+ "name": "Dark Prismarine Stairs",
+ "x": 768,
+ "y": 3232
+ },
+ "prismarine_brick_stairs": {
+ "name": "Prismarine Brick Stairs",
+ "x": 448,
+ "y": 96
+ },
+ "dark_prismarine_stairs": {
+ "name": "Dark Prismarine Stairs",
+ "x": 768,
+ "y": 3232
+ },
+ "sea_lantern": {
+ "name": "Sea Lantern BE",
+ "x": 512,
+ "y": 192
+ },
+ "red_sandstone": {
+ "name": "Red Sandstone",
+ "x": 448,
+ "y": 3360
+ },
+ "chiseled_red_sandstone": {
+ "name": "Chiseled Red Sandstone",
+ "x": 352,
+ "y": 3232
+ },
+ "cut_red_sandstone": {
+ "name": "Cut Red Sandstone",
+ "x": 512,
+ "y": 3232
+ },
+ "red_sandstone_stairs": {
+ "name": "Red Sandstone Stairs",
+ "x": 128,
+ "y": 3296
+ },
+ "magma_block": {
+ "name": "Magma Block BE",
+ "x": 928,
+ "y": 480
+ },
+ "nether_wart_block": {
+ "name": "Nether Wart Block",
+ "x": 544,
+ "y": 3264
+ },
+ "warped_wart_block": {
+ "name": "Warped Wart Block",
+ "x": 832,
+ "y": 128
+ },
+ "red_nether_bricks": {
+ "name": "Red Nether Bricks",
+ "x": 704,
+ "y": 1344
+ },
+ "bone_block": {
+ "name": "Bone Block",
+ "x": 864,
+ "y": 3328
+ },
+ "structure_void": {
+ "name": "Structure Void",
+ "x": 928,
+ "y": 3584
+ },
+ "observer": {
+ "name": "Observer",
+ "x": 864,
+ "y": 3584
+ },
+ "shulker_box": {
+ "name": "Shulker Box",
+ "x": 448,
+ "y": 1600
+ },
+ "white_shulker_box": {
+ "name": "White Shulker Box",
+ "x": 512,
+ "y": 1600
+ },
+ "orange_shulker_box": {
+ "name": "Orange Shulker Box",
+ "x": 384,
+ "y": 1600
+ },
+ "magenta_shulker_box": {
+ "name": "Magenta Shulker Box",
+ "x": 352,
+ "y": 1600
+ },
+ "light_blue_shulker_box": {
+ "name": "Light Blue Shulker Box",
+ "x": 256,
+ "y": 1600
+ },
+ "yellow_shulker_box": {
+ "name": "Yellow Shulker Box",
+ "x": 544,
+ "y": 1600
+ },
+ "lime_shulker_box": {
+ "name": "Lime Shulker Box",
+ "x": 320,
+ "y": 1600
+ },
+ "pink_shulker_box": {
+ "name": "Pink Shulker Box",
+ "x": 416,
+ "y": 1600
+ },
+ "gray_shulker_box": {
+ "name": "Gray Shulker Box",
+ "x": 192,
+ "y": 1600
+ },
+ "light_gray_shulker_box": {
+ "name": "Light Gray Shulker Box",
+ "x": 288,
+ "y": 1600
+ },
+ "cyan_shulker_box": {
+ "name": "Cyan Shulker Box",
+ "x": 160,
+ "y": 1600
+ },
+ "purple_shulker_box": {
+ "name": "Purple Shulker Box",
+ "x": 608,
+ "y": 1408
+ },
+ "blue_shulker_box": {
+ "name": "Blue Shulker Box",
+ "x": 96,
+ "y": 1600
+ },
+ "brown_shulker_box": {
+ "name": "Brown Shulker Box",
+ "x": 128,
+ "y": 1600
+ },
+ "green_shulker_box": {
+ "name": "Green Shulker Box",
+ "x": 224,
+ "y": 1600
+ },
+ "red_shulker_box": {
+ "name": "Red Shulker Box",
+ "x": 480,
+ "y": 1600
+ },
+ "black_shulker_box": {
+ "name": "Black Shulker Box",
+ "x": 64,
+ "y": 1600
+ },
+ "white_glazed_terracotta": {
+ "name": "White Glazed Terracotta",
+ "x": 192,
+ "y": 1536
+ },
+ "orange_glazed_terracotta": {
+ "name": "Orange Glazed Terracotta",
+ "x": 160,
+ "y": 1536
+ },
+ "magenta_glazed_terracotta": {
+ "name": "Magenta Glazed Terracotta",
+ "x": 128,
+ "y": 1536
+ },
+ "light_blue_glazed_terracotta": {
+ "name": "Light Blue Glazed Terracotta",
+ "x": 704,
+ "y": 1472
+ },
+ "yellow_glazed_terracotta": {
+ "name": "Yellow Glazed Terracotta",
+ "x": 544,
+ "y": 1504
+ },
+ "lime_glazed_terracotta": {
+ "name": "Lime Glazed Terracotta",
+ "x": 896,
+ "y": 1472
+ },
+ "pink_glazed_terracotta": {
+ "name": "Pink Glazed Terracotta",
+ "x": 160,
+ "y": 1504
+ },
+ "gray_glazed_terracotta": {
+ "name": "Gray Glazed Terracotta",
+ "x": 512,
+ "y": 1472
+ },
+ "light_gray_glazed_terracotta": {
+ "name": "Light Gray Glazed Terracotta",
+ "x": 800,
+ "y": 1472
+ },
+ "cyan_glazed_terracotta": {
+ "name": "Cyan Glazed Terracotta",
+ "x": 96,
+ "y": 1536
+ },
+ "purple_glazed_terracotta": {
+ "name": "Purple Glazed Terracotta",
+ "x": 256,
+ "y": 1504
+ },
+ "blue_glazed_terracotta": {
+ "name": "Blue Glazed Terracotta",
+ "x": 224,
+ "y": 1472
+ },
+ "brown_glazed_terracotta": {
+ "name": "Brown Glazed Terracotta",
+ "x": 320,
+ "y": 1472
+ },
+ "green_glazed_terracotta": {
+ "name": "Green Glazed Terracotta",
+ "x": 608,
+ "y": 1472
+ },
+ "red_glazed_terracotta": {
+ "name": "Red Glazed Terracotta",
+ "x": 352,
+ "y": 1504
+ },
+ "black_glazed_terracotta": {
+ "name": "Black Glazed Terracotta",
+ "x": 128,
+ "y": 1472
+ },
+ "white_concrete": {
+ "name": "White Concrete",
+ "x": 384,
+ "y": 1504
+ },
+ "orange_concrete": {
+ "name": "Orange Concrete",
+ "x": 0,
+ "y": 1504
+ },
+ "magenta_concrete": {
+ "name": "Magenta Concrete",
+ "x": 928,
+ "y": 1472
+ },
+ "light_blue_concrete": {
+ "name": "Light Blue Concrete",
+ "x": 640,
+ "y": 1472
+ },
+ "yellow_concrete": {
+ "name": "Yellow Concrete",
+ "x": 480,
+ "y": 1504
+ },
+ "lime_concrete": {
+ "name": "Lime Concrete",
+ "x": 832,
+ "y": 1472
+ },
+ "pink_concrete": {
+ "name": "Pink Concrete",
+ "x": 96,
+ "y": 1504
+ },
+ "gray_concrete": {
+ "name": "Gray Concrete",
+ "x": 448,
+ "y": 1472
+ },
+ "light_gray_concrete": {
+ "name": "Light Gray Concrete",
+ "x": 736,
+ "y": 1472
+ },
+ "cyan_concrete": {
+ "name": "Cyan Concrete",
+ "x": 352,
+ "y": 1472
+ },
+ "purple_concrete": {
+ "name": "Purple Concrete",
+ "x": 192,
+ "y": 1504
+ },
+ "blue_concrete": {
+ "name": "Blue Concrete",
+ "x": 160,
+ "y": 1472
+ },
+ "brown_concrete": {
+ "name": "Brown Concrete",
+ "x": 256,
+ "y": 1472
+ },
+ "green_concrete": {
+ "name": "Green Concrete",
+ "x": 544,
+ "y": 1472
+ },
+ "red_concrete": {
+ "name": "Red Concrete",
+ "x": 288,
+ "y": 1504
+ },
+ "black_concrete": {
+ "name": "Black Concrete",
+ "x": 64,
+ "y": 1472
+ },
+ "white_concrete_powder": {
+ "name": "White Concrete Powder",
+ "x": 416,
+ "y": 1504
+ },
+ "orange_concrete_powder": {
+ "name": "Orange Concrete Powder",
+ "x": 32,
+ "y": 1504
+ },
+ "magenta_concrete_powder": {
+ "name": "Magenta Concrete Powder",
+ "x": 960,
+ "y": 1472
+ },
+ "light_blue_concrete_powder": {
+ "name": "Light Blue Concrete Powder",
+ "x": 672,
+ "y": 1472
+ },
+ "yellow_concrete_powder": {
+ "name": "Yellow Concrete Powder",
+ "x": 512,
+ "y": 1504
+ },
+ "lime_concrete_powder": {
+ "name": "Lime Concrete Powder",
+ "x": 864,
+ "y": 1472
+ },
+ "pink_concrete_powder": {
+ "name": "Pink Concrete Powder",
+ "x": 128,
+ "y": 1504
+ },
+ "gray_concrete_powder": {
+ "name": "Gray Concrete Powder",
+ "x": 480,
+ "y": 1472
+ },
+ "light_gray_concrete_powder": {
+ "name": "Light Gray Concrete Powder",
+ "x": 768,
+ "y": 1472
+ },
+ "cyan_concrete_powder": {
+ "name": "Cyan Concrete Powder",
+ "x": 384,
+ "y": 1472
+ },
+ "purple_concrete_powder": {
+ "name": "Purple Concrete Powder",
+ "x": 224,
+ "y": 1504
+ },
+ "blue_concrete_powder": {
+ "name": "Blue Concrete Powder",
+ "x": 192,
+ "y": 1472
+ },
+ "brown_concrete_powder": {
+ "name": "Brown Concrete Powder",
+ "x": 288,
+ "y": 1472
+ },
+ "green_concrete_powder": {
+ "name": "Green Concrete Powder",
+ "x": 576,
+ "y": 1472
+ },
+ "red_concrete_powder": {
+ "name": "Red Concrete Powder",
+ "x": 320,
+ "y": 1504
+ },
+ "black_concrete_powder": {
+ "name": "Black Concrete Powder",
+ "x": 96,
+ "y": 1472
+ },
+ "turtle_egg": {
+ "name": "Turtle Egg",
+ "x": 608,
+ "y": 1952
+ },
+ "dead_tube_coral_block": {
+ "name": "Dead Tube Coral Block",
+ "x": 640,
+ "y": 1696
+ },
+ "dead_brain_coral_block": {
+ "name": "Dead Brain Coral Block",
+ "x": 672,
+ "y": 1696
+ },
+ "dead_bubble_coral_block": {
+ "name": "Dead Bubble Coral Block",
+ "x": 704,
+ "y": 1696
+ },
+ "dead_fire_coral_block": {
+ "name": "Dead Fire Coral Block",
+ "x": 736,
+ "y": 1696
+ },
+ "dead_horn_coral_block": {
+ "name": "Dead Horn Coral Block",
+ "x": 768,
+ "y": 1696
+ },
+ "tube_coral_block": {
+ "name": "Tube Coral Block",
+ "x": 288,
+ "y": 1696
+ },
+ "brain_coral_block": {
+ "name": "Brain Coral Block",
+ "x": 320,
+ "y": 1696
+ },
+ "bubble_coral_block": {
+ "name": "Bubble Coral Block",
+ "x": 352,
+ "y": 1696
+ },
+ "fire_coral_block": {
+ "name": "Fire Coral Block",
+ "x": 384,
+ "y": 1696
+ },
+ "horn_coral_block": {
+ "name": "Horn Coral Block",
+ "x": 416,
+ "y": 1696
+ },
+ "tube_coral": {
+ "name": "Tube Coral",
+ "x": 448,
+ "y": 1696
+ },
+ "brain_coral": {
+ "name": "Brain Coral",
+ "x": 480,
+ "y": 1696
+ },
+ "bubble_coral": {
+ "name": "Bubble Coral",
+ "x": 512,
+ "y": 1696
+ },
+ "fire_coral": {
+ "name": "Fire Coral",
+ "x": 544,
+ "y": 1696
+ },
+ "horn_coral": {
+ "name": "Horn Coral",
+ "x": 576,
+ "y": 1696
+ },
+ "dead_brain_coral": {
+ "name": "Dead Brain Coral",
+ "x": 992,
+ "y": 2336
+ },
+ "dead_bubble_coral": {
+ "name": "Dead Bubble Coral",
+ "x": 0,
+ "y": 2368
+ },
+ "dead_fire_coral": {
+ "name": "Dead Fire Coral",
+ "x": 32,
+ "y": 2368
+ },
+ "dead_horn_coral": {
+ "name": "Dead Horn Coral",
+ "x": 64,
+ "y": 2368
+ },
+ "dead_tube_coral": {
+ "name": "Dead Tube Coral",
+ "x": 960,
+ "y": 2336
+ },
+ "tube_coral_fan": {
+ "name": "Tube Coral Fan",
+ "x": 704,
+ "y": 2112
+ },
+ "brain_coral_fan": {
+ "name": "Brain Coral Fan",
+ "x": 608,
+ "y": 2112
+ },
+ "bubble_coral_fan": {
+ "name": "Bubble Coral Fan",
+ "x": 640,
+ "y": 2112
+ },
+ "fire_coral_fan": {
+ "name": "Fire Coral Fan",
+ "x": 896,
+ "y": 1696
+ },
+ "horn_coral_fan": {
+ "name": "Horn Coral Fan",
+ "x": 672,
+ "y": 2112
+ },
+ "dead_tube_coral_fan": {
+ "name": "Dead Tube Coral Fan",
+ "x": 160,
+ "y": 2144
+ },
+ "dead_brain_coral_fan": {
+ "name": "Dead Brain Coral Fan",
+ "x": 32,
+ "y": 2144
+ },
+ "dead_bubble_coral_fan": {
+ "name": "Dead Bubble Coral Fan",
+ "x": 64,
+ "y": 2144
+ },
+ "dead_fire_coral_fan": {
+ "name": "Dead Fire Coral Fan",
+ "x": 96,
+ "y": 2144
+ },
+ "dead_horn_coral_fan": {
+ "name": "Dead Horn Coral Fan",
+ "x": 128,
+ "y": 2144
+ },
+ "blue_ice": {
+ "name": "Blue Ice",
+ "x": 192,
+ "y": 1728
+ },
+ "conduit": {
+ "name": "Conduit",
+ "x": 512,
+ "y": 2016
+ },
+ "polished_granite_stairs": {
+ "name": "Polished Granite Stairs",
+ "x": 960,
+ "y": 2400
+ },
+ "smooth_red_sandstone_stairs": {
+ "name": "Smooth Red Sandstone Stairs",
+ "x": 384,
+ "y": 3296
+ },
+ "mossy_stone_brick_stairs": {
+ "name": "Mossy Stone Brick Stairs",
+ "x": 416,
+ "y": 3264
+ },
+ "polished_diorite_stairs": {
+ "name": "Polished Diorite Stairs",
+ "x": 832,
+ "y": 3264
+ },
+ "mossy_cobblestone_stairs": {
+ "name": "Mossy Cobblestone Stairs",
+ "x": 320,
+ "y": 3264
+ },
+ "end_stone_brick_stairs": {
+ "name": "End Stone Brick Stairs",
+ "x": 576,
+ "y": 2400
+ },
+ "stone_stairs": {
+ "name": "Stone Stairs",
+ "x": 896,
+ "y": 3296
+ },
+ "smooth_sandstone_stairs": {
+ "name": "Smooth Sandstone Stairs",
+ "x": 480,
+ "y": 3296
+ },
+ "smooth_quartz_stairs": {
+ "name": "Smooth Quartz Stairs",
+ "x": 320,
+ "y": 3296
+ },
+ "granite_stairs": {
+ "name": "Granite Stairs",
+ "x": 640,
+ "y": 2400
+ },
+ "andesite_stairs": {
+ "name": "Andesite Stairs",
+ "x": 832,
+ "y": 3200
+ },
+ "red_nether_brick_stairs": {
+ "name": "Red Nether Brick Stairs",
+ "x": 32,
+ "y": 3296
+ },
+ "polished_andesite_stairs": {
+ "name": "Polished Andesite Stairs",
+ "x": 768,
+ "y": 3264
+ },
+ "diorite_stairs": {
+ "name": "Diorite Stairs",
+ "x": 832,
+ "y": 3232
+ },
+ "polished_granite_slab": {
+ "name": "Polished Granite Slab",
+ "x": 928,
+ "y": 2400
+ },
+ "smooth_red_sandstone_slab": {
+ "name": "Smooth Red Sandstone Slab",
+ "x": 352,
+ "y": 3296
+ },
+ "mossy_stone_brick_slab": {
+ "name": "Mossy Stone Brick Slab",
+ "x": 384,
+ "y": 3264
+ },
+ "polished_diorite_slab": {
+ "name": "Polished Diorite Slab",
+ "x": 800,
+ "y": 3264
+ },
+ "mossy_cobblestone_slab": {
+ "name": "Mossy Cobblestone Slab",
+ "x": 288,
+ "y": 3264
+ },
+ "end_stone_brick_slab": {
+ "name": "End Stone Brick Slab",
+ "x": 544,
+ "y": 2400
+ },
+ "smooth_sandstone_slab": {
+ "name": "Smooth Sandstone Slab",
+ "x": 448,
+ "y": 3296
+ },
+ "smooth_quartz_slab": {
+ "name": "Smooth Quartz Slab",
+ "x": 288,
+ "y": 3296
+ },
+ "granite_slab": {
+ "name": "Granite Slab",
+ "x": 608,
+ "y": 2400
+ },
+ "andesite_slab": {
+ "name": "Andesite Slab",
+ "x": 800,
+ "y": 3200
+ },
+ "red_nether_brick_slab": {
+ "name": "Red Nether Brick Slab",
+ "x": 0,
+ "y": 3296
+ },
+ "polished_andesite_slab": {
+ "name": "Polished Andesite Slab",
+ "x": 736,
+ "y": 3264
+ },
+ "diorite_slab": {
+ "name": "Diorite Slab",
+ "x": 800,
+ "y": 3232
+ },
+ "scaffolding": {
+ "name": "Scaffolding",
+ "x": 416,
+ "y": 2400
+ },
+ "iron_door": {
+ "name": "Iron Door",
+ "x": 384,
+ "y": 928
+ },
+ "oak_door": {
+ "name": "Oak Door",
+ "x": 512,
+ "y": 928
+ },
+ "spruce_door": {
+ "name": "Spruce Door",
+ "x": 800,
+ "y": 928
+ },
+ "birch_door": {
+ "name": "Birch Door",
+ "x": 256,
+ "y": 928
+ },
+ "jungle_door": {
+ "name": "Jungle Door",
+ "x": 448,
+ "y": 928
+ },
+ "acacia_door": {
+ "name": "Acacia Door",
+ "x": 192,
+ "y": 928
+ },
+ "dark_oak_door": {
+ "name": "Dark Oak Door",
+ "x": 320,
+ "y": 928
+ },
+ "crimson_door": {
+ "name": "Crimson Door",
+ "x": 224,
+ "y": 160
+ },
+ "warped_door": {
+ "name": "Warped Door",
+ "x": 352,
+ "y": 160
+ },
+ "repeater": {
+ "name": "Redstone Repeater",
+ "x": 576,
+ "y": 3328
+ },
+ "comparator": {
+ "name": "Redstone Comparator",
+ "x": 512,
+ "y": 3328
+ },
+ "structure_block": {
+ "name": "Structure Block",
+ "x": 96,
+ "y": 1376
+ },
+ "jigsaw": {
+ "name": "Jigsaw Block",
+ "x": 64,
+ "y": 288
+ },
+ "turtle_helmet": {
+ "name": "Turtle Shell",
+ "x": 512,
+ "y": 1664
+ },
+ "scute": {
+ "name": "Scute",
+ "x": 608,
+ "y": 1664
+ },
+ "flint_and_steel": {
+ "name": "Flint and Steel",
+ "x": 480,
+ "y": 3552
+ },
+ "apple": {
+ "name": "Apple",
+ "x": 768,
+ "y": 3168
+ },
+ "bow": {
+ "name": "Bow",
+ "x": 992,
+ "y": 1248
+ },
+ "arrow": {
+ "name": "Arrow",
+ "x": 576,
+ "y": 1248
+ },
+ "coal": {
+ "name": "Coal",
+ "x": 576,
+ "y": 3488
+ },
+ "charcoal": {
+ "name": "Charcoal",
+ "x": 512,
+ "y": 3488
+ },
+ "diamond": {
+ "name": "Diamond",
+ "x": 608,
+ "y": 3488
+ },
+ "iron_ingot": {
+ "name": "Iron Ingot",
+ "x": 896,
+ "y": 3488
+ },
+ "gold_ingot": {
+ "name": "Gold Ingot",
+ "x": 800,
+ "y": 3488
+ },
+ "netherite_ingot": {
+ "name": "Netherite Ingot",
+ "x": 192,
+ "y": 128
+ },
+ "netherite_scrap": {
+ "name": "Netherite Scrap",
+ "x": 224,
+ "y": 128
+ },
+ "wooden_sword": {
+ "name": "Wooden Sword",
+ "x": 800,
+ "y": 3616
+ },
+ "wooden_shovel": {
+ "name": "Wooden Shovel",
+ "x": 192,
+ "y": 3584
+ },
+ "wooden_pickaxe": {
+ "name": "Wooden Pickaxe",
+ "x": 160,
+ "y": 3584
+ },
+ "wooden_axe": {
+ "name": "Wooden Axe",
+ "x": 96,
+ "y": 3584
+ },
+ "wooden_hoe": {
+ "name": "Wooden Hoe",
+ "x": 128,
+ "y": 3584
+ },
+ "stone_sword": {
+ "name": "Stone Sword",
+ "x": 736,
+ "y": 3616
+ },
+ "stone_shovel": {
+ "name": "Stone Shovel",
+ "x": 64,
+ "y": 3584
+ },
+ "stone_pickaxe": {
+ "name": "Stone Pickaxe",
+ "x": 32,
+ "y": 3584
+ },
+ "stone_axe": {
+ "name": "Stone Axe",
+ "x": 992,
+ "y": 3552
+ },
+ "stone_hoe": {
+ "name": "Stone Hoe",
+ "x": 0,
+ "y": 3584
+ },
+ "golden_sword": {
+ "name": "Golden Sword",
+ "x": 608,
+ "y": 3616
+ },
+ "golden_shovel": {
+ "name": "Golden Shovel",
+ "x": 640,
+ "y": 3552
+ },
+ "golden_pickaxe": {
+ "name": "Golden Pickaxe",
+ "x": 608,
+ "y": 3552
+ },
+ "golden_axe": {
+ "name": "Golden Axe",
+ "x": 544,
+ "y": 3552
+ },
+ "golden_hoe": {
+ "name": "Golden Hoe",
+ "x": 576,
+ "y": 3552
+ },
+ "iron_sword": {
+ "name": "Iron Sword",
+ "x": 640,
+ "y": 3616
+ },
+ "iron_shovel": {
+ "name": "Iron Shovel",
+ "x": 768,
+ "y": 3552
+ },
+ "iron_pickaxe": {
+ "name": "Iron Pickaxe",
+ "x": 736,
+ "y": 3552
+ },
+ "iron_axe": {
+ "name": "Iron Axe",
+ "x": 672,
+ "y": 3552
+ },
+ "iron_hoe": {
+ "name": "Iron Hoe",
+ "x": 704,
+ "y": 3552
+ },
+ "diamond_sword": {
+ "name": "Diamond Sword",
+ "x": 576,
+ "y": 3616
+ },
+ "diamond_shovel": {
+ "name": "Diamond Shovel",
+ "x": 288,
+ "y": 3552
+ },
+ "diamond_pickaxe": {
+ "name": "Diamond Pickaxe",
+ "x": 256,
+ "y": 3552
+ },
+ "diamond_axe": {
+ "name": "Diamond Axe",
+ "x": 192,
+ "y": 3552
+ },
+ "diamond_hoe": {
+ "name": "Diamond Hoe",
+ "x": 224,
+ "y": 3552
+ },
+ "netherite_sword": {
+ "name": "Netherite Sword",
+ "x": 384,
+ "y": 128
+ },
+ "netherite_shovel": {
+ "name": "Netherite Shovel",
+ "x": 352,
+ "y": 128
+ },
+ "netherite_pickaxe": {
+ "name": "Netherite Pickaxe",
+ "x": 320,
+ "y": 128
+ },
+ "netherite_axe": {
+ "name": "Netherite Axe",
+ "x": 256,
+ "y": 128
+ },
+ "netherite_hoe": {
+ "name": "Netherite Hoe",
+ "x": 288,
+ "y": 128
+ },
+ "stick": {
+ "name": "Stick",
+ "x": 608,
+ "y": 1120
+ },
+ "bowl": {
+ "name": "Bowl",
+ "x": 384,
+ "y": 3520
+ },
+ "mushroom_stew": {
+ "name": "Mushroom Stew",
+ "x": 224,
+ "y": 3200
+ },
+ "string": {
+ "name": "String",
+ "x": 320,
+ "y": 3520
+ },
+ "feather": {
+ "name": "Feather",
+ "x": 704,
+ "y": 3488
+ },
+ "gunpowder": {
+ "name": "Gunpowder",
+ "x": 864,
+ "y": 3488
+ },
+ "wheat_seeds": {
+ "name": "Wheat Seeds",
+ "x": 320,
+ "y": 1056
+ },
+ "wheat": {
+ "name": "Wheat",
+ "x": 608,
+ "y": 3200
+ },
+ "bread": {
+ "name": "Bread",
+ "x": 896,
+ "y": 3168
+ },
+ "leather_helmet": {
+ "name": "Leather Cap",
+ "x": 608,
+ "y": 2496
+ },
+ "leather_chestplate": {
+ "name": "Leather Tunic",
+ "x": 672,
+ "y": 2496
+ },
+ "leather_leggings": {
+ "name": "Leather Pants",
+ "x": 640,
+ "y": 2496
+ },
+ "leather_boots": {
+ "name": "Leather Boots",
+ "x": 576,
+ "y": 2496
+ },
+ "chainmail_helmet": {
+ "name": "Chainmail Helmet",
+ "x": 320,
+ "y": 2464
+ },
+ "chainmail_chestplate": {
+ "name": "Chainmail Chestplate",
+ "x": 288,
+ "y": 2464
+ },
+ "chainmail_leggings": {
+ "name": "Chainmail Leggings",
+ "x": 352,
+ "y": 2464
+ },
+ "chainmail_boots": {
+ "name": "Chainmail Boots",
+ "x": 256,
+ "y": 2464
+ },
+ "iron_helmet": {
+ "name": "Iron Helmet",
+ "x": 512,
+ "y": 2496
+ },
+ "iron_chestplate": {
+ "name": "Iron Chestplate",
+ "x": 480,
+ "y": 2496
+ },
+ "iron_leggings": {
+ "name": "Iron Leggings",
+ "x": 544,
+ "y": 2496
+ },
+ "iron_boots": {
+ "name": "Iron Boots",
+ "x": 448,
+ "y": 2496
+ },
+ "diamond_helmet": {
+ "name": "Diamond Helmet",
+ "x": 160,
+ "y": 2496
+ },
+ "diamond_chestplate": {
+ "name": "Diamond Chestplate",
+ "x": 128,
+ "y": 2496
+ },
+ "diamond_leggings": {
+ "name": "Diamond Leggings",
+ "x": 192,
+ "y": 2496
+ },
+ "diamond_boots": {
+ "name": "Diamond Boots",
+ "x": 96,
+ "y": 2496
+ },
+ "golden_helmet": {
+ "name": "Golden Helmet",
+ "x": 320,
+ "y": 2496
+ },
+ "golden_chestplate": {
+ "name": "Golden Chestplate",
+ "x": 288,
+ "y": 2496
+ },
+ "golden_leggings": {
+ "name": "Golden Leggings",
+ "x": 352,
+ "y": 2496
+ },
+ "golden_boots": {
+ "name": "Golden Boots",
+ "x": 256,
+ "y": 2496
+ },
+ "netherite_helmet": {
+ "name": "Netherite Helmet",
+ "x": 480,
+ "y": 128
+ },
+ "netherite_chestplate": {
+ "name": "Netherite Chestplate",
+ "x": 448,
+ "y": 128
+ },
+ "netherite_leggings": {
+ "name": "Netherite Leggings",
+ "x": 512,
+ "y": 128
+ },
+ "netherite_boots": {
+ "name": "Netherite Boots",
+ "x": 416,
+ "y": 128
+ },
+ "flint": {
+ "name": "Flint",
+ "x": 736,
+ "y": 3488
+ },
+ "porkchop": {
+ "name": "Raw Porkchop",
+ "x": 480,
+ "y": 3200
+ },
+ "cooked_porkchop": {
+ "name": "Cooked Porkchop",
+ "x": 32,
+ "y": 3200
+ },
+ "painting": {
+ "name": "Painting",
+ "x": 544,
+ "y": 3136
+ },
+ "golden_apple": {
+ "name": "Golden Apple",
+ "x": 160,
+ "y": 3200
+ },
+ "enchanted_golden_apple": {
+ "name": "Enchanted Golden Apple",
+ "x": 128,
+ "y": 256
+ },
+ "oak_sign": {
+ "name": "Oak Sign",
+ "x": 896,
+ "y": 3360
+ },
+ "spruce_sign": {
+ "name": "Spruce Sign",
+ "x": 800,
+ "y": 2368
+ },
+ "birch_sign": {
+ "name": "Birch Sign",
+ "x": 768,
+ "y": 2368
+ },
+ "jungle_sign": {
+ "name": "Jungle Sign",
+ "x": 832,
+ "y": 2368
+ },
+ "acacia_sign": {
+ "name": "Acacia Sign",
+ "x": 864,
+ "y": 2368
+ },
+ "dark_oak_sign": {
+ "name": "Dark Oak Sign",
+ "x": 896,
+ "y": 2368
+ },
+ "crimson_sign": {
+ "name": "Crimson Sign",
+ "x": 512,
+ "y": 160
+ },
+ "warped_sign": {
+ "name": "Warped Sign",
+ "x": 544,
+ "y": 160
+ },
+ "bucket": {
+ "name": "Bucket",
+ "x": 288,
+ "y": 3136
+ },
+ "water_bucket": {
+ "name": "Water Bucket",
+ "x": 384,
+ "y": 3136
+ },
+ "lava_bucket": {
+ "name": "Lava Bucket",
+ "x": 320,
+ "y": 3136
+ },
+ "minecart": {
+ "name": "Minecart",
+ "x": 160,
+ "y": 3616
+ },
+ "saddle": {
+ "name": "Saddle",
+ "x": 928,
+ "y": 3552
+ },
+ "redstone": {
+ "name": "Redstone Dust",
+ "x": 224,
+ "y": 3520
+ },
+ "snowball": {
+ "name": "Snowball",
+ "x": 672,
+ "y": 3616
+ },
+ "oak_boat": {
+ "name": "Oak Boat",
+ "x": 352,
+ "y": 3616
+ },
+ "leather": {
+ "name": "Leather",
+ "x": 960,
+ "y": 3488
+ },
+ "milk_bucket": {
+ "name": "Milk Bucket",
+ "x": 352,
+ "y": 3136
+ },
+ "pufferfish_bucket": {
+ "name": "Bucket of Pufferfish",
+ "x": 704,
+ "y": 1664
+ },
+ "salmon_bucket": {
+ "name": "Bucket of Salmon",
+ "x": 736,
+ "y": 1664
+ },
+ "cod_bucket": {
+ "name": "Bucket of Cod",
+ "x": 672,
+ "y": 1664
+ },
+ "tropical_fish_bucket": {
+ "name": "Bucket of Tropical Fish",
+ "x": 352,
+ "y": 2016
+ },
+ "brick": {
+ "name": "Brick",
+ "x": 480,
+ "y": 3488
+ },
+ "clay_ball": {
+ "name": "Clay Ball",
+ "x": 544,
+ "y": 3488
+ },
+ "dried_kelp_block": {
+ "name": "Dried Kelp Block",
+ "x": 128,
+ "y": 1664
+ },
+ "paper": {
+ "name": "Paper",
+ "x": 64,
+ "y": 3520
+ },
+ "book": {
+ "name": "Book",
+ "x": 448,
+ "y": 3488
+ },
+ "slime_ball": {
+ "name": "Slimeball",
+ "x": 288,
+ "y": 3520
+ },
+ "chest_minecart": {
+ "name": "Minecart with Chest",
+ "x": 192,
+ "y": 3616
+ },
+ "furnace_minecart": {
+ "name": "Minecart with Furnace",
+ "x": 256,
+ "y": 3616
+ },
+ "egg": {
+ "name": "Egg",
+ "x": 128,
+ "y": 3200
+ },
+ "compass": {
+ "name": "Compass",
+ "x": 992,
+ "y": 480
+ },
+ "fishing_rod": {
+ "name": "Fishing Rod",
+ "x": 448,
+ "y": 3552
+ },
+ "clock": {
+ "name": "Clock",
+ "x": 960,
+ "y": 480
+ },
+ "glowstone_dust": {
+ "name": "Glowstone Dust",
+ "x": 768,
+ "y": 3488
+ },
+ "cod": {
+ "name": "Raw Cod",
+ "x": 896,
+ "y": 1664
+ },
+ "salmon": {
+ "name": "Raw Salmon",
+ "x": 928,
+ "y": 1664
+ },
+ "tropical_fish": {
+ "name": "Tropical Fish",
+ "x": 768,
+ "y": 1664
+ },
+ "pufferfish": {
+ "name": "Pufferfish",
+ "x": 864,
+ "y": 1664
+ },
+ "cooked_cod": {
+ "name": "Cooked Cod",
+ "x": 800,
+ "y": 1664
+ },
+ "cooked_salmon": {
+ "name": "Cooked Salmon",
+ "x": 832,
+ "y": 1664
+ },
+ "ink_sac": {
+ "name": "Ink Sac",
+ "x": 736,
+ "y": 3136
+ },
+ "cocoa_beans": {
+ "name": "Cocoa Beans",
+ "x": 608,
+ "y": 3136
+ },
+ "lapis_lazuli": {
+ "name": "Lapis Lazuli",
+ "x": 768,
+ "y": 3136
+ },
+ "white_dye": {
+ "name": "White Dye",
+ "x": 288,
+ "y": 2368
+ },
+ "orange_dye": {
+ "name": "Orange Dye",
+ "x": 928,
+ "y": 3136
+ },
+ "magenta_dye": {
+ "name": "Magenta Dye",
+ "x": 896,
+ "y": 3136
+ },
+ "light_blue_dye": {
+ "name": "Light Blue Dye",
+ "x": 800,
+ "y": 3136
+ },
+ "yellow_dye": {
+ "name": "Yellow Dye",
+ "x": 32,
+ "y": 3168
+ },
+ "lime_dye": {
+ "name": "Lime Dye",
+ "x": 864,
+ "y": 3136
+ },
+ "pink_dye": {
+ "name": "Pink Dye",
+ "x": 960,
+ "y": 3136
+ },
+ "gray_dye": {
+ "name": "Gray Dye",
+ "x": 672,
+ "y": 3136
+ },
+ "light_gray_dye": {
+ "name": "Light Gray Dye",
+ "x": 832,
+ "y": 3136
+ },
+ "cyan_dye": {
+ "name": "Cyan Dye",
+ "x": 640,
+ "y": 3136
+ },
+ "purple_dye": {
+ "name": "Purple Dye",
+ "x": 992,
+ "y": 3136
+ },
+ "blue_dye": {
+ "name": "Blue Dye",
+ "x": 224,
+ "y": 2368
+ },
+ "brown_dye": {
+ "name": "Brown Dye",
+ "x": 256,
+ "y": 2368
+ },
+ "green_dye": {
+ "name": "Green Dye",
+ "x": 704,
+ "y": 3136
+ },
+ "red_dye": {
+ "name": "Red Dye",
+ "x": 0,
+ "y": 3168
+ },
+ "black_dye": {
+ "name": "Black Dye",
+ "x": 192,
+ "y": 2368
+ },
+ "bone_meal": {
+ "name": "Bone Meal",
+ "x": 576,
+ "y": 3136
+ },
+ "bone": {
+ "name": "Bone",
+ "x": 416,
+ "y": 3488
+ },
+ "sugar": {
+ "name": "Sugar",
+ "x": 576,
+ "y": 3200
+ },
+ "cake": {
+ "name": "Cake",
+ "x": 416,
+ "y": 3584
+ },
+ "white_bed": {
+ "name": "White Bed",
+ "x": 512,
+ "y": 3168
+ },
+ "orange_bed": {
+ "name": "Orange Bed",
+ "x": 384,
+ "y": 3168
+ },
+ "magenta_bed": {
+ "name": "Magenta Bed",
+ "x": 352,
+ "y": 3168
+ },
+ "light_blue_bed": {
+ "name": "Light Blue Bed",
+ "x": 256,
+ "y": 3168
+ },
+ "yellow_bed": {
+ "name": "Yellow Bed",
+ "x": 544,
+ "y": 3168
+ },
+ "lime_bed": {
+ "name": "Lime Bed",
+ "x": 320,
+ "y": 3168
+ },
+ "pink_bed": {
+ "name": "Pink Bed",
+ "x": 416,
+ "y": 3168
+ },
+ "gray_bed": {
+ "name": "Gray Bed",
+ "x": 192,
+ "y": 3168
+ },
+ "light_gray_bed": {
+ "name": "Light Gray Bed",
+ "x": 288,
+ "y": 3168
+ },
+ "cyan_bed": {
+ "name": "Cyan Bed",
+ "x": 160,
+ "y": 3168
+ },
+ "purple_bed": {
+ "name": "Purple Bed",
+ "x": 448,
+ "y": 3168
+ },
+ "blue_bed": {
+ "name": "Blue Bed",
+ "x": 96,
+ "y": 3168
+ },
+ "brown_bed": {
+ "name": "Brown Bed",
+ "x": 128,
+ "y": 3168
+ },
+ "green_bed": {
+ "name": "Green Bed",
+ "x": 224,
+ "y": 3168
+ },
+ "red_bed": {
+ "name": "Red Bed",
+ "x": 480,
+ "y": 3168
+ },
+ "black_bed": {
+ "name": "Black Bed",
+ "x": 64,
+ "y": 3168
+ },
+ "cookie": {
+ "name": "Cookie",
+ "x": 96,
+ "y": 3200
+ },
+ "filled_map": {
+ "name": "Map",
+ "x": 832,
+ "y": 3552
+ },
+ "shears": {
+ "name": "Shears",
+ "x": 960,
+ "y": 3552
+ },
+ "melon_slice": {
+ "name": "Melon Slice",
+ "x": 192,
+ "y": 3200
+ },
+ "dried_kelp": {
+ "name": "Dried Kelp",
+ "x": 544,
+ "y": 1664
+ },
+ "pumpkin_seeds": {
+ "name": "Pumpkin Seeds",
+ "x": 128,
+ "y": 1056
+ },
+ "melon_seeds": {
+ "name": "Melon Seeds",
+ "x": 800,
+ "y": 1024
+ },
+ "beef": {
+ "name": "Raw Beef",
+ "x": 384,
+ "y": 3200
+ },
+ "cooked_beef": {
+ "name": "Steak",
+ "x": 480,
+ "y": 832
+ },
+ "chicken": {
+ "name": "Raw Chicken",
+ "x": 416,
+ "y": 3200
+ },
+ "cooked_chicken": {
+ "name": "Cooked Chicken",
+ "x": 992,
+ "y": 3168
+ },
+ "rotten_flesh": {
+ "name": "Rotten Flesh",
+ "x": 544,
+ "y": 3200
+ },
+ "ender_pearl": {
+ "name": "Ender Pearl",
+ "x": 672,
+ "y": 3488
+ },
+ "blaze_rod": {
+ "name": "Blaze Rod",
+ "x": 448,
+ "y": 640
+ },
+ "ghast_tear": {
+ "name": "Ghast Tear",
+ "x": 96,
+ "y": 3136
+ },
+ "gold_nugget": {
+ "name": "Gold Nugget",
+ "x": 832,
+ "y": 3488
+ },
+ "nether_wart": {
+ "name": "Nether Wart",
+ "x": 704,
+ "y": 640
+ },
+ "potion": {
+ "name": "Potion of Luck",
+ "x": 960,
+ "y": 3424
+ },
+ "glass_bottle": {
+ "name": "Glass Bottle",
+ "x": 512,
+ "y": 3552
+ },
+ "spider_eye": {
+ "name": "Spider Eye",
+ "x": 256,
+ "y": 3136
+ },
+ "fermented_spider_eye": {
+ "name": "Fermented Spider Eye",
+ "x": 64,
+ "y": 3136
+ },
+ "blaze_powder": {
+ "name": "Blaze Powder",
+ "x": 416,
+ "y": 640
+ },
+ "magma_cream": {
+ "name": "Magma Cream",
+ "x": 192,
+ "y": 3136
+ },
+ "brewing_stand": {
+ "name": "Brewing Stand",
+ "x": 384,
+ "y": 3584
+ },
+ "cauldron": {
+ "name": "Cauldron",
+ "x": 448,
+ "y": 3584
+ },
+ "ender_eye": {
+ "name": "Eye of Ender",
+ "x": 384,
+ "y": 3552
+ },
+ "glistering_melon_slice": {
+ "name": "Glistering Melon Slice",
+ "x": 128,
+ "y": 3136
+ },
+ "bat_spawn_egg": {
+ "name": "Bat Spawn Egg",
+ "x": 672,
+ "y": 1120
+ },
+ "bee_spawn_egg": {
+ "name": "Bee Spawn Egg",
+ "x": 224,
+ "y": 96
+ },
+ "blaze_spawn_egg": {
+ "name": "Blaze Spawn Egg",
+ "x": 704,
+ "y": 1120
+ },
+ "cat_spawn_egg": {
+ "name": "Cat Spawn Egg",
+ "x": 288,
+ "y": 2400
+ },
+ "cave_spider_spawn_egg": {
+ "name": "Cave Spider Spawn Egg",
+ "x": 736,
+ "y": 1120
+ },
+ "chicken_spawn_egg": {
+ "name": "Chicken Spawn Egg",
+ "x": 768,
+ "y": 1120
+ },
+ "cod_spawn_egg": {
+ "name": "Cod Spawn Egg",
+ "x": 0,
+ "y": 1696
+ },
+ "cow_spawn_egg": {
+ "name": "Cow Spawn Egg",
+ "x": 800,
+ "y": 1120
+ },
+ "creeper_spawn_egg": {
+ "name": "Creeper Spawn Egg",
+ "x": 832,
+ "y": 1120
+ },
+ "dolphin_spawn_egg": {
+ "name": "Dolphin Spawn Egg",
+ "x": 448,
+ "y": 1952
+ },
+ "donkey_spawn_egg": {
+ "name": "Donkey Spawn Egg",
+ "x": 832,
+ "y": 1344
+ },
+ "drowned_spawn_egg": {
+ "name": "Drowned Spawn Egg",
+ "x": 960,
+ "y": 1696
+ },
+ "elder_guardian_spawn_egg": {
+ "name": "Elder Guardian Spawn Egg",
+ "x": 864,
+ "y": 1344
+ },
+ "enderman_spawn_egg": {
+ "name": "Enderman Spawn Egg",
+ "x": 864,
+ "y": 1120
+ },
+ "endermite_spawn_egg": {
+ "name": "Endermite Spawn Egg",
+ "x": 896,
+ "y": 1120
+ },
+ "evoker_spawn_egg": {
+ "name": "Evoker Spawn Egg",
+ "x": 64,
+ "y": 1408
+ },
+ "fox_spawn_egg": {
+ "name": "Fox Spawn Egg",
+ "x": 992,
+ "y": 2432
+ },
+ "ghast_spawn_egg": {
+ "name": "Ghast Spawn Egg",
+ "x": 928,
+ "y": 1120
+ },
+ "guardian_spawn_egg": {
+ "name": "Guardian Spawn Egg",
+ "x": 960,
+ "y": 1120
+ },
+ "hoglin_spawn_egg": {
+ "name": "Hoglin Spawn Egg",
+ "x": 32,
+ "y": 192
+ },
+ "horse_spawn_egg": {
+ "name": "Horse Spawn Egg",
+ "x": 992,
+ "y": 1120
+ },
+ "husk_spawn_egg": {
+ "name": "Husk Spawn Egg",
+ "x": 896,
+ "y": 1344
+ },
+ "llama_spawn_egg": {
+ "name": "Llama Spawn Egg",
+ "x": 96,
+ "y": 1408
+ },
+ "magma_cube_spawn_egg": {
+ "name": "Magma Cube Spawn Egg",
+ "x": 0,
+ "y": 1152
+ },
+ "mooshroom_spawn_egg": {
+ "name": "Mooshroom Spawn Egg",
+ "x": 32,
+ "y": 1152
+ },
+ "mule_spawn_egg": {
+ "name": "Mule Spawn Egg",
+ "x": 928,
+ "y": 1344
+ },
+ "ocelot_spawn_egg": {
+ "name": "Ocelot Spawn Egg",
+ "x": 64,
+ "y": 1152
+ },
+ "panda_spawn_egg": {
+ "name": "Panda Spawn Egg",
+ "x": 352,
+ "y": 2400
+ },
+ "parrot_spawn_egg": {
+ "name": "Parrot Spawn Egg",
+ "x": 224,
+ "y": 1536
+ },
+ "phantom_spawn_egg": {
+ "name": "Phantom Spawn Egg",
+ "x": 672,
+ "y": 1952
+ },
+ "pig_spawn_egg": {
+ "name": "Pig Spawn Egg",
+ "x": 96,
+ "y": 1152
+ },
+ "piglin_spawn_egg": {
+ "name": "Piglin Spawn Egg",
+ "x": 0,
+ "y": 192
+ },
+ "piglin_brute_spawn_egg": {
+ "name": "Piglin Brute Spawn Egg",
+ "x": 160,
+ "y": 256
+ },
+ "pillager_spawn_egg": {
+ "name": "Pillager Spawn Egg",
+ "x": 384,
+ "y": 2400
+ },
+ "polar_bear_spawn_egg": {
+ "name": "Polar Bear Spawn Egg",
+ "x": 960,
+ "y": 1344
+ },
+ "pufferfish_spawn_egg": {
+ "name": "Pufferfish Spawn Egg",
+ "x": 32,
+ "y": 1696
+ },
+ "rabbit_spawn_egg": {
+ "name": "Rabbit Spawn Egg",
+ "x": 128,
+ "y": 1152
+ },
+ "ravager_spawn_egg": {
+ "name": "Ravager Spawn Egg",
+ "x": 320,
+ "y": 2400
+ },
+ "salmon_spawn_egg": {
+ "name": "Salmon Spawn Egg",
+ "x": 64,
+ "y": 1696
+ },
+ "sheep_spawn_egg": {
+ "name": "Sheep Spawn Egg",
+ "x": 160,
+ "y": 1152
+ },
+ "shulker_spawn_egg": {
+ "name": "Shulker Spawn Egg",
+ "x": 192,
+ "y": 1152
+ },
+ "silverfish_spawn_egg": {
+ "name": "Silverfish Spawn Egg",
+ "x": 224,
+ "y": 1152
+ },
+ "skeleton_spawn_egg": {
+ "name": "Skeleton Spawn Egg",
+ "x": 256,
+ "y": 1152
+ },
+ "skeleton_horse_spawn_egg": {
+ "name": "Skeleton Horse Spawn Egg",
+ "x": 992,
+ "y": 1344
+ },
+ "slime_spawn_egg": {
+ "name": "Slime Spawn Egg",
+ "x": 288,
+ "y": 1152
+ },
+ "spider_spawn_egg": {
+ "name": "Spider Spawn Egg",
+ "x": 320,
+ "y": 1152
+ },
+ "squid_spawn_egg": {
+ "name": "Squid Spawn Egg",
+ "x": 352,
+ "y": 1152
+ },
+ "stray_spawn_egg": {
+ "name": "Stray Spawn Egg",
+ "x": 0,
+ "y": 1376
+ },
+ "strider_spawn_egg": {
+ "name": "Strider Spawn Egg",
+ "x": 960,
+ "y": 192
+ },
+ "trader_llama_spawn_egg": {
+ "name": "Trader Llama Spawn Egg",
+ "x": 928,
+ "y": 2432
+ },
+ "tropical_fish_spawn_egg": {
+ "name": "Tropical Fish Spawn Egg",
+ "x": 800,
+ "y": 1696
+ },
+ "turtle_spawn_egg": {
+ "name": "Turtle Spawn Egg",
+ "x": 0,
+ "y": 1728
+ },
+ "vex_spawn_egg": {
+ "name": "Vex Spawn Egg",
+ "x": 128,
+ "y": 1408
+ },
+ "villager_spawn_egg": {
+ "name": "Villager Spawn Egg",
+ "x": 384,
+ "y": 1152
+ },
+ "vindicator_spawn_egg": {
+ "name": "Vindicator Spawn Egg",
+ "x": 160,
+ "y": 1408
+ },
+ "wandering_trader_spawn_egg": {
+ "name": "Wandering Trader Spawn Egg",
+ "x": 960,
+ "y": 2432
+ },
+ "witch_spawn_egg": {
+ "name": "Witch Spawn Egg",
+ "x": 416,
+ "y": 1152
+ },
+ "wither_skeleton_spawn_egg": {
+ "name": "Wither Skeleton Spawn Egg",
+ "x": 32,
+ "y": 1376
+ },
+ "wolf_spawn_egg": {
+ "name": "Wolf Spawn Egg",
+ "x": 448,
+ "y": 1152
+ },
+ "zoglin_spawn_egg": {
+ "name": "Zoglin Spawn Egg",
+ "x": 352,
+ "y": 256
+ },
+ "zombie_spawn_egg": {
+ "name": "Zombie Spawn Egg",
+ "x": 480,
+ "y": 1152
+ },
+ "zombie_horse_spawn_egg": {
+ "name": "Zombie Horse Spawn Egg",
+ "x": 64,
+ "y": 1376
+ },
+ "zombie_villager_spawn_egg": {
+ "name": "Zombie Villager Spawn Egg",
+ "x": 128,
+ "y": 1344
+ },
+ "zombified_piglin_spawn_egg": {
+ "name": "Zombified Piglin Spawn Egg",
+ "x": 512,
+ "y": 1152
+ },
+ "experience_bottle": {
+ "name": "Bottle o' Enchanting",
+ "x": 288,
+ "y": 3456
+ },
+ "fire_charge": {
+ "name": "Fire Charge",
+ "x": 416,
+ "y": 3552
+ },
+ "writable_book": {
+ "name": "Book and Quill",
+ "x": 352,
+ "y": 3520
+ },
+ "written_book": {
+ "name": "Written Book",
+ "x": 256,
+ "y": 3584
+ },
+ "emerald": {
+ "name": "Emerald",
+ "x": 640,
+ "y": 3488
+ },
+ "item_frame": {
+ "name": "Item Frame",
+ "x": 480,
+ "y": 3136
+ },
+ "flower_pot": {
+ "name": "Flower Pot",
+ "x": 448,
+ "y": 3136
+ },
+ "carrot": {
+ "name": "Carrot",
+ "x": 928,
+ "y": 3168
+ },
+ "potato": {
+ "name": "Potato",
+ "x": 288,
+ "y": 3200
+ },
+ "baked_potato": {
+ "name": "Baked Potato",
+ "x": 800,
+ "y": 3168
+ },
+ "poisonous_potato": {
+ "name": "Poisonous Potato",
+ "x": 256,
+ "y": 3200
+ },
+ "map": {
+ "name": "Empty Map",
+ "x": 320,
+ "y": 3552
+ },
+ "golden_carrot": {
+ "name": "Golden Carrot",
+ "x": 160,
+ "y": 3136
+ },
+ "skeleton_skull": {
+ "name": "Skeleton Skull",
+ "x": 192,
+ "y": 960
+ },
+ "wither_skeleton_skull": {
+ "name": "Wither Skeleton Skull",
+ "x": 224,
+ "y": 960
+ },
+ "player_head": {
+ "name": "Player Head",
+ "x": 160,
+ "y": 960
+ },
+ "zombie_head": {
+ "name": "Zombie Head",
+ "x": 256,
+ "y": 960
+ },
+ "creeper_head": {
+ "name": "Creeper Head",
+ "x": 96,
+ "y": 960
+ },
+ "dragon_head": {
+ "name": "Dragon Head",
+ "x": 128,
+ "y": 960
+ },
+ "carrot_on_a_stick": {
+ "name": "Carrot on a Stick",
+ "x": 416,
+ "y": 3520
+ },
+ "warped_fungus_on_a_stick": {
+ "name": "Warped Fungus on a Stick",
+ "x": 992,
+ "y": 192
+ },
+ "nether_star": {
+ "name": "Nether Star",
+ "x": 64,
+ "y": 352
+ },
+ "pumpkin_pie": {
+ "name": "Pumpkin Pie",
+ "x": 320,
+ "y": 3200
+ },
+ "firework_rocket": {
+ "name": "Firework Rocket",
+ "x": 736,
+ "y": 3168
+ },
+ "firework_star": {
+ "name": "Firework Star",
+ "x": 256,
+ "y": 800
+ },
+ "enchanted_book": {
+ "name": "Enchanted Book",
+ "x": 352,
+ "y": 3552
+ },
+ "nether_brick": {
+ "name": "Nether Brick",
+ "x": 992,
+ "y": 3488
+ },
+ "quartz": {
+ "name": "Nether Quartz",
+ "x": 0,
+ "y": 3520
+ },
+ "tnt_minecart": {
+ "name": "Minecart with TNT",
+ "x": 320,
+ "y": 3616
+ },
+ "hopper_minecart": {
+ "name": "Minecart with Hopper",
+ "x": 288,
+ "y": 3616
+ },
+ "prismarine_shard": {
+ "name": "Prismarine Shard",
+ "x": 160,
+ "y": 3520
+ },
+ "prismarine_crystals": {
+ "name": "Prismarine Crystals",
+ "x": 128,
+ "y": 3520
+ },
+ "rabbit": {
+ "name": "Raw Rabbit",
+ "x": 512,
+ "y": 3200
+ },
+ "cooked_rabbit": {
+ "name": "Cooked Rabbit",
+ "x": 64,
+ "y": 3200
+ },
+ "rabbit_stew": {
+ "name": "Rabbit Stew",
+ "x": 352,
+ "y": 3200
+ },
+ "rabbit_foot": {
+ "name": "Rabbit's Foot",
+ "x": 224,
+ "y": 3136
+ },
+ "rabbit_hide": {
+ "name": "Rabbit Hide",
+ "x": 192,
+ "y": 3520
+ },
+ "armor_stand": {
+ "name": "Armor Stand",
+ "x": 416,
+ "y": 3136
+ },
+ "iron_horse_armor": {
+ "name": "Iron Horse Armor",
+ "x": 192,
+ "y": 32
+ },
+ "golden_horse_armor": {
+ "name": "Golden Horse Armor",
+ "x": 32,
+ "y": 32
+ },
+ "diamond_horse_armor": {
+ "name": "Diamond Horse Armor",
+ "x": 896,
+ "y": 0
+ },
+ "leather_horse_armor": {
+ "name": "Leather Horse Armor",
+ "x": 160,
+ "y": 1376
+ },
+ "lead": {
+ "name": "Lead",
+ "x": 800,
+ "y": 3552
+ },
+ "name_tag": {
+ "name": "Name Tag",
+ "x": 864,
+ "y": 3552
+ },
+ "command_block_minecart": {
+ "name": "Minecart with Command Block",
+ "x": 224,
+ "y": 3616
+ },
+ "mutton": {
+ "name": "Raw Mutton",
+ "x": 448,
+ "y": 3200
+ },
+ "cooked_mutton": {
+ "name": "Cooked Mutton",
+ "x": 0,
+ "y": 3200
+ },
+ "white_banner": {
+ "name": "White Banner",
+ "x": 608,
+ "y": 768
+ },
+ "orange_banner": {
+ "name": "Orange Banner",
+ "x": 864,
+ "y": 736
+ },
+ "magenta_banner": {
+ "name": "Magenta Banner",
+ "x": 672,
+ "y": 736
+ },
+ "light_blue_banner": {
+ "name": "Light Blue Banner",
+ "x": 96,
+ "y": 736
+ },
+ "yellow_banner": {
+ "name": "Yellow Banner",
+ "x": 800,
+ "y": 768
+ },
+ "lime_banner": {
+ "name": "Lime Banner",
+ "x": 480,
+ "y": 736
+ },
+ "pink_banner": {
+ "name": "Pink Banner",
+ "x": 32,
+ "y": 768
+ },
+ "gray_banner": {
+ "name": "Gray Banner",
+ "x": 736,
+ "y": 704
+ },
+ "light_gray_banner": {
+ "name": "Light Gray Banner",
+ "x": 288,
+ "y": 736
+ },
+ "cyan_banner": {
+ "name": "Cyan Banner",
+ "x": 544,
+ "y": 704
+ },
+ "purple_banner": {
+ "name": "Purple Banner",
+ "x": 224,
+ "y": 768
+ },
+ "blue_banner": {
+ "name": "Blue Banner",
+ "x": 160,
+ "y": 704
+ },
+ "brown_banner": {
+ "name": "Brown Banner",
+ "x": 352,
+ "y": 704
+ },
+ "green_banner": {
+ "name": "Green Banner",
+ "x": 928,
+ "y": 704
+ },
+ "red_banner": {
+ "name": "Red Banner",
+ "x": 416,
+ "y": 768
+ },
+ "black_banner": {
+ "name": "Black Banner",
+ "x": 992,
+ "y": 672
+ },
+ "end_crystal": {
+ "name": "End Crystal",
+ "x": 608,
+ "y": 3168
+ },
+ "chorus_fruit": {
+ "name": "Chorus Fruit",
+ "x": 960,
+ "y": 3168
+ },
+ "popped_chorus_fruit": {
+ "name": "Popped Chorus Fruit",
+ "x": 96,
+ "y": 3520
+ },
+ "beetroot": {
+ "name": "Beetroot",
+ "x": 864,
+ "y": 3168
+ },
+ "beetroot_seeds": {
+ "name": "Beetroot Seeds",
+ "x": 320,
+ "y": 3392
+ },
+ "beetroot_soup": {
+ "name": "Beetroot Soup",
+ "x": 832,
+ "y": 3168
+ },
+ "dragon_breath": {
+ "name": "Dragon's Breath",
+ "x": 32,
+ "y": 3136
+ },
+ "splash_potion": {
+ "name": "Splash Potion of Luck",
+ "x": 512,
+ "y": 3456
+ },
+ "spectral_arrow": {
+ "name": "Spectral Arrow",
+ "x": 704,
+ "y": 3616
+ },
+ "tipped_arrow": {
+ "name": "Tipped Arrow",
+ "x": 544,
+ "y": 1248
+ },
+ "lingering_potion": {
+ "name": "Lingering Potion of Luck",
+ "x": 0,
+ "y": 3488
+ },
+ "shield": {
+ "name": "Shield",
+ "x": 960,
+ "y": 2496
+ },
+ "elytra": {
+ "name": "Elytra",
+ "x": 224,
+ "y": 2496
+ },
+ "spruce_boat": {
+ "name": "Spruce Boat",
+ "x": 384,
+ "y": 3616
+ },
+ "birch_boat": {
+ "name": "Birch Boat",
+ "x": 64,
+ "y": 3616
+ },
+ "jungle_boat": {
+ "name": "Jungle Boat",
+ "x": 128,
+ "y": 3616
+ },
+ "acacia_boat": {
+ "name": "Acacia Boat",
+ "x": 32,
+ "y": 3616
+ },
+ "dark_oak_boat": {
+ "name": "Dark Oak Boat",
+ "x": 96,
+ "y": 3616
+ },
+ "totem_of_undying": {
+ "name": "Totem of Undying",
+ "x": 768,
+ "y": 3616
+ },
+ "shulker_shell": {
+ "name": "Shulker Shell",
+ "x": 256,
+ "y": 3520
+ },
+ "iron_nugget": {
+ "name": "Iron Nugget",
+ "x": 928,
+ "y": 3488
+ },
+ "knowledge_book": {
+ "name": "Knowledge Book",
+ "x": 800,
+ "y": 3584
+ },
+ "debug_stick": {
+ "name": "Debug Stick",
+ "x": 608,
+ "y": 1120
+ },
+ "music_disc_pigstep": {
+ "name": "Music Disc 13",
+ "x": 0,
+ "y": 672
+ },
+ "trident": {
+ "name": "Trident",
+ "x": 608,
+ "y": 1696
+ },
+ "phantom_membrane": {
+ "name": "Phantom Membrane",
+ "x": 32,
+ "y": 1728
+ },
+ "nautilus_shell": {
+ "name": "Nautilus Shell",
+ "x": 448,
+ "y": 1728
+ },
+ "heart_of_the_sea": {
+ "name": "Heart of the Sea",
+ "x": 416,
+ "y": 1728
+ },
+ "crossbow": {
+ "name": "Crossbow",
+ "x": 416,
+ "y": 2368
+ },
+ "suspicious_stew": {
+ "name": "Suspicious Stew",
+ "x": 928,
+ "y": 2368
+ },
+ "loom": {
+ "name": "Loom",
+ "x": 416,
+ "y": 2432
+ },
+ "flower_banner_pattern": {
+ "name": "Banner Pattern",
+ "x": 704,
+ "y": 2368
+ },
+ "creeper_banner_pattern": {
+ "name": "Banner Pattern",
+ "x": 704,
+ "y": 2368
+ },
+ "skull_banner_pattern": {
+ "name": "Banner Pattern",
+ "x": 704,
+ "y": 2368
+ },
+ "mojang_banner_pattern": {
+ "name": "Banner Pattern",
+ "x": 704,
+ "y": 2368
+ },
+ "globe_banner_pattern": {
+ "name": "Banner Pattern",
+ "x": 704,
+ "y": 2368
+ },
+ "piglin_banner_pattern": {
+ "name": "Banner Pattern",
+ "x": 704,
+ "y": 2368
+ },
+ "composter": {
+ "name": "Composter",
+ "x": 864,
+ "y": 2432
+ },
+ "barrel": {
+ "name": "Barrel",
+ "x": 64,
+ "y": 2432
+ },
+ "smoker": {
+ "name": "Smoker",
+ "x": 480,
+ "y": 2432
+ },
+ "blast_furnace": {
+ "name": "Blast Furnace",
+ "x": 288,
+ "y": 2432
+ },
+ "cartography_table": {
+ "name": "Cartography Table",
+ "x": 608,
+ "y": 2432
+ },
+ "fletching_table": {
+ "name": "Fletching Table",
+ "x": 640,
+ "y": 2432
+ },
+ "grindstone": {
+ "name": "Grindstone",
+ "x": 896,
+ "y": 2432
+ },
+ "lectern": {
+ "name": "Lectern",
+ "x": 576,
+ "y": 0
+ },
+ "smithing_table": {
+ "name": "Smithing Table",
+ "x": 672,
+ "y": 2432
+ },
+ "stonecutter": {
+ "name": "Stonecutter BE",
+ "x": 800,
+ "y": 1056
+ },
+ "bell": {
+ "name": "Bell",
+ "x": 256,
+ "y": 2400
+ },
+ "lantern": {
+ "name": "Lantern",
+ "x": 576,
+ "y": 2432
+ },
+ "soul_lantern": {
+ "name": "Soul Lantern",
+ "x": 896,
+ "y": 128
+ },
+ "sweet_berries": {
+ "name": "Sweet Berries",
+ "x": 768,
+ "y": 2432
+ },
+ "campfire": {
+ "name": "Campfire",
+ "x": 832,
+ "y": 2432
+ },
+ "soul_campfire": {
+ "name": "Soul Campfire",
+ "x": 960,
+ "y": 256
+ },
+ "shroomlight": {
+ "name": "Shroomlight",
+ "x": 704,
+ "y": 128
+ },
+ "honeycomb": {
+ "name": "Honeycomb",
+ "x": 928,
+ "y": 32
+ },
+ "bee_nest": {
+ "name": "Bee Nest",
+ "x": 992,
+ "y": 32
+ },
+ "beehive": {
+ "name": "Beehive",
+ "x": 960,
+ "y": 32
+ },
+ "honey_bottle": {
+ "name": "Honey Bottle",
+ "x": 896,
+ "y": 32
+ },
+ "honey_block": {
+ "name": "Honey Block",
+ "x": 320,
+ "y": 96
+ },
+ "honeycomb_block": {
+ "name": "Honeycomb Block",
+ "x": 64,
+ "y": 96
+ },
+ "lodestone": {
+ "name": "Lodestone",
+ "x": 928,
+ "y": 192
+ },
+ "netherite_block": {
+ "name": "Block of Netherite",
+ "x": 96,
+ "y": 128
+ },
+ "ancient_debris": {
+ "name": "Ancient Debris",
+ "x": 160,
+ "y": 128
+ },
+ "target": {
+ "name": "Target",
+ "x": 96,
+ "y": 192
+ },
+ "crying_obsidian": {
+ "name": "Crying Obsidian",
+ "x": 128,
+ "y": 192
+ },
+ "blackstone": {
+ "name": "Blackstone",
+ "x": 96,
+ "y": 256
+ },
+ "blackstone_slab": {
+ "name": "Blackstone Slab",
+ "x": 608,
+ "y": 256
+ },
+ "blackstone_stairs": {
+ "name": "Blackstone Stairs",
+ "x": 640,
+ "y": 256
+ },
+ "gilded_blackstone": {
+ "name": "Gilded Blackstone",
+ "x": 512,
+ "y": 256
+ },
+ "polished_blackstone": {
+ "name": "Polished Blackstone",
+ "x": 544,
+ "y": 256
+ },
+ "polished_blackstone_slab": {
+ "name": "Polished Blackstone Slab",
+ "x": 800,
+ "y": 256
+ },
+ "polished_blackstone_stairs": {
+ "name": "Polished Blackstone Stairs",
+ "x": 832,
+ "y": 256
+ },
+ "chiseled_polished_blackstone": {
+ "name": "Chiseled Polished Blackstone",
+ "x": 416,
+ "y": 256
+ },
+ "polished_blackstone_bricks": {
+ "name": "Polished Blackstone Bricks",
+ "x": 576,
+ "y": 256
+ },
+ "polished_blackstone_brick_slab": {
+ "name": "Polished Blackstone Brick Slab",
+ "x": 704,
+ "y": 256
+ },
+ "polished_blackstone_brick_stairs": {
+ "name": "Polished Blackstone Brick Stairs",
+ "x": 736,
+ "y": 256
+ },
+ "cracked_polished_blackstone_bricks": {
+ "name": "Cracked Polished Blackstone Bricks",
+ "x": 480,
+ "y": 256
+ },
+ "respawn_anchor": {
+ "name": "Respawn Anchor",
+ "x": 448,
+ "y": 192
+ }
+}
\ No newline at end of file
diff --git a/lib/menus/components/bossbars_overlay.js b/lib/menus/components/bossbars_overlay.js
new file mode 100644
index 0000000000000000000000000000000000000000..77d29166b631c9ed92607a414b251bd5bfb01a2a
--- /dev/null
+++ b/lib/menus/components/bossbars_overlay.js
@@ -0,0 +1,147 @@
+const { LitElement, html, css } = require('lit')
+const { styleMap } = require('lit/directives/style-map.js')
+
+const colors = ['pink', 'blue', 'red', 'green', 'yellow', 'purple', 'white']
+const divs = [0, 6, 10, 12, 20]
+const translations = {
+ 'entity.minecraft.ender_dragon': 'Ender Dragon',
+ 'entity.minecraft.wither': 'Wither'
+}
+class BossBar extends LitElement {
+ constructor (bar) {
+ super()
+ this.bar = bar
+ this.title = ''
+ this.progress = 0
+ this.bossBarStyles = {}
+ this.fillStyles = {}
+ this.div1Styles = {}
+ this.div2Styles = {}
+ }
+
+ static get styles () {
+ return css`
+ .container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+ .title {
+ font-size: 7px;
+ color: #fff;
+ }
+ .bossbar {
+ background-image: url("textures/1.18.1/gui/bars.png");
+ width: 182px;
+ height: 5px;
+ position: relative;
+ }
+ .bossbar .fill {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 5px;
+ width: 0;
+ background-image: url("textures/1.18.1/gui/bars.png");
+ }`
+ }
+
+ static get properties () {
+ return {
+ bar: { type: Object }
+ }
+ }
+
+ render () {
+ this.updateBar(this.bar)
+
+ return html`
+ `
+ }
+
+ setTitle (bar) {
+ if (bar._title.text) this.title = bar._title.text
+ else this.title = translations[this._title.translate] || 'Unkown Entity'
+ }
+
+ setColor (bar) {
+ this.bossBarStyles.backgroundPositionY = `-${colors.indexOf(bar._color) * 10}px`
+ this.fillStyles.backgroundPositionY = `-${colors.indexOf(bar._color) * 10 + 5}px`
+ }
+
+ setProgress (bar) {
+ this.fillStyles.width = `${bar._health * 100}%`
+ this.div2Styles.width = `${bar._health * 100}%`
+ }
+
+ setDiv (bar) {
+ this.div1Styles.backgroundPositionY = `-${divs.indexOf(bar._dividers) * 10 + 70}px`
+ this.div2Styles.backgroundPositionY = `-${divs.indexOf(bar._dividers) * 10 + 75}px`
+ }
+
+ updateBar (bar) {
+ this.setTitle(bar)
+ this.setColor(bar)
+ this.setDiv(bar)
+ this.setProgress(bar)
+ }
+}
+
+class BossBars extends LitElement {
+ constructor () {
+ super()
+ this.bossBars = new Map()
+ }
+
+ static get styles () {
+ return css`
+ .bossBars {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ position: absolute;
+ top: 9px;
+ left: 50%;
+ transform: translate(-50%);
+ }`
+ }
+
+ static get properties () {
+ return {
+ bossBars: { type: Map }
+ }
+ }
+
+ render () {
+ return html`
+ ${[...this.bossBars.values()]}
+
`
+ }
+
+ init () {
+ this.bot.on('bossBarCreated', async (bossBar) => {
+ this.bossBars.set(bossBar.entityUUID, new BossBar(bossBar))
+ this.requestUpdate()
+ })
+ this.bot.on('bossBarUpdated', (bossBar) => {
+ const bar = this.bossBars.get(bossBar.entityUUID)
+ bar.bar = bossBar
+ bar.requestUpdate()
+ })
+ this.bot.on('bossBarDeleted', (bossBar) => {
+ this.bossBars.delete(bossBar.entityUUID)
+ this.requestUpdate()
+ })
+ }
+}
+
+window.customElements.define('pmui-bossbars-overlay', BossBars)
+window.customElements.define('pmui-bossbar', BossBar)
diff --git a/lib/menus/components/breath_bar.js b/lib/menus/components/breath_bar.js
new file mode 100644
index 0000000000000000000000000000000000000000..fe8fcca8a9888d000182bfbfbe930e6a457f5b53
--- /dev/null
+++ b/lib/menus/components/breath_bar.js
@@ -0,0 +1,88 @@
+const { LitElement, html, css } = require('lit')
+
+class BreathBar extends LitElement {
+ static get styles () {
+ return css`
+ .breathbar {
+ position: absolute;
+ display: flex;
+ flex-direction: row-reverse;
+ left: calc(50% + 91px);
+ transform: translate(-100%);
+ bottom: 40px;
+ --offset: calc(-1 * 16px);
+ --bg-x: calc(-1 * 16px);
+ --bg-y: calc(-1 * 18px);
+ }
+
+ .breath {
+ width: 9px;
+ height: 9px;
+ margin-left: -1px;
+ }
+
+ .breath.full {
+ background-image: url('textures/1.17.1/gui/icons.png');
+ background-size: 256px;
+ background-position: var(--offset) var(--bg-y);
+ }
+
+ .breath.half {
+ background-image: url('textures/1.17.1/gui/icons.png');
+ background-size: 256px;
+ background-position: calc(var(--offset) - 9) var(--bg-y);
+ }
+ `
+ }
+
+ gameModeChanged (gamemode) {
+ this.shadowRoot.querySelector('#breathbar').classList.toggle('creative', gamemode === 1)
+ }
+
+ updateOxygen (hValue) {
+ const breathbar = this.shadowRoot.querySelector('#breathbar')
+ breathbar.style.display = 'block'
+
+ const breaths = breathbar.children
+
+ for (let i = 0; i < breaths.length; i++) {
+ breaths[i].classList.remove('full')
+ breaths[i].classList.remove('half')
+ }
+
+ for (let i = 0; i < Math.ceil(hValue / 2); i++) {
+ if (i >= breaths.length) break
+
+ if (hValue % 2 !== 0 && Math.ceil(hValue / 2) === i + 1) {
+ breaths[i].classList.add('half')
+ } else {
+ breaths[i].classList.add('full')
+ }
+ }
+
+ // if (hValue === 20) {
+ // setTimeout(() => {
+ // breathbar.style.display = 'none'
+ // }, 1000)
+ // }
+ }
+
+ render () {
+ return html`
+
+ `
+ }
+}
+
+window.customElements.define('pmui-breathbar', BreathBar)
diff --git a/lib/menus/components/button.js b/lib/menus/components/button.js
new file mode 100644
index 0000000000000000000000000000000000000000..fde9b10e644364315a82964146bde0a37413d3a6
--- /dev/null
+++ b/lib/menus/components/button.js
@@ -0,0 +1,140 @@
+const { LitElement, html, css } = require('lit')
+
+const audioContext = new window.AudioContext()
+const sounds = {}
+
+async function playSound (path) {
+ let volume = 1
+ const options = document.getElementById('options-screen')
+ if (options) {
+ volume = options.sound / 100
+ }
+
+ let soundBuffer = sounds[path]
+
+ if (!soundBuffer) {
+ const res = await window.fetch(path)
+ const data = await res.arrayBuffer()
+
+ soundBuffer = await audioContext.decodeAudioData(data)
+ sounds[path] = soundBuffer
+ }
+
+ const gainNode = audioContext.createGain()
+ const source = audioContext.createBufferSource()
+ source.buffer = soundBuffer
+ source.connect(gainNode)
+ gainNode.connect(audioContext.destination)
+ gainNode.gain.value = volume
+ source.start(0)
+}
+
+class Button extends LitElement {
+ static get styles () {
+ return css`
+ .button {
+ --txrV: 66px;
+ position: relative;
+ width: 200px;
+ height: 20px;
+ font-family: minecraft, mojangles, monospace;
+ font-size: 10px;
+ color: white;
+ text-shadow: 1px 1px #222;
+ border: none;
+ z-index: 1;
+ outline: none;
+ }
+
+ .button:hover,
+ .button:focus-visible {
+ --txrV: 86px;
+ }
+
+ .button:disabled {
+ --txrV: 46px;
+ color: #A0A0A0;
+ text-shadow: 1px 1px #111;
+ }
+
+ .button::after {
+ content: '';
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 50%;
+ height: 20px;
+ background: url('textures/1.17.1/gui/widgets.png');
+ background-size: 256px;
+ background-position-y: calc(var(--txrV) * -1);
+ z-index: -1;
+ }
+
+ .button::before {
+ content: '';
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 50%;
+ width: 50%;
+ height: 20px;
+ background: url('textures/1.17.1/gui/widgets.png');
+ background-size: 256px;
+ background-position-x: calc(-200px + 100%);
+ background-position-y: calc(var(--txrV) * -1);
+ z-index: -1;
+ }
+ `
+ }
+
+ static get properties () {
+ return {
+ label: {
+ type: String,
+ attribute: 'pmui-label'
+ },
+ width: {
+ type: String,
+ attribute: 'pmui-width'
+ },
+ disabled: {
+ type: Boolean,
+ attribute: 'pmui-disabled'
+ },
+ onPress: {
+ type: Function,
+ attribute: 'pmui-click'
+ }
+ }
+ }
+
+ constructor () {
+ super()
+ this.label = ''
+ this.disabled = false
+ this.width = '200px'
+ this.onPress = () => {}
+ }
+
+ render () {
+ return html`
+
+ ${this.label}
+ `
+ }
+
+ onBtnClick () {
+ playSound('click_stereo.ogg')
+ this.dispatchEvent(new window.CustomEvent('pmui-click'))
+ }
+}
+
+window.customElements.define('pmui-button', Button)
+const _playSound = playSound
+export { _playSound as playSound }
diff --git a/lib/menus/components/common.js b/lib/menus/components/common.js
new file mode 100644
index 0000000000000000000000000000000000000000..17b7275c9950d620b4cdd63f604227c3aa10e4bb
--- /dev/null
+++ b/lib/menus/components/common.js
@@ -0,0 +1,72 @@
+import { css } from 'lit'
+
+const commonCss = css`
+ .dirt-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: url('textures/1.17.1/gui/options_background.png'), rgba(0, 0, 0, 0.75);
+ background-size: 16px;
+ background-repeat: repeat;
+ width: 100%;
+ height: 100%;
+ transform-origin: top left;
+ transform: scale(2);
+ background-blend-mode: overlay;
+ }
+
+ .bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: rgba(0, 0, 0, 0.75);
+ width: 100%;
+ height: 100%;
+ }
+
+ .title {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translate(-50%);
+ font-size: 10px;
+ color: white;
+ text-align: center;
+ text-shadow: 1px 1px #222;
+ }
+
+ .text {
+ color: white;
+ font-size: 10px;
+ text-shadow: 1px 1px #222;
+ }
+`
+
+/** @returns {boolean} */
+function isMobile () {
+ const m = require('ismobilejs').default()
+ return m.any
+}
+
+/**
+ * @param {string} url
+ */
+function openURL (url) {
+ window.open(url, '_blank', 'noopener,noreferrer')
+}
+
+/**
+ * @param {HTMLElement} prev
+ * @param {HTMLElement} next
+ */
+function displayScreen (prev, next) {
+ prev.style.display = 'none'
+ next.style.display = 'block'
+}
+
+export {
+ commonCss,
+ isMobile,
+ openURL,
+ displayScreen
+}
diff --git a/lib/menus/components/debug_overlay.js b/lib/menus/components/debug_overlay.js
new file mode 100644
index 0000000000000000000000000000000000000000..293929b11bb73867771ce16eb42ce46a8d0dc346
--- /dev/null
+++ b/lib/menus/components/debug_overlay.js
@@ -0,0 +1,146 @@
+const { LitElement, html, css } = require('lit')
+
+class DebugOverlay extends LitElement {
+ static get styles () {
+ return css`
+ .debug-left-side,
+ .debug-right-side {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ z-index: 40;
+ }
+
+ .debug-left-side {
+ top: 1px;
+ left: 1px;
+ }
+
+ .debug-right-side {
+ top: 1px;
+ right: 1px;
+ }
+
+ p {
+ display: block;
+ color: white;
+ font-size: 10px;
+ width: fit-content;
+ height: 9px;
+ margin: 0;
+ padding: 0;
+ padding-bottom: 1px;
+ background: rgba(110, 110, 110, 0.5);
+ }
+
+ .debug-right-side p {
+ margin-left: auto;
+ }
+
+ .empty {
+ display: block;
+ height: 9px;
+ }
+ `
+ }
+
+ static get properties () {
+ return {
+ showOverlay: { type: Boolean },
+ cursorBlock: { type: Object },
+ bot: { type: Object },
+ customEntries: { type: Object }
+ }
+ }
+
+ constructor () {
+ super()
+ this.showOverlay = false
+ this.customEntries = {}
+ }
+
+ firstUpdated () {
+ document.addEventListener('keydown', e => {
+ e ??= window.event
+ if (e.key === 'F3') {
+ this.showOverlay = !this.showOverlay
+ e.preventDefault()
+ }
+ })
+ }
+
+ updated (changedProperties) {
+ if (changedProperties.has('bot')) {
+ this.bot.on('move', () => {
+ this.requestUpdate()
+ })
+ this.bot.on('time', () => {
+ this.requestUpdate()
+ })
+ this.bot.on('entitySpawn', () => {
+ this.requestUpdate()
+ })
+ this.bot.on('entityGone', () => {
+ this.requestUpdate()
+ })
+ }
+ }
+
+ render () {
+ if (!this.showOverlay) {
+ return html``
+ }
+
+ const target = this.cursorBlock
+ const targetDiggable = (target && this.bot.canDigBlock(target))
+
+ const pos = this.bot.entity.position
+ const rot = [this.bot.entity.yaw, this.bot.entity.pitch]
+
+ const viewDegToMinecraft = (yaw) => yaw % 360 - 180 * (yaw < 0 ? -1 : 1)
+
+ const quadsDescription = [
+ 'north (towards negative Z)',
+ 'east (towards positive X)',
+ 'south (towards positive Z)',
+ 'west (towards negative X)'
+ ]
+
+ const minecraftYaw = viewDegToMinecraft(rot[0] * -180 / Math.PI)
+ const minecraftQuad = Math.floor(((minecraftYaw + 180) / 90 + 0.5) % 4)
+
+ const renderProp = (name, value) => {
+ return html`${name}: ${typeof value === 'boolean' ? html`${value} ` : value}
`
+ }
+
+ const skyL = this.bot.world.getSkyLight(this.bot.entity.position)
+ const biomeId = this.bot.world.getBiome(this.bot.entity.position)
+
+ return html`
+
+
Prismarine Web Client (${this.bot.version})
+
E: ${Object.values(this.bot.entities).length}
+
${this.bot.game.dimension}
+
+
XYZ: ${pos.x.toFixed(3)} / ${pos.y.toFixed(3)} / ${pos.z.toFixed(3)}
+
Chunk: ${Math.floor(pos.x % 16)} ~ ${Math.floor(pos.z % 16)} in ${Math.floor(pos.x / 16)} ~ ${Math.floor(pos.z / 16)}
+
Facing (viewer): ${rot[0].toFixed(3)} ${rot[1].toFixed(3)}
+
Facing (minecraft): ${quadsDescription[minecraftQuad]} (${minecraftYaw.toFixed(1)} ${(rot[1] * -180 / Math.PI).toFixed(1)})
+
Light: ${skyL} (${skyL} sky)
+
Biome: minecraft:${window.mcData.biomesArray[biomeId].name}
+
Day: ${this.bot.time.day}
+
+ ${Object.entries(this.customEntries).map(([name, value]) => html`
${name}: ${value}
`)}
+
+
+
+
Renderer: three.js r${global.THREE.REVISION}
+
+ ${targetDiggable ? html`
${target.name}
${Object.entries(target.getProperties()).map(([n, p], idx, arr) => renderProp(n, p, arr[idx + 1]))}` : ''}
+ ${targetDiggable ? html`
Looking at: ${target.position.x} ${target.position.y} ${target.position.z}
` : ''}
+
+ `
+ }
+}
+
+window.customElements.define('pmui-debug-overlay', DebugOverlay)
diff --git a/lib/menus/components/edit_box.js b/lib/menus/components/edit_box.js
new file mode 100644
index 0000000000000000000000000000000000000000..38d206466c0ab819ede2483d9f3e874518a40103
--- /dev/null
+++ b/lib/menus/components/edit_box.js
@@ -0,0 +1,97 @@
+const { LitElement, html, css } = require('lit')
+
+class EditBox extends LitElement {
+ static get styles () {
+ return css`
+ .edit-container {
+ position: relative;
+ width: 200px;
+ height: 20px;
+ background: black;
+ border: 1px solid grey;
+ }
+
+ .edit-container:hover,
+ .edit-container:focus-within {
+ border-color: white;
+ }
+
+ .edit-container label {
+ position: absolute;
+ z-index: 2;
+ pointer-events: none;
+ bottom: 21px;
+ left: 0;
+ font-size: 10px;
+ color: rgb(206, 206, 206);
+ text-shadow: 1px 1px black;
+ }
+
+ .edit-box {
+ position: relative;
+ outline: none;
+ border: none;
+ background: none;
+ left: 1px;
+ width: calc(100% - 2px);
+ height: 100%;
+ font-family: minecraft, mojangles, monospace;
+ font-size: 10px;
+ color: white;
+ text-shadow: 1px 1px #222;
+ }
+ `
+ }
+
+ constructor () {
+ super()
+ this.width = '200px'
+ this.id = ''
+ this.value = ''
+ this.label = ''
+ }
+
+ static get properties () {
+ return {
+ width: {
+ type: String,
+ attribute: 'pmui-width'
+ },
+ id: {
+ type: String,
+ attribute: 'pmui-id'
+ },
+ label: {
+ type: String,
+ attribute: 'pmui-label'
+ },
+ value: {
+ type: String,
+ attribute: 'pmui-value'
+ }
+ }
+ }
+
+ render () {
+ return html`
+
+ ${this.label}
+ { this.value = e.target.value }}
+ class="edit-box">
+
+ `
+ }
+}
+
+window.customElements.define('pmui-editbox', EditBox)
diff --git a/lib/menus/components/food_bar.js b/lib/menus/components/food_bar.js
new file mode 100644
index 0000000000000000000000000000000000000000..de1342ce32ca96ba870b56feb7ee41a56aa8c453
--- /dev/null
+++ b/lib/menus/components/food_bar.js
@@ -0,0 +1,117 @@
+const { LitElement, html, css } = require('lit')
+
+class FoodBar extends LitElement {
+ static get styles () {
+ return css`
+ .foodbar {
+ position: absolute;
+ display: flex;
+ flex-direction: row-reverse;
+ left: calc(50% + 91px);
+ transform: translate(-100%);
+ bottom: 30px;
+ --lightened: 0;
+ --offset: calc(-1 * (52px));
+ --bg-x: calc(-1 * (16px + 9px * var(--lightened)));
+ --bg-y: calc(-1 * 27px);
+ }
+
+ .food {
+ width: 9px;
+ height: 9px;
+ background-image: url('textures/1.17.1/gui/icons.png'), url('textures/1.17.1/gui/icons.png');
+ background-size: 256px, 256px;
+ background-position: var(--bg-x) var(--bg-y), var(--bg-x) var(--bg-y);
+ margin-left: -1px;
+ }
+
+ .food.full {
+ background-position: var(--offset) var(--bg-y), var(--bg-x) var(--bg-y);
+ }
+
+ .food.half {
+ background-position: calc(var(--offset) - 9px) var(--bg-y), var(--bg-x) var(--bg-y);
+ }
+
+ .foodbar.low .food {
+ animation: lowHungerAnim 0.2s steps(2, end) infinite;
+ }
+
+ .foodbar.low .food:nth-of-type(2n) {
+ animation-direction: reverse;
+ }
+
+ .foodbar.low .food:nth-of-type(3n) {
+ animation-duration: 0.1s;
+ }
+
+ .foodbar.updated {
+ animation: updatedAnim 0.3s steps(2, end) 2;
+ }
+
+ @keyframes lowHungerAnim {
+ to { transform: translateY(1px); }
+ }
+
+ @keyframes updatedAnim {
+ to { --lightened: 1; }
+ }
+ `
+ }
+
+ gameModeChanged (gamemode) {
+ this.shadowRoot.querySelector('#foodbar').classList.toggle('creative', gamemode === 1)
+ }
+
+ onHungerUpdate () {
+ this.shadowRoot.querySelector('#foodbar').classList.toggle('updated', true)
+ if (this.hungerTimeout) clearTimeout(this.hungerTimeout)
+ this.hungerTimeout = setTimeout(() => {
+ this.shadowRoot.querySelector('#foodbar').classList.toggle('updated', false)
+ this.hungerTimeout = null
+ }, 1000)
+ }
+
+ updateHunger (hValue, d) {
+ const foodbar = this.shadowRoot.querySelector('#foodbar')
+ foodbar.classList.toggle('low', hValue <= 5)
+
+ const foods = foodbar.children
+
+ for (let i = 0; i < foods.length; i++) {
+ foods[i].classList.remove('full')
+ foods[i].classList.remove('half')
+ }
+
+ // if (d) this.onHungerUpdate()
+
+ for (let i = 0; i < Math.ceil(hValue / 2); i++) {
+ if (i >= foods.length) break
+
+ if (hValue % 2 !== 0 && Math.ceil(hValue / 2) === i + 1) {
+ foods[i].classList.add('half')
+ } else {
+ foods[i].classList.add('full')
+ }
+ }
+ }
+
+ render () {
+ return html`
+
+ `
+ }
+}
+
+window.customElements.define('pmui-foodbar', FoodBar)
diff --git a/lib/menus/components/health_bar.js b/lib/menus/components/health_bar.js
new file mode 100644
index 0000000000000000000000000000000000000000..9c0d8803de8732b01f1361f5f35584ef6b1f98ba
--- /dev/null
+++ b/lib/menus/components/health_bar.js
@@ -0,0 +1,158 @@
+const { LitElement, html, css } = require('lit')
+
+function getEffectClass (effect) {
+ switch (effect.id) {
+ case 19: return 'poisoned'
+ case 20: return 'withered'
+ case 22: return 'absorption'
+ default: return ''
+ }
+}
+
+class HealthBar extends LitElement {
+ static get styles () {
+ return css`
+ .health {
+ position: absolute;
+ display: flex;
+ flex-direction: row;
+ left: calc(50% - 91px);
+ bottom: 30px;
+ --hardcore: 0;
+ --kind: 0;
+ --lightened: 0;
+ --offset: calc(-1 * (52px + (9px * (4 * var(--kind) + var(--lightened) * 2)) ));
+ --bg-x: calc(-1 * (16px + 9px * var(--lightened)));
+ --bg-y: calc(-1 * var(--hardcore) * 45px);
+ }
+
+ .health.creative {
+ display: none;
+ }
+
+ .health.hardcore {
+ --hardcore: 1;
+ }
+
+ .health.poisoned {
+ --kind: 1;
+ }
+
+ .health.withered {
+ --kind: 2;
+ }
+
+ .health.absorption {
+ --kind: 3;
+ }
+
+ .heart {
+ width: 9px;
+ height: 9px;
+ background-image: url('textures/1.17.1/gui/icons.png'), url('textures/1.17.1/gui/icons.png');
+ background-size: 256px, 256px;
+ background-position: var(--bg-x) var(--bg-y), var(--bg-x) var(--bg-y);
+ margin-left: -1px;
+ }
+
+ .heart.full {
+ background-position: var(--offset) var(--bg-y), var(--bg-x) var(--bg-y);
+ }
+
+ .heart.half {
+ background-position: calc(var(--offset) - 9px) var(--bg-y), var(--bg-x) var(--bg-y);
+ }
+
+ .health.low .heart {
+ animation: lowHealthAnim 0.2s steps(2, end) infinite;
+ }
+
+ .health.low .heart:nth-of-type(2n) {
+ animation-direction: reverse;
+ }
+
+ .health.low .heart:nth-of-type(3n) {
+ animation-duration: 0.1s;
+ }
+
+ .health.damaged {
+ animation: damagedAnim 0.3s steps(2, end) 2;
+ }
+
+ @keyframes lowHealthAnim {
+ to {
+ transform: translateY(1px);
+ }
+ }
+
+ @keyframes damagedAnim {
+ to { --lightened: 1; }
+ }
+ `
+ }
+
+ effectAdded (effect) {
+ this.shadowRoot.querySelector('#health').classList.add(getEffectClass(effect))
+ }
+
+ effectEnded (effect) {
+ this.shadowRoot.querySelector('#health').classList.remove(getEffectClass(effect))
+ }
+
+ onDamage () {
+ this.shadowRoot.querySelector('#health').classList.toggle('damaged', true)
+ if (this.hurtTimeout) clearTimeout(this.hurtTimeout)
+ this.hurtTimeout = setTimeout(() => {
+ this.shadowRoot.querySelector('#health').classList.toggle('damaged', false)
+ this.hurtTimeout = null
+ }, 1000)
+ }
+
+ gameModeChanged (gamemode, hardcore) {
+ this.shadowRoot.querySelector('#health').classList.toggle('creative', gamemode === 1)
+ this.shadowRoot.querySelector('#health').classList.toggle('hardcore', hardcore)
+ }
+
+ updateHealth (hValue, d) {
+ const health = this.shadowRoot.querySelector('#health')
+ health.classList.toggle('low', hValue <= 4)
+
+ const hearts = health.children
+
+ for (let i = 0; i < hearts.length; i++) {
+ hearts[i].classList.remove('full')
+ hearts[i].classList.remove('half')
+ }
+
+ if (d) this.onDamage()
+
+ for (let i = 0; i < Math.ceil(hValue / 2); i++) {
+ if (i >= hearts.length) break
+
+ if (hValue % 2 !== 0 && Math.ceil(hValue / 2) === i + 1) {
+ hearts[i].classList.add('half')
+ } else {
+ hearts[i].classList.add('full')
+ }
+ }
+ }
+
+ render () {
+ return html`
+
+ `
+ }
+}
+
+window.customElements.define('pmui-healthbar', HealthBar)
diff --git a/lib/menus/components/hotbar.js b/lib/menus/components/hotbar.js
new file mode 100644
index 0000000000000000000000000000000000000000..e98a584864913eadf246cdf548e45070d26830af
--- /dev/null
+++ b/lib/menus/components/hotbar.js
@@ -0,0 +1,210 @@
+const { LitElement, html, css } = require('lit')
+const invsprite = require('../../invsprite.json')
+
+class Hotbar extends LitElement {
+ static get styles () {
+ return css`
+ .hotbar {
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translate(-50%);
+ width: 182px;
+ height: 22px;
+ background: url("textures/1.16.4/gui/widgets.png");
+ background-size: 256px;
+ }
+
+ #hotbar-selected {
+ position: absolute;
+ left: -1px;
+ top: -1px;
+ width: 24px;
+ height: 24px;
+ background: url("textures/1.16.4/gui/widgets.png");
+ background-size: 256px;
+ background-position-y: -22px;
+ }
+
+ #hotbar-items-wrapper {
+ position: absolute;
+ top: 0;
+ left: 1px;
+ display: flex;
+ flex-direction: row;
+ height: 22px;
+ margin: 0;
+ padding: 0;
+ }
+
+ .hotbar-item {
+ position: relative;
+ width: 20px;
+ height: 22px;
+ }
+
+ .item-icon {
+ top: 3px;
+ left: 2px;
+ position: absolute;
+ width: 32px;
+ height: 32px;
+ transform-origin: top left;
+ transform: scale(0.5);
+ background-image: url('invsprite.png');
+ background-size: 1024px auto;
+ }
+
+ .item-stack {
+ position: absolute;
+ color: white;
+ font-size: 10px;
+ text-shadow: 1px 1px 0 rgb(63, 63, 63);
+ right: 1px;
+ bottom: 1px;
+ }
+
+ #hotbar-item-name {
+ color: white;
+ position: absolute;
+ bottom: 51px;
+ left: 50%;
+ transform: translate(-50%);
+ text-shadow: rgb(63, 63, 63) 1px 1px 0px;
+ font-family: mojangles, minecraft, monospace;
+ font-size: 10px;
+ text-align: center;
+ }
+
+ .hotbar-item-name-fader {
+ opacity: 0;
+ transition: visibility 0s, opacity 1s linear;
+ transition-delay: 2s;
+ }
+ `
+ }
+
+ static get properties () {
+ return {
+ activeItemName: { type: String },
+ bot: { type: Object },
+ viewerVersion: { type: String }
+ }
+ }
+
+ constructor () {
+ super()
+ this.activeItemName = ''
+ }
+
+ updated (changedProperties) {
+ if (changedProperties.has('bot')) {
+ // inventory listener
+ this.bot.once('spawn', () => {
+ this.init()
+ })
+ }
+ }
+
+ init () {
+ this.reloadHotbar()
+ this.reloadHotbarSelected(0)
+
+ document.addEventListener('wheel', (e) => {
+ const newSlot = ((this.bot.quickBarSlot + Math.sign(e.deltaY)) % 9 + 9) % 9
+ this.reloadHotbarSelected(newSlot)
+ })
+
+ document.addEventListener('keydown', (e) => {
+ const numPressed = e.code.substr(5)
+ if (numPressed < 1 || numPressed > 9) return
+ this.reloadHotbarSelected(numPressed - 1)
+ })
+
+ this.bot.inventory.on('updateSlot', (slot, oldItem, newItem) => {
+ if (slot >= this.bot.inventory.hotbarStart + 9) return
+ if (slot < this.bot.inventory.hotbarStart) return
+
+ const sprite = newItem ? invsprite[newItem.name] : invsprite.air
+ const slotEl = this.shadowRoot.getElementById('hotbar-' + (slot - this.bot.inventory.hotbarStart))
+ const slotIcon = slotEl.children[0]
+ const slotStack = slotEl.children[1]
+ slotIcon.style['background-position-x'] = `-${sprite.x}px`
+ slotIcon.style['background-position-y'] = `-${sprite.y}px`
+ slotStack.innerHTML = newItem?.count > 1 ? newItem.count : ''
+ })
+ }
+
+ async reloadHotbar () {
+ for (let i = 0; i < 9; i++) {
+ const item = this.bot.inventory.slots[this.bot.inventory.hotbarStart + i]
+ const sprite = item ? invsprite[item.name] : invsprite.air
+ const slotEl = this.shadowRoot.getElementById('hotbar-' + i)
+ const slotIcon = slotEl.children[0]
+ const slotStack = slotEl.children[1]
+ slotIcon.style['background-position-x'] = `-${sprite.x}px`
+ slotIcon.style['background-position-y'] = `-${sprite.y}px`
+ slotStack.innerHTML = item?.count > 1 ? item.count : ''
+ }
+ }
+
+ async reloadHotbarSelected (slot) {
+ const item = this.bot.inventory.slots[this.bot.inventory.hotbarStart + slot]
+ const newLeftPos = (-1 + 20 * slot) + 'px'
+ this.shadowRoot.getElementById('hotbar-selected').style.left = newLeftPos
+ this.bot.setQuickBarSlot(slot)
+ this.activeItemName = item?.displayName ?? ''
+ const name = this.shadowRoot.getElementById('hotbar-item-name')
+ name.classList.remove('hotbar-item-name-fader')
+ setTimeout(() => name.classList.add('hotbar-item-name-fader'), 10)
+ }
+
+ render () {
+ return html`
+
+
${this.activeItemName}
+
+
+
+ `
+ }
+}
+
+window.customElements.define('pmui-hotbar', Hotbar)
diff --git a/lib/menus/components/playerlist_overlay.js b/lib/menus/components/playerlist_overlay.js
new file mode 100644
index 0000000000000000000000000000000000000000..c98441d8a51e6dd84a0751fced62ddf8f85d98cc
--- /dev/null
+++ b/lib/menus/components/playerlist_overlay.js
@@ -0,0 +1,179 @@
+const { LitElement, html, css } = require('lit')
+
+const MAX_ROWS_PER_COL = 10
+
+class PlayerListOverlay extends LitElement {
+ static get styles () {
+ return css`
+ .playerlist-container {
+ position: absolute;
+ background-color: rgba(0, 0, 0, 0.3);
+ top: 9px;
+ left: 50%;
+ transform: translate(-50%);
+ width: fit-content;
+ padding: 1px;
+ display: flex;
+ flex-direction: column;
+ gap: 1px 0;
+ place-items: center;
+ z-index: 30;
+ }
+
+ .title {
+ color: white;
+ text-shadow: 1px 1px 0px #3f3f3f;
+ font-size: 10px;
+ margin: 0;
+ padding: 0;
+ }
+
+ .playerlist-entry {
+ overflow: hidden;
+ color: white;
+ font-size: 10px;
+ margin: 0px;
+ line-height: calc(100% - 1px);
+ text-shadow: 1px 1px 0px #3f3f3f;
+ font-family: mojangles, minecraft, monospace;
+ background: rgba(255, 255, 255, 0.1);
+ width: 100%;
+ }
+
+ .active-player {
+ color: rgb(42, 204, 237);
+ text-shadow: 1px 1px 0px rgb(4, 44, 67);
+ }
+
+ .playerlist-ping {
+ text-align: right;
+ float: right;
+ padding-left: 10px;
+ }
+
+ .playerlist-ping-value {
+ color: rgb(114, 255, 114);
+ text-shadow: 1px 1px 0px rgb(28, 105, 28);
+ float: left;
+ margin: 0;
+ margin-right: 1px;
+ }
+
+ .playerlist-ping-label {
+ text-shadow: 1px 1px 0px #3f3f3f;
+ color: white;
+ float: right;
+ margin: 0px;
+ }
+
+ .player-lists {
+ display: flex;
+ flex-direction: row;
+ place-items: center;
+ place-content: center;
+ gap: 0 4px;
+ }
+
+ .player-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1px 0;
+ min-width: 80px;
+ }
+ `
+ }
+
+ static get properties () {
+ return {
+ serverIP: { type: String },
+ clientId: { type: String },
+ players: { type: Object }
+ }
+ }
+
+ constructor () {
+ super()
+ this.serverIP = ''
+ this.clientId = ''
+ this.players = {}
+ }
+
+ init (bot, ip) {
+ const playerList = this.shadowRoot.querySelector('#playerlist-container')
+
+ this.isOpen = false
+ this.players = bot.players
+ this.clientId = bot.player.uuid
+ this.serverIP = ip
+
+ this.requestUpdate()
+
+ const showList = (shouldShow = true) => {
+ playerList.style.display = shouldShow ? 'block' : 'none'
+ this.isOpen = shouldShow
+ }
+
+ document.addEventListener('keydown', e => {
+ e ??= window.event
+ if (e.key === 'Tab') {
+ showList(true)
+ e.preventDefault()
+ }
+ })
+
+ document.addEventListener('keyup', e => {
+ if (!this.isOpen) return
+ e ??= window.event
+ if (e.key === 'Tab') {
+ showList(false)
+ e.preventDefault()
+ }
+ })
+
+ bot.on('playerUpdated', () => this.requestUpdate()) // LitElement seems to be batching requests, so it should be fine?
+ bot.on('playerJoined', () => this.requestUpdate())
+ bot.on('playerLeft', () => this.requestUpdate())
+ }
+
+ render () {
+ const lists = []
+ const players = Object.values(this.players).sort((a, b) => {
+ if (a.username > b.username) return 1
+ if (a.username < b.username) return -1
+ return 0
+ })
+
+ let tempList = []
+ for (let i = 0; i < players.length; i++) {
+ tempList.push(players[i])
+
+ if ((i + 1) / MAX_ROWS_PER_COL === 1 || i + 1 === players.length) {
+ lists.push([...tempList])
+ tempList = []
+ }
+ }
+
+ return html`
+
+
Server IP: ${this.serverIP}
+
+ ${lists.map(list => html`
+
+ ${list.map(player => html`
+
+ `)}
+
+ `)}
+
+
+ `
+ }
+}
+
+window.customElements.define('pmui-playerlist-overlay', PlayerListOverlay)
diff --git a/lib/menus/components/slider.js b/lib/menus/components/slider.js
new file mode 100644
index 0000000000000000000000000000000000000000..57e2317b420b86ca4ceb853fcc545279b7c2f071
--- /dev/null
+++ b/lib/menus/components/slider.js
@@ -0,0 +1,176 @@
+const { LitElement, html, css } = require('lit')
+
+class Slider extends LitElement {
+ static get styles () {
+ return css`
+ .slider-container {
+ --txrV: -46px;
+ position: relative;
+ width: 150px;
+ height: 20px;
+ font-family: minecraft, mojangles, monospace;
+ font-size: 10px;
+ color: white;
+ text-shadow: 1px 1px #220;
+ z-index: 1;
+ }
+
+ .slider-thumb {
+ --txrV: -66px;
+ pointer-events: none;
+ width: 8px;
+ height: 20px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 3;
+ }
+
+ .slider-container:hover .slider-thumb {
+ --txrV: -86px;
+ }
+
+ .slider-container::after,
+ .slider-thumb::after {
+ content: '';
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 50%;
+ height: 20px;
+ background: url('textures/1.17.1/gui/widgets.png');
+ background-size: 256px;
+ background-position-y: var(--txrV);
+ z-index: -1;
+ }
+
+ .slider-container::before,
+ .slider-thumb::before {
+ content: '';
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 50%;
+ width: 50%;
+ height: 20px;
+ background: url('textures/1.17.1/gui/widgets.png');
+ background-size: 256px;
+ background-position-x: calc(-200px + 100%);
+ background-position-y: var(--txrV);
+ z-index: -1;
+ }
+
+ .slider {
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ -webkit-appearance: none;
+ appearance: none;
+ background: none;
+ width: 100%;
+ height: 20px;
+ margin: 0;
+ }
+
+ .slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ position: relative;
+ appearance: none;
+ width: 8px;
+ height: 20px;
+ background: transparent;
+ }
+
+ .slider::-moz-range-thumb {
+ width: 8px;
+ height: 20px;
+ background: transparent;
+ }
+
+ label {
+ pointer-events: none;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 6;
+ width: max-content;
+ }
+ `
+ }
+
+ constructor () {
+ super()
+ this.label = ''
+ this.type = '%'
+ this.width = '150px'
+ this.value = '50'
+ this.min = '0'
+ this.max = '100'
+ this.ratio = (Number(this.value) - Number(this.min)) / (Number(this.max) - Number(this.min))
+ }
+
+ updated () {
+ this.ratio = (Number(this.value) - Number(this.min)) / (Number(this.max) - Number(this.min))
+ }
+
+ static get properties () {
+ return {
+ label: {
+ type: String,
+ attribute: 'pmui-label'
+ },
+ type: {
+ type: String,
+ attribute: 'pmui-type'
+ },
+ width: {
+ type: String,
+ attribute: 'pmui-width'
+ },
+ value: {
+ type: String,
+ attribute: 'pmui-value'
+ },
+ min: {
+ type: String,
+ attribute: 'pmui-min'
+ },
+ max: {
+ type: String,
+ attribute: 'pmui-max'
+ },
+ ratio: { type: Number }
+ }
+ }
+
+ render () {
+ return html`
+
+
{
+ const range = e.target
+ this.ratio = (range.value - range.min) / (range.max - range.min)
+ this.value = range.value
+ }}>
+
+
${this.label}: ${this.value}${this.type}
+
+ `
+ }
+}
+
+window.customElements.define('pmui-slider', Slider)
diff --git a/lib/menus/hud.js b/lib/menus/hud.js
new file mode 100644
index 0000000000000000000000000000000000000000..25b63cef882b8b8b9079bc17f6673a8c78f80a30
--- /dev/null
+++ b/lib/menus/hud.js
@@ -0,0 +1,370 @@
+const { LitElement, html, css } = require('lit')
+const { isMobile } = require('./components/common')
+
+class Hud extends LitElement {
+ static get styles () {
+ return css`
+ :host {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: -2;
+ width: 100%;
+ height: 100%;
+ }
+
+ .crosshair {
+ width: 16px;
+ height: 16px;
+ background: url('textures/1.17.1/gui/icons.png');
+ background-size: 256px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 2;
+ }
+
+ #xp-label {
+ position: absolute;
+ top: -8px;
+ left: 50%;
+ transform: translate(-50%);
+ font-size: 10px;
+ font-family: minecraft, mojangles, monospace;
+ color: rgb(30, 250, 30);
+ text-shadow: 0px -1px #000, 0px 1px #000, 1px 0px #000, -1px 0px #000;
+ z-index: 10;
+ }
+
+ #xp-bar-bg {
+ position: absolute;
+ left: 50%;
+ bottom: 24px;
+ transform: translate(-50%);
+ width: 182px;
+ height: 5px;
+ background-image: url('textures/1.16.4/gui/icons.png');
+ background-size: 256px;
+ background-position-y: -64px;
+ }
+
+ .xp-bar {
+ width: 182px;
+ height: 5px;
+ background-image: url('textures/1.17.1/gui/icons.png');
+ background-size: 256px;
+ background-position-y: -69px;
+ }
+
+ .mobile-top-btns {
+ display: none;
+ flex-direction: row;
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translate(-50%);
+ gap: 0 1px;
+ z-index: 20;
+ }
+
+ .pause-btn,
+ .chat-btn {
+ border: none;
+ outline: none;
+ width: 18px;
+ height: 18px;
+ background-image: url('extra-textures/gui.png');
+ background-size: 256px;
+ background-position-x: -200px;
+ background-position-y: -64px;
+ }
+
+ .chat-btn {
+ background-position-y: -82px;
+ }
+
+ .mobile-control-forward,
+ .mobile-control-back,
+ .mobile-control-left,
+ .mobile-control-sneak,
+ .mobile-control-jump,
+ .mobile-control-right {
+ position: absolute;
+ width: 22px;
+ height: 22px;
+ background-image: url('extra-textures/gui.png');
+ background-size: 256px;
+ border: none;
+ outline: none;
+ }
+
+ .mobile-control-forward:active,
+ .mobile-control-back:active,
+ .mobile-control-sneak:active,
+ .mobile-control-left:active,
+ .mobile-control-jump:active,
+ .mobile-control-right:active {
+ filter: brightness(85%);
+ }
+
+ .mobile-control-forward {
+ top: 0;
+ left: 50%;
+ transform: translate(-50%);
+ background-position: -2px -109px;
+ }
+
+ .mobile-control-back {
+ bottom: 0;
+ left: 50%;
+ transform: translate(-50%);
+ background-position: -54px -109px;
+ }
+
+ .mobile-control-left {
+ top: 50%;
+ left: 0;
+ transform: translate(0, -50%);
+ background-position: -28px -109px;
+ }
+
+ .mobile-control-right {
+ top: 50%;
+ right: 0;
+ transform: translate(0, -50%);
+ background-position: -80px -109px;
+ }
+
+ .mobile-control-jump {
+ position: relative;
+ width: 18px;
+ height: 18px;
+ background-position: -108px -111px;
+ }
+
+ .mobile-control-sneak {
+ width: 18px;
+ height: 18px;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-position: -218px -82px;
+ }
+ .mobile-control-sneak.is-down {
+ background-position: -218px -64px;
+ }
+
+ .mobile-controls-left {
+ display: none;
+ position: absolute;
+ left: 8px;
+ bottom: 0;
+ width: 70px;
+ height: 70px;
+ z-index: 30;
+ opacity: 0.65;
+ transform-origin: bottom left;
+ transform: scale(1.5);
+ }
+
+ .mobile-controls-right {
+ display: none;
+ flex-direction: column;
+ place-items: center;
+ place-content: center;
+ position: absolute;
+ right: 20px;
+ bottom: 0;
+ width: 22px;
+ height: 70px;
+ z-index: 30;
+ opacity: 0.65;
+ transform-origin: bottom right;
+ transform: scale(1.5);
+ }
+ `
+ }
+
+ static get properties () {
+ return {
+ bot: { type: Object }
+ }
+ }
+
+ /**
+ * @param {import('mineflayer').Bot} bot
+ */
+ preload (bot) {
+ const bossBars = this.shadowRoot.querySelector('#bossbars-overlay')
+ bossBars.bot = bot
+
+ bossBars.init()
+ }
+
+ /**
+ * @param {globalThis.THREE.Renderer} renderer
+ * @param {import('mineflayer').Bot} bot
+ * @param {string} host
+ */
+ init (renderer, bot, host) {
+ const debugMenu = this.shadowRoot.querySelector('#debug-overlay')
+ const playerList = this.shadowRoot.querySelector('#playerlist-overlay')
+ const healthbar = this.shadowRoot.querySelector('#health-bar')
+ const foodbar = this.shadowRoot.querySelector('#food-bar')
+ // const breathbar = this.shadowRoot.querySelector('#breath-bar')
+ const chat = this.shadowRoot.querySelector('#chat')
+ const hotbar = this.shadowRoot.querySelector('#hotbar')
+ const xpLabel = this.shadowRoot.querySelector('#xp-label')
+
+ this.bot = bot
+ hotbar.bot = bot
+ debugMenu.bot = bot
+
+ hotbar.init()
+ chat.init(bot._client, renderer)
+ bot.on('spawn', () => playerList.init(bot, host))
+
+ bot.on('entityHurt', (entity) => {
+ if (entity !== bot.entity) return
+ healthbar.onDamage()
+ })
+
+ bot.on('entityEffect', (entity, effect) => {
+ if (entity !== bot.entity) return
+ healthbar.effectAdded(effect)
+ })
+
+ bot.on('entityEffectEnd', (entity, effect) => {
+ if (entity !== bot.entity) return
+ healthbar.effectEnded(effect)
+ })
+
+ bot.on('game', () => {
+ healthbar.gameModeChanged(bot.player.gamemode, bot.game.hardcore)
+ foodbar.gameModeChanged(bot.player.gamemode)
+ // breathbar.gameModeChanged(bot.player.gamemode)
+ this.shadowRoot.querySelector('#xp-bar-bg').style.display = bot.player.gamemode === 1 ? 'none' : 'block'
+ })
+
+ bot.on('health', () => {
+ healthbar.updateHealth(bot.health, true)
+ foodbar.updateHunger(bot.food, true)
+ })
+
+ bot.on('experience', () => {
+ this.shadowRoot.querySelector('#xp-bar-bg').firstElementChild.style.width = `${182 * bot.experience.progress}px`
+ xpLabel.innerHTML = bot.experience.level
+ xpLabel.style.display = bot.experience.level > 0 ? 'block' : 'none'
+ })
+
+ // bot.on('breath', () => {
+ // breathbar.updateOxygen(bot.oxygenLevel)
+ // })
+
+ bot.on('spawn', () => {
+ this.shadowRoot.querySelector('#xp-bar-bg').style.display = bot.player.gamemode === 1 ? 'none' : 'block'
+ this.shadowRoot.querySelector('#xp-bar-bg').firstElementChild.style.width = `${182 * bot.experience.progress}px`
+ xpLabel.innerHTML = bot.experience.level
+ xpLabel.style.display = bot.experience.level > 0 ? 'block' : 'none'
+ healthbar.gameModeChanged(bot.player.gamemode, bot.game.hardcore)
+ healthbar.updateHealth(bot.health)
+ foodbar.updateHunger(bot.food)
+ // breathbar.updateOxygen(bot.oxygenLevel ?? 20)
+ })
+
+ if (document.getElementById('options-screen').forceMobileControls || isMobile()) {
+ this.showMobileControls(true)
+ } else {
+ this.showMobileControls(false)
+ }
+ }
+
+ /** @param {boolean} bl */
+ showMobileControls (bl) {
+ this.shadowRoot.querySelector('#mobile-top').style.display = bl ? 'flex' : 'none'
+ this.shadowRoot.querySelector('#mobile-left').style.display = bl ? 'block' : 'none'
+ this.shadowRoot.querySelector('#mobile-right').style.display = bl ? 'flex' : 'none'
+ }
+
+ /**
+ * @param {string} id
+ * @param {boolean} action
+ */
+ mobileControl (e, id, action) {
+ e.stopPropagation()
+ this.bot.setControlState(id, action)
+ }
+
+ render () {
+ return html`
+
+ {
+ e.stopPropagation()
+ this.shadowRoot.querySelector('#chat').enableChat(false)
+ }}>
+ {
+ e.stopPropagation()
+ document.getElementById('pause-screen').enableGameMenu()
+ }}>
+
+
+ this.mobileControl(e, 'forward', true)}
+ @touchend=${(e) => this.mobileControl(e, 'forward', false)}
+ @mousedown=${(e) => this.mobileControl(e, 'forward', true)}
+ @mouseup=${(e) => this.mobileControl(e, 'forward', false)}
+ >
+ this.mobileControl(e, 'back', true)}
+ @touchend=${(e) => this.mobileControl(e, 'back', false)}
+ @mousedown=${(e) => this.mobileControl(e, 'back', true)}
+ @mouseup=${(e) => this.mobileControl(e, 'back', false)}
+ >
+ this.mobileControl(e, 'right', true)}
+ @touchend=${(e) => this.mobileControl(e, 'right', false)}
+ @mousedown=${(e) => this.mobileControl(e, 'right', true)}
+ @mouseup=${(e) => this.mobileControl(e, 'right', false)}
+ >
+ this.mobileControl(e, 'left', true)}
+ @touchend=${(e) => this.mobileControl(e, 'left', false)}
+ @mousedown=${(e) => this.mobileControl(e, 'left', true)}
+ @mouseup=${(e) => this.mobileControl(e, 'left', false)}
+ >
+ {
+ e.stopPropagation()
+ const b = e.target.classList.toggle('is-down')
+ this.bot.setControlState('sneak', b)
+ }}>
+
+
+ this.mobileControl(e, 'jump', true)}
+ @touchend=${(e) => this.mobileControl(e, 'jump', false)}
+ @mousedown=${(e) => this.mobileControl(e, 'jump', true)}
+ @mouseup=${(e) => this.mobileControl(e, 'jump', false)}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+ }
+}
+
+window.customElements.define('pmui-hud', Hud)
diff --git a/lib/menus/keybinds_screen.js b/lib/menus/keybinds_screen.js
new file mode 100644
index 0000000000000000000000000000000000000000..7598c2122786fef6e87c3b564c0529b31052483d
--- /dev/null
+++ b/lib/menus/keybinds_screen.js
@@ -0,0 +1,172 @@
+const { LitElement, html, css } = require('lit')
+const { commonCss, displayScreen } = require('./components/common')
+
+class KeyBindsScreen extends LitElement {
+ static get styles () {
+ return css`
+ ${commonCss}
+ .title {
+ top: 4px;
+ }
+
+ main {
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ top: 30px;
+ left: 50%;
+ transform: translate(-50%);
+ width: 100%;
+ height: calc(100% - 64px);
+ place-items: center;
+ background: rgba(0, 0, 0, 0.5);
+ box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.7), inset 0 -3px 6px rgba(0, 0, 0, 0.7);
+ }
+
+ .keymap-list {
+ width: 288px;
+ display: flex;
+ flex-direction: column;
+ padding: 4px 0;
+ overflow-y: auto;
+ }
+
+ .keymap-list::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ .keymap-list::-webkit-scrollbar-track {
+ background: #000;
+ }
+
+ .keymap-list::-webkit-scrollbar-thumb {
+ background: #ccc;
+ box-shadow: inset -1px -1px 0 #4f4f4f;
+ }
+
+ .keymap-entry {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ height: 20px;
+ place-content: center;
+ place-items: center;
+ justify-content: space-between;
+ }
+
+ span {
+ color: white;
+ text-shadow: 1px 1px 0 rgb(63, 63, 63);
+ font-size: 10px;
+ }
+
+ .keymap-entry-btns {
+ display: flex;
+ flex-direction: row;
+ gap: 4px;
+ }
+
+ .bottom-btns {
+ display: flex;
+ flex-direction: row;
+ width: 310px;
+ height: 20px;
+ justify-content: space-between;
+ position: absolute;
+ bottom: 9px;
+ left: 50%;
+ transform: translate(-50%);
+ }
+
+ `
+ }
+
+ static get properties () {
+ return {
+ keymaps: { type: Object },
+ selected: { type: Number }
+ }
+ }
+
+ constructor () {
+ super()
+ this.selected = -1
+ this.keymaps = [
+ { defaultKey: 'KeyW', key: 'KeyW', name: 'Walk Forwards' },
+ { defaultKey: 'KeyS', key: 'KeyS', name: 'Walk Backwards' },
+ { defaultKey: 'KeyA', key: 'KeyA', name: 'Strafe Left' },
+ { defaultKey: 'KeyD', key: 'KeyD', name: 'Strafe Right' },
+ { defaultKey: 'Space', key: 'Space', name: 'Jump' },
+ { defaultKey: 'ShiftLeft', key: 'ShiftLeft', name: 'Sneak' },
+ { defaultKey: 'ControlLeft', key: 'ControlLeft', name: 'Sprint' },
+ { defaultKey: 'KeyT', key: 'KeyT', name: 'Open Chat' },
+ { defaultKey: 'Slash', key: 'Slash', name: 'Open Command' },
+ // { defaultKey: '0', key: '0', name: 'Attack/Destroy' },
+ // { defaultKey: '1', key: '1', name: 'Place Block' },
+ { defaultKey: 'KeyQ', key: 'KeyQ', name: 'Drop Item' }
+ // { defaultKey: 'Digit1', key: 'Digit1', name: 'Hotbar Slot 1' },
+ // { defaultKey: 'Digit2', key: 'Digit2', name: 'Hotbar Slot 2' },
+ // { defaultKey: 'Digit3', key: 'Digit3', name: 'Hotbar Slot 3' },
+ // { defaultKey: 'Digit4', key: 'Digit4', name: 'Hotbar Slot 4' },
+ // { defaultKey: 'Digit5', key: 'Digit5', name: 'Hotbar Slot 5' },
+ // { defaultKey: 'Digit6', key: 'Digit6', name: 'Hotbar Slot 6' },
+ // { defaultKey: 'Digit7', key: 'Digit7', name: 'Hotbar Slot 7' },
+ // { defaultKey: 'Digit8', key: 'Digit8', name: 'Hotbar Slot 8' },
+ // { defaultKey: 'Digit9', key: 'Digit9', name: 'Hotbar Slot 9' },
+ // { defaultKey: 'KeyE', key: 'KeyE', name: 'Open Inventory' }
+ ]
+
+ document.addEventListener('keydown', (e) => {
+ if (this.selected !== -1) {
+ this.keymaps[this.selected].key = e.code
+ this.selected = -1
+ this.requestUpdate()
+ }
+ })
+ }
+
+ render () {
+ return html`
+
+
+ Key Binds
+
+
+
+ ${this.keymaps.map((m, i) => html`
+
+
${m.name}
+
+
+
{
+ e.target.setAttribute('pmui-label', `> ${m.key} <`)
+ this.selected = i
+ this.requestUpdate()
+ }}>
+
{
+ this.keymaps[i].key = this.keymaps[i].defaultKey
+ this.requestUpdate()
+ this.selected = -1
+ }}>
+
+
+ `)}
+
+
+
+
+
v.key !== v.defaultKey)} @pmui-click=${this.onResetAllPress}>
+
displayScreen(this, document.getElementById('options-screen'))}>
+
+ `
+ }
+
+ onResetAllPress () {
+ for (let i = 0; i < this.keymaps.length; i++) {
+ this.keymaps[i].key = this.keymaps[i].defaultKey
+ }
+ this.requestUpdate()
+ }
+}
+
+window.customElements.define('pmui-keybindsscreen', KeyBindsScreen)
diff --git a/lib/menus/loading_screen.js b/lib/menus/loading_screen.js
new file mode 100644
index 0000000000000000000000000000000000000000..3066022f041135dbad2ed4a2f9a5ef47e8890c23
--- /dev/null
+++ b/lib/menus/loading_screen.js
@@ -0,0 +1,67 @@
+const { LitElement, html, css } = require('lit')
+const { commonCss } = require('./components/common')
+
+class LoadingScreen extends LitElement {
+ static get styles () {
+ return css`
+ ${commonCss}
+ .title {
+ top: 30px;
+ }
+
+ #cancel-btn {
+ position: absolute;
+ top: calc(20% + 50px);
+ left: 50%;
+ transform: translate(-50%);
+ }
+ `
+ }
+
+ static get properties () {
+ return {
+ status: { type: String },
+ loadingText: { type: String },
+ hasError: { type: Number }
+ }
+ }
+
+ constructor () {
+ super()
+ this.hasError = false
+ this.status = 'Waiting for JS load'
+ }
+
+ firstUpdated () {
+ this.statusRunner()
+ }
+
+ async statusRunner () {
+ const array = ['.', '..', '...', '']
+ const timer = ms => new Promise((resolve) => setTimeout(resolve, ms))
+
+ const load = async () => {
+ for (let i = 0; true; i = ((i + 1) % array.length)) {
+ this.loadingText = this.status + array[i]
+ await timer(500)
+ }
+ }
+
+ load()
+ }
+
+ render () {
+ return html`
+
+
+ ${this.hasError ? this.status : this.loadingText}
+
+ ${this.hasError
+ ? html` window.location.reload()}> `
+ : ''
+ }
+ `
+ }
+}
+
+window.customElements.define('pmui-loadingscreen', LoadingScreen)
diff --git a/lib/menus/options_screen.js b/lib/menus/options_screen.js
new file mode 100644
index 0000000000000000000000000000000000000000..a05ae55f2aa819770d3020c4a79dc30e771ae769
--- /dev/null
+++ b/lib/menus/options_screen.js
@@ -0,0 +1,158 @@
+const { LitElement, html, css } = require('lit')
+const { commonCss, displayScreen, isMobile } = require('./components/common')
+
+class OptionsScreen extends LitElement {
+ static get styles () {
+ return css`
+ ${commonCss}
+ .title {
+ top: 4px;
+ }
+
+ main {
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ top: calc(100% / 6 - 6px);
+ left: 50%;
+ width: 310px;
+ gap: 4px 0;
+ place-items: center;
+ place-content: center;
+ transform: translate(-50%);
+ }
+
+ .wrapper {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ gap: 0 10px;
+ height: 20px;
+ }
+ `
+ }
+
+ static get properties () {
+ return {
+ isInsideWorld: { type: Boolean },
+ mouseSensitivityX: { type: Number },
+ mouseSensitivityY: { type: Number },
+ chatWidth: { type: Number },
+ chatHeight: { type: Number },
+ chatScale: { type: Number },
+ sound: { type: Number },
+ renderDistance: { type: Number },
+ fov: { type: Number },
+ guiScale: { type: Number }
+ }
+ }
+
+ constructor () {
+ super()
+ this.isInsideWorld = false
+
+ const getValue = (item, defaultValue, convertFn) => window.localStorage.getItem(item) ? convertFn(window.localStorage.getItem(item)) : defaultValue
+
+ this.mouseSensitivityX = getValue('mouseSensX', 50, (v) => Math.floor(Number(v) * 10000))
+ this.mouseSensitivityY = getValue('mouseSensY', 50, (v) => Math.floor(Number(v) * 10000))
+ this.chatWidth = getValue('chatWidth', 320, (v) => Number(v))
+ this.chatHeight = getValue('chatHeight', 180, (v) => Number(v))
+ this.chatScale = getValue('chatScale', 100, (v) => Number(v))
+ this.sound = getValue('sound', 50, (v) => Number(v))
+ this.renderDistance = getValue('renderDistance', 6, (v) => Number(v))
+ this.fov = getValue('fov', 75, (v) => Number(v))
+ this.guiScale = getValue('guiScale', 3, (v) => Number(v))
+ this.forceMobileControls = getValue('forceMobileControls', false, (v) => v === 'true')
+
+ document.documentElement.style.setProperty('--chatScale', `${this.chatScale / 100}`)
+ document.documentElement.style.setProperty('--chatWidth', `${this.chatWidth}px`)
+ document.documentElement.style.setProperty('--chatHeight', `${this.chatHeight}px`)
+ document.documentElement.style.setProperty('--guiScale', `${this.guiScale}`)
+ }
+
+ render () {
+ return html`
+
+
+ Options
+
+
+
+
{
+ this.mouseSensitivityX = Number(e.target.value)
+ window.localStorage.setItem('mouseSensX', this.mouseSensitivityX * 0.0001)
+ }}>
+
{
+ this.mouseSensitivityY = Number(e.target.value)
+ window.localStorage.setItem('mouseSensY', this.mouseSensitivityY * 0.0001)
+ }}>
+
+
+
{
+ this.chatWidth = Number(e.target.value)
+ window.localStorage.setItem('chatWidth', `${this.chatWidth}`)
+ document.documentElement.style.setProperty('--chatWidth', `${this.chatWidth}px`)
+ }}>
+
{
+ this.chatHeight = Number(e.target.value)
+ window.localStorage.setItem('chatHeight', `${this.chatHeight}`)
+ document.documentElement.style.setProperty('--chatHeight', `${this.chatHeight}px`)
+ }}>
+
+
+
{
+ this.chatScale = Number(e.target.value)
+ window.localStorage.setItem('chatScale', `${this.chatScale}`)
+ document.documentElement.style.setProperty('--chatScale', `${this.chatScale / 100}`)
+ }}>
+
{
+ this.sound = Number(e.target.value)
+ window.localStorage.setItem('sound', `${this.sound}`)
+ }}>
+
+
+
displayScreen(this, document.getElementById('keybinds-screen'))}>
+
{
+ this.guiScale = Number(e.target.value)
+ window.localStorage.setItem('guiScale', `${this.guiScale}`)
+ document.documentElement.style.setProperty('--guiScale', `${this.guiScale}`)
+ }}>
+
+ ${this.isInsideWorld
+? ''
+: html`
+
+
{
+ this.renderDistance = Number(e.target.value)
+ window.localStorage.setItem('renderDistance', `${this.renderDistance}`)
+ }}>
+
{
+ this.fov = Number(e.target.value)
+ window.localStorage.setItem('fov', `${this.fov}`)
+
+ this.dispatchEvent(new window.CustomEvent('fov_changed', { fov: this.fov }))
+ }}>
+
+ `}
+
+
+
{
+ this.forceMobileControls = !this.forceMobileControls
+ window.localStorage.setItem('forceMobileControls', `${this.forceMobileControls}`)
+ if (this.forceMobileControls || isMobile()) {
+ document.getElementById('hud').showMobileControls(true)
+ } else {
+ document.getElementById('hud').showMobileControls(false)
+ }
+ this.requestUpdate()
+ }
+ }>
+
+
+ displayScreen(this, document.getElementById(this.isInsideWorld ? 'pause-screen' : 'title-screen'))}>
+
+ `
+ }
+}
+
+window.customElements.define('pmui-optionsscreen', OptionsScreen)
diff --git a/lib/menus/pause_screen.js b/lib/menus/pause_screen.js
new file mode 100644
index 0000000000000000000000000000000000000000..d38567624c43d81cbf515da72db5c2d4ef2070a6
--- /dev/null
+++ b/lib/menus/pause_screen.js
@@ -0,0 +1,110 @@
+const { LitElement, html, css } = require('lit')
+const { openURL, displayScreen } = require('./components/common')
+
+class PauseScreen extends LitElement {
+ static get styles () {
+ return css`
+ .bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: rgba(0, 0, 0, 0.75);
+ width: 100%;
+ height: 100%;
+ }
+
+ .title {
+ position: absolute;
+ top: 40px;
+ left: 50%;
+ transform: translate(-50%);
+ font-size: 10px;
+ color: white;
+ text-shadow: 1px 1px #222;
+ }
+
+ main {
+ display: flex;
+ flex-direction: column;
+ gap: 4px 0;
+ position: absolute;
+ left: 50%;
+ width: 204px;
+ top: calc(25% + 48px - 16px);
+ transform: translate(-50%);
+ }
+
+ .row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ width: 100%;
+ }
+ `
+ }
+
+ constructor () {
+ super()
+ this.inMenu = false
+ }
+
+ init (renderer) {
+ const chat = document.getElementById('hud').shadowRoot.querySelector('#chat')
+ const self = this
+
+ document.addEventListener('keydown', e => {
+ if (chat.inChat) return
+ e = e || window.event
+ if (e.keyCode === 27 || e.key === 'Escape' || e.key === 'Esc') {
+ if (self.inMenu) {
+ self.disableGameMenu(renderer)
+ } else {
+ self.enableGameMenu()
+ }
+ }
+ })
+ }
+
+ render () {
+ return html`
+
+
+ Game Menu
+
+
+
+
+
openURL('https://github.com/PrismarineJS/prismarine-web-client')}>
+
openURL('https://discord.gg/4Ucm684Fq3')}>
+
+ displayScreen(this, document.getElementById('options-screen'))}>
+ window.location.reload()}>
+
+ `
+ }
+
+ disableGameMenu (renderer = false) {
+ this.inMenu = false
+ this.style.display = 'none'
+ if (renderer) {
+ renderer.domElement.requestPointerLock()
+ }
+ }
+
+ enableGameMenu () {
+ this.inMenu = true
+ document.exitPointerLock()
+ this.style.display = 'block'
+ this.focus()
+ }
+
+ onReturnPress (renderer = false) {
+ this.inMenu = false
+ this.style.display = 'none'
+ if (renderer) {
+ renderer.domElement.requestPointerLock()
+ }
+ }
+}
+
+window.customElements.define('pmui-pausescreen', PauseScreen)
diff --git a/lib/menus/play_screen.js b/lib/menus/play_screen.js
new file mode 100644
index 0000000000000000000000000000000000000000..57bd22efc251e1c1d51e59a63a309ec3aec58711
--- /dev/null
+++ b/lib/menus/play_screen.js
@@ -0,0 +1,163 @@
+const { LitElement, html, css } = require('lit')
+const { commonCss, displayScreen } = require('./components/common')
+
+class PlayScreen extends LitElement {
+ static get styles () {
+ return css`
+ ${commonCss}
+ .title {
+ top: 12px;
+ }
+
+ .edit-boxes {
+ position: absolute;
+ top: 59px;
+ left: 50%;
+ display: flex;
+ flex-direction: column;
+ gap: 14px 0;
+ transform: translate(-50%);
+ width: 310px;
+ }
+
+ .wrapper {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ gap: 0 4px;
+ }
+
+ .button-wrapper {
+ display: flex;
+ flex-direction: row;
+ gap: 0 4px;
+ position: absolute;
+ bottom: 9px;
+ left: 50%;
+ transform: translate(-50%);
+ width: 310px;
+ }
+
+ .extra-info-bv {
+ font-size: 10px;
+ color: rgb(206, 206, 206);
+ text-shadow: 1px 1px black;
+ position: absolute;
+ left: calc(50% + 2px);
+ bottom: -34px;
+ }
+ `
+ }
+
+ static get properties () {
+ return {
+ server: { type: String },
+ serverport: { type: Number },
+ proxy: { type: String },
+ proxyport: { type: Number },
+ username: { type: String },
+ password: { type: String },
+ version: { type: String }
+ }
+ }
+
+ constructor () {
+ super()
+ this.username = window.localStorage.getItem('username') ?? 'pviewer' + (Math.floor(Math.random() * 1000))
+ this.password = window.localStorage.getItem('password') ?? ''
+ window.fetch('config.json').then(res => res.json()).then(config => {
+ this.server = window.localStorage.getItem('server') ?? config.defaultHost
+ this.serverport = window.localStorage.getItem('serverport') ?? config.defaultHostPort ?? 25565
+ this.proxy = window.localStorage.getItem('proxy') ?? config.defaultProxy
+ this.proxyport = window.localStorage.getItem('proxyport') ?? (!config.defaultProxy && !config.defaultProxyPort ? '' : config.defaultProxyPort ?? 443)
+ this.version = window.localStorage.getItem('version') ?? config.defaultVersion
+ })
+ }
+
+ render () {
+ return html`
+
+
+ Join a Server
+
+
+
+
{ this.server = e.target.value }}
+ >
+
{ this.serverport = e.target.value }}
+ >
+
+
+
{ this.proxy = e.target.value }}
+ >
+
{ this.proxyport = e.target.value }}
+ >
+
+
+
{ this.username = e.target.value }}
+ >
+
{ this.version = e.target.value }}
+ >
+
+
+
+
+
+ `
+ }
+
+ onConnectPress () {
+ window.localStorage.setItem('username', this.username)
+ window.localStorage.setItem('password', this.password)
+ window.localStorage.setItem('server', this.server)
+ window.localStorage.setItem('serverport', this.serverport)
+ window.localStorage.setItem('proxy', this.proxy)
+ window.localStorage.setItem('proxyport', this.proxyport)
+ window.localStorage.setItem('version', this.version)
+
+ this.dispatchEvent(new window.CustomEvent('connect', {
+ detail: {
+ server: `${this.server}:${this.serverport}`,
+ proxy: `${this.proxy}${this.proxy !== '' ? `:${this.proxyport}` : ''}`,
+ username: this.username,
+ password: this.password,
+ botVersion: this.version
+ }
+ }))
+ }
+}
+
+window.customElements.define('pmui-playscreen', PlayScreen)
diff --git a/lib/menus/title_screen.js b/lib/menus/title_screen.js
new file mode 100644
index 0000000000000000000000000000000000000000..820910b8a2e73507db1ee7a3aaa557571d2677ec
--- /dev/null
+++ b/lib/menus/title_screen.js
@@ -0,0 +1,124 @@
+const { openURL, displayScreen } = require('./components/common')
+const { LitElement, html, css } = require('lit')
+
+class TitleScreen extends LitElement {
+ static get styles () {
+ return css`
+ .minecraft {
+ position: absolute;
+ top: 30px;
+ left: calc(50% - 137px);
+ }
+
+ .minecraft .minec {
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ background-image: url('textures/1.17.1/gui/title/minecraft.png');
+ background-size: 256px;
+ width: 155px;
+ height: 44px;
+ }
+
+ .minecraft .raft {
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 155px;
+ background-image: url('textures/1.17.1/gui/title/minecraft.png');
+ background-size: 256px;
+ width: 155px;
+ height: 44px;
+ background-position-y: -45px;
+ }
+
+ .minecraft .edition {
+ display: block;
+ position: absolute;
+ top: 37px;
+ left: calc(88px + 5px);
+ background-image: url('extra-textures/edition.png');
+ background-size: 128px;
+ width: 88px;
+ height: 14px;
+ }
+
+ .splash {
+ position: absolute;
+ top: 32px;
+ left: 227px;
+ color: #ff0;
+ transform: translate(-50%, -50%) rotateZ(-20deg) scale(1);
+ width: max-content;
+ text-shadow: 1px 1px #220;
+ font-size: 10px;
+ animation: splashAnim 400ms infinite alternate linear;
+ }
+
+ @keyframes splashAnim {
+ to {
+ transform: translate(-50%, -50%) rotateZ(-20deg) scale(1.07);
+ }
+ }
+
+ .menu {
+ display: flex;
+ flex-direction: column;
+ gap: 4px 0;
+ position: absolute;
+ top: calc(25% + 48px);
+ left: 50%;
+ width: 200px;
+ transform: translate(-50%);
+ }
+
+ .menu-row {
+ display: flex;
+ flex-direction: row;
+ gap: 0 4px;
+ width: 100%;
+ }
+
+ .bottom-info {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ position: absolute;
+ bottom: -1px;
+ left: 1px;
+ width: calc(100% - 2px);
+ color: white;
+ text-shadow: 1px 1px #222;
+ font-size: 10px;
+ }
+ `
+ }
+
+ render () {
+ return html`
+
+
+
+
+
Prismarine is a beautiful block
+
+
+
+
+
+ Prismarine Web Client
+ A Minecraft client in the browser!
+
+ `
+ }
+}
+
+window.customElements.define('pmui-titlescreen', TitleScreen)
diff --git a/lib/perf_hooks_replacement.js b/lib/perf_hooks_replacement.js
new file mode 100644
index 0000000000000000000000000000000000000000..8539602ab41a1727f9450eff3fc244c29ee83acb
--- /dev/null
+++ b/lib/perf_hooks_replacement.js
@@ -0,0 +1 @@
+module.exports.performance = window.performance
diff --git a/lib/vr.js b/lib/vr.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ada13acd8db30f768d116d64e14fd4fc3aff342
--- /dev/null
+++ b/lib/vr.js
@@ -0,0 +1,84 @@
+/* global THREE */
+
+const { VRButton } = require('three/examples/jsm/webxr/VRButton.js')
+const { GLTFLoader } = require('three/examples/jsm/loaders/GLTFLoader.js')
+const { XRControllerModelFactory } = require('three/examples/jsm/webxr/XRControllerModelFactory.js')
+const TWEEN = require('@tweenjs/tween.js')
+
+function initVR (bot, renderer, viewer) {
+ if (!('xr' in navigator)) return
+ if (!navigator.userAgent.includes('OculusBrowser')) return
+
+ // VR
+ document.body.appendChild(VRButton.createButton(renderer))
+ renderer.xr.enabled = true
+
+ // hack for vr camera
+ const user = new THREE.Group()
+ user.add(viewer.camera)
+ viewer.scene.add(user)
+ const controllerModelFactory = new XRControllerModelFactory(new GLTFLoader())
+ const controller1 = renderer.xr.getControllerGrip(0)
+ const controller2 = renderer.xr.getControllerGrip(1)
+ let hand1 = controllerModelFactory.createControllerModel(controller1)
+ controller1.addEventListener('connected', (event) => {
+ hand1.xrInputSource = event.data
+ user.add(controller1)
+ })
+ controller1.add(hand1)
+ let hand2 = controllerModelFactory.createControllerModel(controller2)
+ controller2.addEventListener('connected', (event) => {
+ hand2.xrInputSource = event.data
+ user.add(controller2)
+ })
+ controller2.add(hand2)
+
+ viewer.setFirstPersonCamera = function (pos, yaw, pitch) {
+ if (pos) new TWEEN.Tween(user.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start()
+ user.rotation.set(pitch, yaw, 0, 'ZYX')
+ }
+
+ let rotSnapReset = true
+ let yawOffset = 0
+ renderer.setAnimationLoop(() => {
+ if (hand1.xrInputSource && hand2.xrInputSource) {
+ hand1.xAxis = hand1.xrInputSource.gamepad.axes[2]
+ hand1.yAxis = hand1.xrInputSource.gamepad.axes[3]
+ hand2.xAxis = hand2.xrInputSource.gamepad.axes[2]
+ hand2.yAxis = hand2.xrInputSource.gamepad.axes[3]
+ if (hand1.xrInputSource.handedness === 'right') {
+ const tmp = hand2
+ hand2 = hand1
+ hand1 = tmp
+ }
+ }
+
+ if (rotSnapReset) {
+ if (Math.abs(hand1.xAxis) > 0.8) {
+ yawOffset -= Math.PI / 4 * Math.sign(hand1.xAxis)
+ rotSnapReset = false
+ }
+ } else if (Math.abs(hand1.xAxis) < 0.1) {
+ rotSnapReset = true
+ }
+
+ viewer.setFirstPersonCamera(null, yawOffset, 0)
+
+ const xrCamera = renderer.xr.getCamera(viewer.camera)
+ const d = xrCamera.getWorldDirection()
+ bot.entity.yaw = Math.atan2(-d.x, -d.z)
+ bot.entity.pitch = Math.asin(d.y)
+
+ bot.physics.stepHeight = 1
+ bot.setControlState('forward', hand2.yAxis < -0.5)
+ bot.setControlState('back', hand2.yAxis > 0.5)
+ bot.setControlState('right', hand2.xAxis < -0.5)
+ bot.setControlState('left', hand2.xAxis > 0.5)
+
+ TWEEN.update()
+ viewer.update()
+ renderer.render(viewer.scene, viewer.camera)
+ })
+}
+
+module.exports = { initVR }
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..027c561eb343cb634b1fdf36d228227ddc7c032e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,79 @@
+{
+ "name": "prismarine-web-client",
+ "version": "1.6.0",
+ "description": "A minecraft client running in a browser",
+ "main": "index.js",
+ "scripts": {
+ "postinstall": "node scripts/patchPackages.js",
+ "build": "webpack --config webpack.prod.js",
+ "build-dev": "webpack --config webpack.dev.js",
+ "start": "node --max-old-space-size=8192 server.js 8080 dev",
+ "prod-start": "node server.js",
+ "build-dev-start": "npm run build-dev && npm run prod-start",
+ "build-start": "npm run build && npm run prod-start",
+ "prepublishOnly": "npm run build",
+ "lint": "standard",
+ "fix": "standard --fix",
+ "test": "npm run lint && mocha"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/PrismarineJS/prismarine-web-client.git"
+ },
+ "keywords": [
+ "prismarine",
+ "web",
+ "client"
+ ],
+ "author": "PrismarineJS",
+ "license": "MIT",
+ "bin": {
+ "prismarine-web-client": "./server.js"
+ },
+ "bugs": {
+ "url": "https://github.com/PrismarineJS/prismarine-web-client/issues"
+ },
+ "homepage": "https://github.com/PrismarineJS/prismarine-web-client#readme",
+ "dependencies": {
+ "compression": "^1.7.4",
+ "express": "^4.17.1",
+ "ismobilejs": "^1.1.1",
+ "lit": "^2.0.2",
+ "net-browserify": "PrismarineJS/net-browserify",
+ "querystring": "^0.2.1",
+ "url": "^0.11.0"
+ },
+ "devDependencies": {
+ "assert": "^2.0.0",
+ "browserify-zlib": "^0.2.0",
+ "buffer": "^6.0.3",
+ "clean-webpack-plugin": "^4.0.0",
+ "constants-browserify": "^1.0.0",
+ "copy-webpack-plugin": "^11.0.0",
+ "crypto-browserify": "^3.12.0",
+ "events": "^3.2.0",
+ "html-webpack-plugin": "^5.3.1",
+ "http-browserify": "^1.7.0",
+ "http-server": "^14.1.0",
+ "https-browserify": "^1.0.0",
+ "lodash-webpack-plugin": "^0.11.6",
+ "memfs": "^3.2.0",
+ "mineflayer": "^4.17.0",
+ "mineflayer-pathfinder": "^2.0.0",
+ "mocha": "^10.0.0",
+ "os-browserify": "^0.3.0",
+ "path-browserify": "^1.0.1",
+ "prismarine-viewer": "^1.28.0",
+ "process": "PrismarineJS/node-process",
+ "standard": "^17.0.0",
+ "stream-browserify": "^3.0.0",
+ "three": "0.127.0",
+ "timers-browserify": "^2.0.12",
+ "webpack": "^5.11.0",
+ "webpack-cli": "^5.0.0",
+ "webpack-dev-middleware": "^6.0.0",
+ "webpack-dev-server": "^4.0.0",
+ "webpack-merge": "^5.7.3",
+ "workbox-webpack-plugin": "^6.1.2"
+ }
+}
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..9f661d51190ce16105438e9aa0980fe3952157f2
--- /dev/null
+++ b/screenshot.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:05abe3a1da01114bc46c0a93f0f1f8ad7f99904ab21ea4f1bff5fcc7a335afce
+size 2710634
diff --git a/scripts/patchPackages.js b/scripts/patchPackages.js
new file mode 100644
index 0000000000000000000000000000000000000000..f6dd391d451b4ced1ec85058eda1865a2a1916e5
--- /dev/null
+++ b/scripts/patchPackages.js
@@ -0,0 +1,25 @@
+// @ts-check
+const path = require('path')
+const dataPath = path.join(require.resolve('minecraft-data'), '../data.js')
+
+const fs = require('fs')
+
+const lines = fs.readFileSync(dataPath, 'utf8').split('\n')
+if (lines[0] === '//patched') {
+ console.log('Already patched')
+ process.exit(0)
+}
+
+function removeLinesBetween (start, end) {
+ const startIndex = lines.findIndex(line => line === start)
+ if (startIndex === -1) return
+ const endIndex = startIndex + lines.slice(startIndex).findIndex(line => line === end)
+ // insert block comments
+ lines.splice(startIndex, 0, '/*')
+ lines.splice(endIndex + 2, 0, '*/')
+}
+
+removeLinesBetween(" 'bedrock': {", ' }')
+
+lines.unshift('//patched')
+fs.writeFileSync(dataPath, lines.join('\n'), 'utf8')
diff --git a/server.js b/server.js
new file mode 100644
index 0000000000000000000000000000000000000000..f78b9fec9541c32e5a0328f53d4aa96099006018
--- /dev/null
+++ b/server.js
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+
+const express = require('express')
+const netApi = require('net-browserify')
+const compression = require('compression')
+const path = require('path')
+
+// Create our app
+const app = express()
+
+app.get('/config.json', (_, res) => res.sendFile(path.join(__dirname, 'config.json')))
+
+app.use(compression())
+app.use(netApi({ allowOrigin: '*' }))
+if (process.argv[3] === 'dev') {
+ // https://webpack.js.org/guides/development/#using-webpack-dev-middleware
+ const webpackDevMiddleware = require('webpack-dev-middleware')
+ const config = require('./webpack.dev.js')
+ const webpack = require('webpack')
+ const compiler = webpack(config)
+
+ app.use(
+ webpackDevMiddleware(compiler, {
+ publicPath: config.output.publicPath
+ })
+ )
+} else {
+ app.use(express.static(path.join(__dirname, './public')))
+}
+
+// Start the server
+const server = app.listen(process.argv[2] === undefined ? 8080 : process.argv[2], function () {
+ console.log('Server listening on port ' + server.address().port)
+})
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000000000000000000000000000000000000..b9bb8431772a4d8a283c92b27b5b0fe4009061bd
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,120 @@
+:root {
+ --guiScaleFactor: 3;
+ --guiScale: 3;
+ --chatWidth: 320px;
+ --chatHeight: 180px;
+ --chatScale: 1;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html {
+ height: 100vh;
+ overflow: hidden;
+}
+
+.dirt-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: url('textures/1.17.1/gui/options_background.png'), rgba(0, 0, 0, 0.7);
+ background-size: 16px;
+ background-repeat: repeat;
+ width: 100%;
+ height: 100%;
+ transform-origin: top left;
+ transform: scale(2);
+ background-blend-mode: overlay;
+}
+
+@font-face {
+ font-family: minecraft;
+ src: url(minecraftia.woff);
+}
+
+@font-face {
+ font-family: mojangles;
+ src: url(mojangles.ttf);
+}
+
+body {
+ overflow: hidden;
+ position: relative;
+ margin:0;
+ padding:0;
+ height: 100vh;
+ font-family: sans-serif;
+ background: #333;
+ /* background: linear-gradient(#141e30, #243b55); */
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ font-size: 0;
+ margin: 0;
+ padding: 0;
+}
+
+#ui-root {
+ position: absolute;
+ top: 0;
+ left: 0;
+ transform-origin: top left;
+ transform: scale(var(--guiScale));
+ width: calc(100% / var(--guiScale));
+ height: calc(100% / var(--guiScale));
+ z-index: 10;
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: -o-crisp-edges;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ font-family: minecraft, mojangles, monospace;
+}
+
+@media only screen and (max-width: 971px) {
+ #ui-root {
+ transform: scale(2);
+ width: calc(100% / 2);
+ height: calc(100% / 2);
+ }
+}
+
+@media only screen and (max-height: 670px) {
+ #ui-root {
+ transform: scale(2);
+ width: calc(100% / 2);
+ height: calc(100% / 2);
+ }
+}
+
+@media only screen and (max-width: 561px) {
+ #ui-root {
+ transform: scale(1);
+ width: calc(100% / 1);
+ height: calc(100% / 1);
+ }
+}
+
+@media only screen and (max-height: 430px) {
+ #ui-root {
+ transform: scale(1);
+ width: calc(100% / 1);
+ height: calc(100% / 1);
+ }
+}
\ No newline at end of file
diff --git a/test/basic.test.js b/test/basic.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..09e92f488c7889aa689bd6f0c85ddb757ab2a4e3
--- /dev/null
+++ b/test/basic.test.js
@@ -0,0 +1,7 @@
+/* eslint-env mocha */
+
+describe('basic', () => {
+ it('test', () => {
+
+ })
+})
diff --git a/webpack.common.js b/webpack.common.js
new file mode 100644
index 0000000000000000000000000000000000000000..16fa228c4104dc04c822e897222a3ae24b5ff88d
--- /dev/null
+++ b/webpack.common.js
@@ -0,0 +1,86 @@
+const webpack = require('webpack')
+const path = require('path')
+const CopyPlugin = require('copy-webpack-plugin')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const WorkboxPlugin = require('workbox-webpack-plugin')
+// https://webpack.js.org/guides/production/
+
+const config = {
+ entry: path.resolve(__dirname, './index.js'),
+ output: {
+ path: path.resolve(__dirname, './public'),
+ filename: './index.js',
+ publicPath: './'
+ },
+ resolve: {
+ alias: {
+ 'minecraft-protocol': path.resolve(
+ __dirname,
+ 'node_modules/minecraft-protocol/src/index.js'
+ ), // Hack to allow creating the client in a browser
+ express: false,
+ net: 'net-browserify',
+ fs: false,
+ jose: false,
+ '@azure/msal-node': false
+ },
+ fallback: {
+ jose: false,
+ zlib: require.resolve('browserify-zlib'),
+ stream: require.resolve('stream-browserify'),
+ buffer: require.resolve('buffer/'),
+ events: require.resolve('events/'),
+ assert: require.resolve('assert/'),
+ crypto: require.resolve('crypto-browserify'),
+ path: require.resolve('path-browserify'),
+ constants: require.resolve('constants-browserify'),
+ os: require.resolve('os-browserify/browser'),
+ http: require.resolve('http-browserify'),
+ https: require.resolve('https-browserify'),
+ timers: require.resolve('timers-browserify'),
+ // fs: require.resolve("fs-memory/singleton"),
+ child_process: false,
+ tls: false,
+ perf_hooks: path.resolve(__dirname, 'lib/perf_hooks_replacement.js'),
+ dns: path.resolve(__dirname, 'lib/dns.js')
+ }
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ template: 'index.html',
+ hash: true,
+ minify: false
+ }),
+ // fix "process is not defined" error:
+ new webpack.ProvidePlugin({
+ process: 'process/browser'
+ }),
+ new webpack.ProvidePlugin({
+ Buffer: ['buffer', 'Buffer']
+ }),
+ new webpack.NormalModuleReplacementPlugin(
+ /prismarine-viewer[/|\\]viewer[/|\\]lib[/|\\]utils/,
+ './utils.web.js'
+ ),
+ new WorkboxPlugin.GenerateSW({
+ // these options encourage the ServiceWorkers to get in there fast
+ // and not allow any straggling "old" SWs to hang around
+ clientsClaim: true,
+ skipWaiting: true,
+ include: ['index.html', 'manifest.json'] // not caching a lot as anyway this works only online
+ }),
+ new CopyPlugin({
+ patterns: [
+ { from: path.join(__dirname, '/styles.css'), to: './styles.css' },
+ { from: path.join(__dirname, '/node_modules/prismarine-viewer/public/blocksStates/'), to: './blocksStates/' },
+ { from: path.join(__dirname, '/node_modules/prismarine-viewer/public/textures/'), to: './textures/' },
+ { from: path.join(__dirname, '/node_modules/prismarine-viewer/public/worker.js'), to: './' },
+ { from: path.join(__dirname, 'assets/'), to: './' },
+ { from: path.join(__dirname, 'extra-textures/'), to: './extra-textures/' },
+ { from: path.join(__dirname, 'config.json'), to: './config.json' }
+ ]
+ })
+ ]
+}
+
+module.exports = config
diff --git a/webpack.dev.js b/webpack.dev.js
new file mode 100644
index 0000000000000000000000000000000000000000..90628721e9482c86d2317124c5d7293a186d3838
--- /dev/null
+++ b/webpack.dev.js
@@ -0,0 +1,16 @@
+const { merge } = require('webpack-merge')
+const common = require('./webpack.common.js')
+const path = require('path')
+
+module.exports = merge(common, {
+ mode: 'development',
+ devtool: 'inline-source-map',
+ cache: true,
+ devServer: {
+ contentBase: path.resolve(__dirname, './public'),
+ compress: true,
+ inline: true,
+ // open: true,
+ hot: true
+ }
+})
diff --git a/webpack.prod.js b/webpack.prod.js
new file mode 100644
index 0000000000000000000000000000000000000000..281540b25437d2562e70d7f1b10d082ec160332f
--- /dev/null
+++ b/webpack.prod.js
@@ -0,0 +1,15 @@
+const { merge } = require('webpack-merge')
+const common = require('./webpack.common.js')
+
+const LodashModuleReplacementPlugin = require('lodash-webpack-plugin')
+const { CleanWebpackPlugin } = require('clean-webpack-plugin')
+const webpack = require('webpack')
+
+module.exports = merge(common, {
+ mode: 'production',
+ plugins: [
+ new CleanWebpackPlugin(),
+ new webpack.optimize.ModuleConcatenationPlugin(),
+ new LodashModuleReplacementPlugin()
+ ]
+})