feat: new scalable options GUI. Refactored options & main menu to React

chore: added storybook!
fixed too big ts language service memory consumption (json mc-data)
This commit is contained in:
Vitaly Turovsky 2023-10-11 19:38:16 +03:00
commit 9351732d09
45 changed files with 1171 additions and 788 deletions

View file

@ -15,7 +15,7 @@ jobs:
- run: pnpm lint
- run: pnpm check-build
- run: nohup pnpm prod-start &
- run: nohup node cypress/minecraft-server.mjs &
- run: nohup pnpm test-mc-server &
- uses: cypress-io/github-action@v5
with:
install: false

View file

@ -49,7 +49,7 @@ it('Loads & renders singleplayer', () => {
},
renderDistance: 2
})
cy.get('#title-screen').find('[data-test-id="singleplayer-button"]', { includeShadowDom: true }).click()
cy.get('[data-test-id="singleplayer-button"]', { includeShadowDom: true }).click()
testWorldLoad()
})
@ -60,7 +60,7 @@ it('Joins to server', {
window.localStorage.version = ''
visit()
// todo replace with data-test
cy.get('#title-screen').find('[data-test-id="connect-screen-button"]', { includeShadowDom: true }).click()
cy.get('[data-test-id="connect-screen-button"]', { includeShadowDom: true }).click()
cy.get('input#serverip', { includeShadowDom: true }).clear().focus().type('localhost')
cy.get('[data-test-id="connect-to-server"]', { includeShadowDom: true }).click()
testWorldLoad()
@ -68,7 +68,7 @@ it('Joins to server', {
it('Loads & renders zip world', () => {
cleanVisit()
cy.get('#title-screen').find('[data-test-id="select-file-folder"]', { includeShadowDom: true }).click({ shiftKey: true })
cy.get('[data-test-id="select-file-folder"]', { includeShadowDom: true }).click({ shiftKey: true })
cy.get('input[type="file"]').selectFile('cypress/superflat.zip', { force: true })
testWorldLoad()
})

View file

@ -44,9 +44,6 @@
<pmui-loading-error-screen id="loading-error-screen" style="display: none;"></pmui-loading-error-screen>
<pmui-playscreen id="play-screen" style="display: none;"></pmui-playscreen>
<pmui-keybindsscreen id="keybinds-screen" style="display: none;"></pmui-keybindsscreen>
<pmui-optionsscreen id="options-screen" style="display: none;"></pmui-optionsscreen>
<pmui-advanced-optionsscreen style="display: none;"></pmui-advanced-optionsscreen>
<pmui-titlescreen id="title-screen" style="display: none;"></pmui-titlescreen>
<pmui-notification></pmui-notification>
<context-menu id="context-menu"></context-menu>
</div>

View file

@ -55,6 +55,7 @@
"react-dom": "^18.2.0",
"stats-gl": "^1.0.5",
"stats.js": "^0.17.0",
"title-case": "3.x",
"valtio": "^1.11.1",
"workbox-build": "^7.0.0"
},
@ -110,7 +111,8 @@
"prismarine-world": "github:zardoy/prismarine-world#next-era",
"minecraft-data": "3.45.0",
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
"minecraft-protocol": "github:zardoy/minecraft-protocol#custom-client-extra"
"minecraft-protocol": "github:zardoy/minecraft-protocol#custom-client-extra",
"react": "^18.2.0"
}
}
}

197
pnpm-lock.yaml generated
View file

@ -10,6 +10,7 @@ overrides:
minecraft-data: 3.45.0
prismarine-provider-anvil: github:zardoy/prismarine-provider-anvil#everything
minecraft-protocol: github:zardoy/minecraft-protocol#custom-client-extra
react: ^18.2.0
importers:
@ -108,6 +109,9 @@ importers:
stats.js:
specifier: ^0.17.0
version: 0.17.0
title-case:
specifier: 3.x
version: 3.0.3
valtio:
specifier: ^1.11.1
version: 1.11.2(@types/react@18.2.20)(react@18.2.0)
@ -1748,7 +1752,7 @@ packages:
/@dimaka/interface@0.0.3-alpha.0(@babel/core@7.22.11)(@popperjs/core@2.11.8)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-BzRUaLk+jhC1uvcix4SXRGhw7k39pldlvwtRT0NGFsuQt1uMV9vOhGMzm3rLorVXQ7vAmqcZk3Hy8oYlXKQoDw==}
peerDependencies:
react: ^17.0.2
react: ^18.2.0
dependencies:
'@emotion/css': 11.5.0(@babel/core@7.22.11)
'@juggle/resize-observer': 3.3.1
@ -1844,7 +1848,7 @@ packages:
/@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0):
resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==}
peerDependencies:
react: '>=16.8.0'
react: ^18.2.0
dependencies:
react: 18.2.0
dev: true
@ -2293,7 +2297,7 @@ packages:
/@floating-ui/react-dom@2.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==}
peerDependencies:
react: '>=16.8.0'
react: ^18.2.0
react-dom: '>=16.8.0'
dependencies:
'@floating-ui/dom': 1.5.3
@ -2888,7 +2892,7 @@ packages:
/@mdx-js/react@2.3.0(react@18.2.0):
resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==}
peerDependencies:
react: '>=16'
react: ^18.2.0
dependencies:
'@types/mdx': 2.0.8
'@types/react': 18.2.20
@ -2963,7 +2967,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -2984,7 +2988,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3007,7 +3011,7 @@ packages:
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3021,7 +3025,7 @@ packages:
resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3035,7 +3039,7 @@ packages:
resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3050,7 +3054,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3074,7 +3078,7 @@ packages:
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3089,7 +3093,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3111,7 +3115,7 @@ packages:
resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3127,7 +3131,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3157,7 +3161,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3178,7 +3182,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3199,7 +3203,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3228,7 +3232,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3269,7 +3273,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3289,7 +3293,7 @@ packages:
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3305,7 +3309,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3332,7 +3336,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3355,7 +3359,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3381,7 +3385,7 @@ packages:
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3395,7 +3399,7 @@ packages:
resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3410,7 +3414,7 @@ packages:
resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3425,7 +3429,7 @@ packages:
resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3439,7 +3443,7 @@ packages:
resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3453,7 +3457,7 @@ packages:
resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3468,7 +3472,7 @@ packages:
resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -3484,7 +3488,7 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react: ^18.2.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
@ -3590,7 +3594,7 @@ packages:
/@storybook/addon-actions@7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-SsqZr3js5NinKPnC8AeNI7Ij+Q6fIl9tRdRmSulEgjksjOg7E5S1/Wsn5Bb2CCgj7MaX6VxGyC7s3XskQtDiIQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
react:
@ -3624,7 +3628,7 @@ packages:
/@storybook/addon-backgrounds@7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-+LHTZB/ZYMAzkyD5ZxSriBsqmsrvIaW/Nnd/BeuXGbkrVKKqM0qAKiFZAfjc2WchA1piVNy0/1Rsf+kuYCEiJw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
react:
@ -3652,7 +3656,7 @@ packages:
/@storybook/addon-controls@7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-4lq3sycEUIsK8SUWDYc60QgF4vV9FZZ3lDr6M7j2W9bOnvGw49d2fbdlnq+bX1ZprZZ9VgglQpBAorQB3BXZRw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
react:
@ -3684,7 +3688,7 @@ packages:
/@storybook/addon-docs@7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-dLaub+XWFq4hChw+xfuF9yYg0Txp77FUawKoAigccfjWXx+OOhRV3XTuAcknpXkYq94GWynHgUFXosXT9kbDNA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@jest/transform': 29.7.0
@ -3718,7 +3722,7 @@ packages:
/@storybook/addon-essentials@7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-dWodufrt71TK7ELkeIvVae/x4PzECUlbOm57Iqqt4yQCyR291CgvI4PjeB8un2HbpcXCGZ+N/Oj3YkytvzBi4A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@storybook/addon-actions': 7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
@ -3755,7 +3759,7 @@ packages:
/@storybook/addon-links@7.4.6(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-BPygElZKX+CPI9Se6GJNk1dYc5oxuhA+vHigO1tBqhiM6VkHyFP3cvezJNQvpNYhkUnu3cxnZXb3UJnlRbPY3g==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
react:
@ -3780,7 +3784,7 @@ packages:
/@storybook/addon-measure@7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-nCymMLaHnxv8TE3yEM1A9Tulb1NuRXRNmtsdHTkjv7P1aWCxZo8A/GZaottKe/GLT8jSRjZ+dnpYWrbAhw6wTQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
react:
@ -3806,7 +3810,7 @@ packages:
/@storybook/addon-outline@7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-errNUblRVDLpuEaHQPr/nsrnsUkD2ARmXawkRvizgDWLIDMDJYjTON3MUCaVx3x+hlZ3I6X//G5TVcma8tCc8A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
react:
@ -3832,7 +3836,7 @@ packages:
/@storybook/addon-toolbars@7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-L9m2FBcKeteGq7qIYsMJr0LEfiH7Wdrv5IDcldZTn68eZUJTh1p4GdJZcOmzX1P5IFRr76hpu03iWsNlWQjpbQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
react:
@ -3855,7 +3859,7 @@ packages:
/@storybook/addon-viewport@7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-INDtk54j7bi7NgxMfd2ATmbA0J7nAd6X8itMkLIyPuPJtx8bYHPDORyemDOd0AojgmAdTOAyUtDYdI/PFeo4Cw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
react:
@ -3882,7 +3886,7 @@ packages:
/@storybook/blocks@7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-HxBSAeOiTZW2jbHQlo1upRWFgoMsaAyKijUFf5MwwMNIesXCuuTGZDJ3xTABwAVLK2qC9Ektfbo0CZCiPVuDRQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@storybook/channels': 7.4.6
@ -4075,7 +4079,7 @@ packages:
/@storybook/components@7.4.6(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-nIRBhewAgrJJVafyCzuaLx1l+YOfvvD5dOZ0JxZsxJsefOdw1jFpUqUZ5fIpQ2moyvrR0mAUFw378rBfMdHz5Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@radix-ui/react-select': 1.2.2(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
@ -4246,7 +4250,7 @@ packages:
/@storybook/manager-api@7.4.6(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-inrm3DIbCp8wjXSN/wK6e6i2ysQ/IEmtC7IN0OJ7vdrp+USCooPT448SQTUmVctUGCFmOU3fxXByq8g77oIi7w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@storybook/channels': 7.4.6
@ -4310,7 +4314,7 @@ packages:
/@storybook/react-dom-shim@7.4.6(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-DSq8l9FDocUF1ooVI+TF83pddj1LynE/Hv0/y8XZhc3IgJ/HkuOQuUmfz29ezgfAi9gFYUR8raTIBi3/xdoRmw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
@ -4321,7 +4325,7 @@ packages:
resolution: {integrity: sha512-jkjnrf3FxzR5wcmebXRPflrsM4WIDjWyW/NVFJwxi5PeIOk7fE7/QAPrm4NFRUu2Q7DeuH3oLKsw8bigvUI9RA==}
engines: {node: '>=16'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
vite: ^3.0.0 || ^4.0.0
dependencies:
@ -4349,7 +4353,7 @@ packages:
resolution: {integrity: sha512-w0dVo64baFFPTGpUOWFqkKsu6pQincoymegSNgqaBd5DxEyMDRiRoTWSJHMKE9BwgE8SyWhRkP1ak1mkccSOhQ==}
engines: {node: '>=16.0.0'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
typescript: '*'
peerDependenciesMeta:
@ -4388,7 +4392,7 @@ packages:
/@storybook/router@7.4.6(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Vl1esrHkcHxDKqc+HY7+6JQpBPW3zYvGk0cQ2rxVMhWdLZTAz1hss9DqzN9tFnPyfn0a1Q77EpMySkUrvWKKNQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@storybook/client-logger': 7.4.6
@ -4417,7 +4421,7 @@ packages:
/@storybook/theming@7.4.6(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0)
@ -4441,7 +4445,7 @@ packages:
resolution: {integrity: sha512-L/y6MTLbqfHaM0faK9Yl8n5PIyW4daZrtk7NfaOT6UjgNFjOx3o4CctYew6oj90cNk5HdZQX2OZny043GxDLZw==}
engines: {node: ^14.18 || >=16}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@storybook/builder-vite': 7.4.6(typescript@5.2.2)(vite@4.4.10)
@ -5192,7 +5196,7 @@ packages:
resolution: {integrity: sha512-glABtx54mh4XSaK6BNALWE3mlshPjcPwPsRj/GnOXEA7WJY/6n43iJoukbaYF3758mGZRU5Fq6gklyFjBg0yHQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
react: ^18.0.0
react: ^18.2.0
react-dom: ^18.0.0
dependencies:
classnames: 2.3.2
@ -6427,8 +6431,8 @@ packages:
emittery: 0.10.2
lodash-es: 4.17.21
optionalDependencies:
react: 17.0.2
use-typed-event-listener: 4.0.2(react@17.0.2)(typescript@5.2.2)
react: 18.2.0
use-typed-event-listener: 4.0.2(react@18.2.0)(typescript@5.2.2)
transitivePeerDependencies:
- typescript
dev: true
@ -9977,7 +9981,7 @@ packages:
resolution: {integrity: sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==}
engines: {node: '>= 10'}
peerDependencies:
react: '>= 0.14.0'
react: ^18.2.0
dependencies:
react: 18.2.0
dev: true
@ -10344,7 +10348,7 @@ packages:
/nano-css@5.3.5(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==}
peerDependencies:
react: '*'
react: ^18.2.0
react-dom: '*'
dependencies:
css-tree: 1.1.3
@ -11430,7 +11434,7 @@ packages:
/qrcode.react@3.1.0(react@18.2.0):
resolution: {integrity: sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
dependencies:
react: 18.2.0
dev: false
@ -11554,7 +11558,7 @@ packages:
/react-colorful@5.6.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
peerDependencies:
react: '>=16.8.0'
react: ^18.2.0
react-dom: '>=16.8.0'
dependencies:
react: 18.2.0
@ -11600,7 +11604,7 @@ packages:
/react-element-to-jsx-string@15.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==}
peerDependencies:
react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0
react: ^18.2.0
react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0
dependencies:
'@base2/pretty-print-object': 1.0.1
@ -11617,7 +11621,7 @@ packages:
/react-inspector@6.0.2(react@18.2.0):
resolution: {integrity: sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==}
peerDependencies:
react: ^16.8.4 || ^17.0.0 || ^18.0.0
react: ^18.2.0
dependencies:
react: 18.2.0
dev: true
@ -11637,7 +11641,7 @@ packages:
resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==}
peerDependencies:
'@popperjs/core': ^2.0.0
react: ^16.8.0 || ^17 || ^18
react: ^18.2.0
react-dom: ^16.8.0 || ^17 || ^18
dependencies:
'@popperjs/core': 2.11.8
@ -11650,7 +11654,7 @@ packages:
/react-portal@4.2.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-fE9kOBagwmTXZ3YGRYb4gcMy+kSA+yLO0xnPankjRlfBv4uCpFXqKPfkpsGQQR15wkZ9EssnvTOl1yMzbkxhPQ==}
peerDependencies:
react: ^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0
react: ^18.2.0
react-dom: ^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0
dependencies:
prop-types: 15.8.1
@ -11668,7 +11672,7 @@ packages:
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -11684,7 +11688,7 @@ packages:
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -11703,7 +11707,7 @@ packages:
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -11718,7 +11722,7 @@ packages:
/react-universal-interface@0.6.2(react@18.2.0)(tslib@2.6.2):
resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==}
peerDependencies:
react: '*'
react: ^18.2.0
tslib: '*'
dependencies:
react: 18.2.0
@ -11728,7 +11732,7 @@ packages:
/react-use-measure@2.1.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==}
peerDependencies:
react: '>=16.13'
react: ^18.2.0
react-dom: '>=16.13'
dependencies:
debounce: 1.2.1
@ -11739,7 +11743,7 @@ packages:
/react-use@17.3.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-hs7+tS4rRm1QLHPfanLCqXIi632tP4V7Sai1ENUP2WTufU6am++tU9uSw9YrNCFqbABiEv0ndKU1XCUcfu2tXA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0
react: ^18.2.0
react-dom: ^16.8.0 || ^17.0.0
dependencies:
'@types/js-cookie': 2.2.7
@ -11760,16 +11764,6 @@ packages:
tslib: 2.6.2
dev: false
/react@17.0.2:
resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
dev: true
optional: true
/react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
@ -13076,6 +13070,12 @@ packages:
engines: {node: '>=14.0.0'}
dev: true
/title-case@3.0.3:
resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==}
dependencies:
tslib: 2.6.2
dev: false
/tmp@0.2.1:
resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==}
engines: {node: '>=8.17.0'}
@ -13477,7 +13477,7 @@ packages:
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -13487,20 +13487,10 @@ packages:
tslib: 2.6.2
dev: true
/use-deep-compare@1.1.0(react@17.0.2):
resolution: {integrity: sha512-6yY3zmKNCJ1jjIivfZMZMReZjr8e6iC6Uqtp701jvWJ6ejC/usXD+JjmslZDPJQgX8P4B1Oi5XSLHkOLeYSJsA==}
peerDependencies:
react: '>=16.8.0'
dependencies:
dequal: 1.0.0
react: 17.0.2
dev: true
optional: true
/use-deep-compare@1.1.0(react@18.2.0):
resolution: {integrity: sha512-6yY3zmKNCJ1jjIivfZMZMReZjr8e6iC6Uqtp701jvWJ6ejC/usXD+JjmslZDPJQgX8P4B1Oi5XSLHkOLeYSJsA==}
peerDependencies:
react: '>=16.8.0'
react: ^18.2.0
dependencies:
dequal: 1.0.0
react: 18.2.0
@ -13509,7 +13499,7 @@ packages:
/use-resize-observer@9.1.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==}
peerDependencies:
react: 16.8.0 - 18
react: ^18.2.0
react-dom: 16.8.0 - 18
dependencies:
'@juggle/resize-observer': 3.3.1
@ -13522,7 +13512,7 @@ packages:
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -13536,32 +13526,15 @@ packages:
/use-sync-external-store@1.2.0(react@18.2.0):
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^18.2.0
dependencies:
react: 18.2.0
/use-typed-event-listener@4.0.2(react@17.0.2)(typescript@5.2.2):
resolution: {integrity: sha512-AhjRw+qg5t7OWg7en2Q4XDCdi2iyhALFl71tITL3FeZqt/jD1Qa6KnbH/UolARtID1Gd0IElizbCgolv3ZbUFA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
peerDependencies:
react: '>=16.14.0'
typescript: '>=4.1.2'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@babel/runtime': 7.22.11
react: 17.0.2
typescript: 5.2.2
use-deep-compare: 1.1.0(react@17.0.2)
dev: true
optional: true
/use-typed-event-listener@4.0.2(react@18.2.0)(typescript@5.2.2):
resolution: {integrity: sha512-AhjRw+qg5t7OWg7en2Q4XDCdi2iyhALFl71tITL3FeZqt/jD1Qa6KnbH/UolARtID1Gd0IElizbCgolv3ZbUFA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
peerDependencies:
react: '>=16.14.0'
react: ^18.2.0
typescript: '>=4.1.2'
peerDependenciesMeta:
typescript:
@ -13634,7 +13607,7 @@ packages:
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=16.8'
react: '>=16.8'
react: ^18.2.0
peerDependenciesMeta:
'@types/react':
optional: true
@ -14313,7 +14286,7 @@ packages:
resolution: {integrity: sha512-/WfLJuXiEJimt61KGMHebrFBwckkCHGhAgVXTgPQHl6IMzjqm6MREb1OnDSnCRiSmRdhgdFCctceg6tSm79hiw==}
engines: {node: '>=12.7.0'}
peerDependencies:
react: '>=16.8'
react: ^18.2.0
peerDependenciesMeta:
react:
optional: true

View file

@ -1,7 +1,7 @@
//@ts-check
import { polyfillNode } from 'esbuild-plugin-polyfill-node'
import { join, dirname } from 'path'
import { join, dirname, basename } from 'path'
import * as fs from 'fs'
import { filesize } from 'filesize'
@ -52,7 +52,7 @@ const plugins = [
build.onResolve({
filter: /.*/,
}, async ({ path, ...rest }) => {
if (['.woff', '.woff2', '.ttf'].some(ext => path.endsWith(ext))) {
if (['.woff', '.woff2', '.ttf'].some(ext => path.endsWith(ext)) || path.startsWith('extra-textures/')) {
return {
path,
namespace: 'assets',
@ -63,6 +63,7 @@ const plugins = [
build.onEnd(async ({ metafile, outputFiles }) => {
// write outputFiles
//@ts-ignore
for (const file of outputFiles) {
await fs.promises.writeFile(file.path, file.contents)
}
@ -89,7 +90,7 @@ const plugins = [
}, async ({ resolveDir, path, importer, kind, pluginData }) => {
if (pluginData?.__internal) return
if (!resolveDir.startsWith(process.cwd())) {
const redirected = await build.resolve(path, { kind, importer, resolveDir: process.cwd(), pluginData: {__internal: true}, });
const redirected = await build.resolve(path, { kind, importer, resolveDir: process.cwd(), pluginData: { __internal: true }, })
return redirected
}
// disallow imports from outside the root directory to ensure modules are resolved from node_modules of this workspace
@ -109,7 +110,9 @@ const plugins = [
build.onStart(() => {
time = Date.now()
})
build.onEnd(({ errors, outputFiles, metafile, warnings }) => {
build.onEnd(({ errors, outputFiles: _outputFiles, metafile, warnings }) => {
/** @type {any} */
const outputFiles = _outputFiles
const elapsed = Date.now() - time
if (errors.length) {
@ -230,6 +233,31 @@ const plugins = [
})
}
},
{
name: 'react-displayname',
setup (build) {
build.onLoad({
filter: /.tsx$/,
}, async ({ path }) => {
let file = await fs.promises.readFile(path, 'utf8')
const fileName = basename(path, '.tsx')
let replaced = false
const varName = `__${fileName}_COMPONENT`
file = file.replace(/export default /, () => {
replaced = true
return `const ${varName} = `
})
if (replaced) {
file += `;${varName}.displayName = '${fileName}';export default ${varName};`
}
return {
contents: file,
loader: 'tsx',
}
})
}
},
polyfillNode({
polyfills: {
fs: false,

43
src/basicSounds.ts Normal file
View file

@ -0,0 +1,43 @@
import { options } from './optionsStorage'
let audioContext: AudioContext
const sounds: Record<string, any> = {}
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
const loadingSounds = []
const convertedSounds = []
export async function loadSound (path: string) {
if (loadingSounds.includes(path)) return
loadingSounds.push(path)
const res = await window.fetch(path)
const data = await res.arrayBuffer()
sounds[path] = data
loadingSounds.splice(loadingSounds.indexOf(path), 1)
}
export async function playSound (path) {
audioContext ??= new window.AudioContext()
for (const [soundName, sound] of Object.entries(sounds)) {
if (convertedSounds.includes(soundName)) continue
sounds[soundName] = await audioContext.decodeAudioData(sound)
convertedSounds.push(soundName)
}
const volume = options.volume / 100
const soundBuffer = sounds[path]
if (!soundBuffer) {
console.warn(`Sound ${path} not loaded`)
return
}
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)
}

View file

@ -4,9 +4,8 @@ import { classMap } from 'lit/directives/class-map.js'
import { LitElement, html, css } from 'lit'
import { isCypress } from './utils'
import { getBuiltinCommandsList, tryHandleBuiltinCommand } from './builtinCommands'
import { notification } from './menus/notification'
import { options } from './optionsStorage'
import { activeModalStack, hideCurrentModal, showModal, miscUiState } from './globalState'
import { activeModalStack, hideCurrentModal, showModal, miscUiState, notification } from './globalState'
import { formatMessage } from './botUtils'
import { getColorShadow, messageFormatStylesMap } from './react/MessageFormatted'

View file

@ -1,8 +1,7 @@
import { promisify } from 'util'
import * as nbt from 'prismarine-nbt'
import { showNotification } from './menus/notification'
import { openWorldDirectory, openWorldZip } from './browserfs'
import { isGameActive } from './globalState'
import { isGameActive, showNotification } from './globalState'
const parseNbt = promisify(nbt.parse)
window.nbt = nbt

17
src/flyingSquidUtils.ts Normal file
View file

@ -0,0 +1,17 @@
import * as crypto from 'crypto'
import UUID from 'uuid-1345'
// https://github.com/PrismarineJS/node-minecraft-protocol/blob/cf1f67117d586b5e6e21f0d9602da12e9fcf46b6/src/server/login.js#L170
function javaUUID (s: string) {
const hash = crypto.createHash('md5')
hash.update(s, 'utf8')
const buffer = hash.digest()
buffer[6] = (buffer[6] & 15) | 48
buffer[8] = (buffer[8] & 63) | 128
return buffer
}
export function nameToMcOfflineUUID (name) {
return (new UUID(javaUUID('OfflinePlayer:' + name))).toString()
}

View file

@ -2,7 +2,7 @@
import { proxy, ref, subscribe } from 'valtio'
import { pointerLock } from './utils'
import { options } from './optionsStorage'
import { OptionsGroupType, options } from './optionsStorage'
// todo: refactor structure with support of hideNext=false
@ -27,8 +27,6 @@ subscribe(activeModalStack, () => {
if (activeModalStack.length === 0) {
if (isGameActive(false)) {
void pointerLock.requestPointerLock()
} else {
showModal(document.getElementById('title-screen'))
}
} else {
document.exitPointerLock?.()
@ -94,6 +92,10 @@ export const hideCurrentModal = (_data = undefined, onHide = undefined) => {
}
}
export const openOptionsMenu = (group: OptionsGroupType) => {
showModal({ reactType: `options-${group}` })
}
// ---
export const currentContextMenu = proxy({ items: [] as ContextMenuItem[] | null, x: 0, y: 0 })
@ -146,6 +148,19 @@ export const gameAdditionalState = proxy({
window.gameAdditionalState = gameAdditionalState
// rename current (non-stackable) notification to one-time (system) notification
const initialNotification = {
show: false,
autoHide: true,
message: '',
type: 'info',
}
export const notification = proxy(initialNotification)
export const showNotification = (/** @type {Partial<typeof notification>} */newNotification) => {
Object.assign(notification, { show: true, ...newNotification }, initialNotification)
}
const savePlayers = () => {
if (!window.localServer) return
for (const player of window.localServer.players) {
@ -167,7 +182,7 @@ window.inspectPlayer = () => require('fs').promises.readFile('/world/playerdata/
// todo move from global state
window.addEventListener('beforeunload', (event) => {
// todo-low maybe exclude chat?
if (!isGameActive(true) && activeModalStack.at(-1)?.elem.id !== 'chat') return
if (!isGameActive(true) && activeModalStack.at(-1)?.elem?.id !== 'chat') return
if (sessionStorage.lastReload && !options.preventDevReloadWhilePlaying) return
if (!options.closeConfirmation) return

4
src/globals.d.ts vendored
View file

@ -47,6 +47,10 @@ declare module '*.css' {
const css: string
export default css
}
declare module '*.json' {
const json: any
export = json
}
declare module '*.png' {
const png: string
export default png

View file

@ -21,11 +21,7 @@ import './menus/play_screen'
import './menus/pause_screen'
import './menus/loading_or_error_screen'
import './menus/keybinds_screen'
import './menus/options_screen'
import './menus/advanced_options_screen'
import { notification } from './menus/notification'
import './menus/title_screen'
import { initWithRenderer, statsEnd, statsStart } from './rightTopStats'
import { initWithRenderer, statsEnd, statsStart } from './topRightStats'
import { options, watchValue } from './optionsStorage'
import './reactUi.jsx'
@ -57,7 +53,8 @@ import {
isGameActive,
miscUiState,
gameAdditionalState,
resetStateAfterDisconnect
resetStateAfterDisconnect,
notification
} from './globalState'
import {
@ -197,7 +194,7 @@ async function main () {
const connectSingleplayer = (serverOverrides = {}) => {
void connect({ singleplayer: true, username: options.localUsername, password: '', serverOverrides })
}
document.querySelector('#title-screen').addEventListener('singleplayer', (e) => {
window.addEventListener('singleplayer', (e) => {
//@ts-expect-error
connectSingleplayer(e.detail)
})
@ -246,7 +243,7 @@ async function connect (connectOptions: {
const p2pMultiplayer = !!connectOptions.peerId
miscUiState.singleplayer = singeplayer
miscUiState.flyingSquid = singeplayer || p2pMultiplayer
const { renderDistance, maxMultiplayerRenderDistance } = options
const { renderDistance, maxMultiplayerRenderDistance = renderDistance } = options
const server = cleanConnectIp(connectOptions.server, '25565')
const proxy = cleanConnectIp(connectOptions.proxy, undefined)
const { username, password } = connectOptions
@ -719,15 +716,14 @@ window.addEventListener('keydown', (e) => {
window.addEventListener('keydown', (e) => {
if (e.code === 'F11') {
e.preventDefault()
goFullscreen(true)
void goFullscreen(true)
}
if (e.code === 'KeyL' && e.altKey) {
console.clear()
}
})
addPanoramaCubeMap()
showModal(document.getElementById('title-screen'))
void addPanoramaCubeMap()
void main()
downloadAndOpenFile().then((downloadAction) => {
if (downloadAction) return

View file

@ -5,7 +5,7 @@ import * as nbt from 'prismarine-nbt'
import { proxy } from 'valtio'
import { gzip } from 'node-gzip'
import { options } from './optionsStorage'
import { nameToMcOfflineUUID } from './utils'
import { nameToMcOfflineUUID } from './flyingSquidUtils'
import { forceCachedDataPaths } from './browserfs'
const parseNbt = promisify(nbt.parse)
@ -124,7 +124,7 @@ export const loadSave = async (root = '/world') => {
}
fsState.saveLoaded = true
document.querySelector('#title-screen').dispatchEvent(new CustomEvent('singleplayer', {
window.dispatchEvent(new CustomEvent('singleplayer', {
// todo check gamemode level.dat data etc
detail: {
version,

View file

@ -1,92 +0,0 @@
//@ts-check
const { html, css, LitElement } = require('lit')
const { subscribe } = require('valtio')
const { hideCurrentModal } = require('../globalState')
const { getScreenRefreshRate } = require('../utils')
const { options } = require('../optionsStorage')
const { commonCss, openURL } = require('./components/common')
class AdvancedOptionsScreen extends LitElement {
/** @type {null | number} */
frameLimitMax = null
constructor () {
super()
subscribe(options, () => {
this.requestUpdate()
})
}
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;
}
`
}
render () {
return html`
<div class="${'dirt-bg'}"></div>
<p class="title">Advanced Options</p>
<main>
<div class="wrapper">
<pmui-button pmui-width="150px" pmui-label=${`Always Show Mobile Controls: ${options.alwaysShowMobileControls ? 'ON' : 'OFF'}`} @pmui-click=${() => {
options.alwaysShowMobileControls = !options.alwaysShowMobileControls
}
}></pmui-button>
<!-- todo rename button, also might be unstable -->
<pmui-button pmui-width="150px" pmui-label="Guide: Disable VSync" @click=${() => openURL('https://gist.github.com/zardoy/6e5ce377d2b4c1e322e660973da069cd')}></pmui-button>
</div>
<div class="wrapper">
<pmui-slider .disabled=${!this.frameLimitMax} pmui-label="Frame Limit" .valueDisplay=${options.frameLimit || 'VSync'} pmui-value="${options.frameLimit || this.frameLimitMax + 1}"
pmui-type="${options.frameLimit ? 'fps' : ''}" pmui-min="20" pmui-max="${this.frameLimitMax + 1}" @input=${(e) => {
const newVal = e.target.value
options.frameLimit = newVal > this.frameLimitMax ? false : newVal
this.requestUpdate()
}}></pmui-slider>
<pmui-button pmui-width="20px" pmui-icon="pixelarticons:lock-open" @click=${async () => {
const rate = await getScreenRefreshRate()
this.frameLimitMax = rate
this.requestUpdate()
}}></pmui-button>
</div>
<div class="wrapper">
<pmui-slider pmui-width="150px" pmui-label="Touch Buttons Size" pmui-value="${options.touchButtonsSize}" pmui-type="%" pmui-min="20" pmui-max="100" @input=${(e) => {
options.touchButtonsSize = +e.target.value
}}></pmui-slider>
<pmui-button pmui-width="150px" pmui-label="${`Use Dedicated GPU: ${options.highPerformanceGpu ? 'ON' : 'OFF'}`}" title="Changing requires page reload. Only for those who have two GPUs e.g. on laptops" @pmui-click=${() => {
options.highPerformanceGpu = !options.highPerformanceGpu
}}></pmui-button>
</div>
<pmui-button pmui-width="200px" pmui-label="Done" @pmui-click=${() => hideCurrentModal()}></pmui-button>
</main>
`
}
}
window.customElements.define('pmui-advanced-optionsscreen', AdvancedOptionsScreen)

View file

@ -1,49 +1,7 @@
//@ts-check
import { LitElement, html, css, unsafeCSS } from 'lit'
import widgetsGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png'
import { options } from '../../optionsStorage'
let audioContext
/** @type {Record<string, any>} */
const sounds = {}
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
const loadingSounds = []
const convertedSounds = []
async function loadSound (path) {
loadingSounds.push(path)
const res = await window.fetch(path)
const data = await res.arrayBuffer()
sounds[path] = data
loadingSounds.splice(loadingSounds.indexOf(path), 1)
}
export async function playSound (path) {
audioContext ??= new window.AudioContext()
for (const [soundName, sound] of Object.entries(sounds)) {
if (convertedSounds.includes(soundName)) continue
sounds[soundName] = await audioContext.decodeAudioData(sound)
convertedSounds.push(soundName)
}
const volume = options.volume / 100
const soundBuffer = sounds[path]
if (!soundBuffer) {
console.warn(`Sound ${path} not loaded`)
return
}
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)
}
import { playSound, loadSound } from '../../basicSounds'
class Button extends LitElement {
static get styles () {

View file

@ -2,23 +2,8 @@
// create lit element
const { LitElement, html, css } = require('lit')
const { proxy, subscribe } = require('valtio/vanilla')
// move to globalState?
// rename current (non-stackable) notification to one-time (system) notification
const initialNotification = {
show: false,
autoHide: true,
message: '',
type: 'info',
}
export const notification = proxy(initialNotification)
export const showNotification = (/** @type {Partial<typeof notification>} */newNotification) => {
Object.assign(notification, { show: true, ...newNotification }, initialNotification)
}
window.notification = notification
const { subscribe } = require('valtio')
const { notification } = require('../globalState')
class Notification extends LitElement {
static get properties () {

View file

@ -1,156 +0,0 @@
const { LitElement, html, css } = require('lit')
const { subscribe } = require('valtio')
const { subscribeKey } = require('valtio/utils')
const { showModal, hideCurrentModal, isGameActive, miscUiState } = require('../globalState')
const { toNumber, openFilePicker, setLoadingScreenStatus } = require('../utils')
const { options, watchValue } = require('../optionsStorage')
const { getResourcePackName, uninstallTexturePack, resourcePackState } = require('../texturePack')
const { fsState } = require('../loadSave')
const { commonCss, 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;
}
`
}
constructor () {
super()
watchValue(options, o => {
document.documentElement.style.setProperty('--chatScale', `${o.chatScale / 100}`)
document.documentElement.style.setProperty('--chatWidth', `${o.chatWidth}px`)
document.documentElement.style.setProperty('--chatHeight', `${o.chatHeight}px`)
document.documentElement.style.setProperty('--guiScale', `${o.guiScale}`)
})
subscribe(options, () => {
this.requestUpdate()
})
subscribeKey(miscUiState, 'singleplayer', () => {
this.requestUpdate()
})
subscribeKey(resourcePackState, 'resourcePackInstalled', () => {
this.requestUpdate()
})
}
render () {
return html`
<div class="${isGameActive(false) ? 'bg' : 'dirt-bg'}"></div>
<p class="title">Options</p>
<main>
<div class="wrapper">
<pmui-slider pmui-label="Mouse Sensitivity X" pmui-value="${options.mouseSensX}" pmui-min="1" pmui-max="100" @input=${(e) => {
options.mouseSensX = +e.target.value
}}></pmui-slider>
<pmui-slider pmui-label="Mouse Sensitivity Y" pmui-value="${options.mouseSensY}" pmui-min="1" pmui-max="100" @input=${(e) => {
options.mouseSensY = +e.target.value
}}></pmui-slider>
</div>
<div class="wrapper">
<pmui-slider pmui-label="Chat Width" pmui-value="${options.chatWidth}" pmui-min="0" pmui-max="320" pmui-type="px" @input=${(e) => {
options.chatWidth = +e.target.value
}}></pmui-slider>
<pmui-slider pmui-label="Chat Height" pmui-value="${options.chatHeight}" pmui-min="0" pmui-max="180" pmui-type="px" @input=${(e) => {
options.chatHeight = +e.target.value
}}></pmui-slider>
</div>
<div class="wrapper">
<pmui-slider pmui-label="Chat Scale" pmui-value="${options.chatScale}" pmui-min="0" pmui-max="100" @input=${(e) => {
options.chatScale = +e.target.value
}}></pmui-slider>
<pmui-slider pmui-label="Sound Volume" pmui-value="${options.volume}" pmui-min="0" pmui-max="100" @input=${(e) => {
options.volume = +e.target.value
}}></pmui-slider>
</div>
<div class="wrapper">
<pmui-button .disabled=${true} pmui-width="150px" pmui-label="Key Binds" @pmui-click=${() => showModal(document.getElementById('keybinds-screen'))}></pmui-button>
<pmui-slider pmui-label="Gui Scale" pmui-value="${options.guiScale}" pmui-min="1" pmui-max="4" pmui-type="" @change=${(e) => {
options.guiScale = +e.target.value
}}></pmui-slider>
</div>
<div class="wrapper">
<pmui-slider pmui-label="Render Distance" pmui-value="${options.renderDistance}" .disabled="${isGameActive(false) && !miscUiState.singleplayer ? 'Can be changed only from main menu for now' : undefined}" pmui-min="2" pmui-max="${miscUiState.singleplayer ? 16 : 6}" pmui-type=" chunks" @change=${(e) => {
options.renderDistance = +e.target.value
}}></pmui-slider>
<pmui-slider pmui-label="Field of View" pmui-value="${options.fov}" pmui-min="30" pmui-max="110" pmui-type="" @input=${(e) => {
options.fov = +e.target.value
}}></pmui-slider>
</div>
<div class="wrapper">
<pmui-button pmui-width="150px" pmui-label=${'Advanced'} @pmui-click=${() => {
showModal(document.querySelector('pmui-advanced-optionsscreen'))
}
}></pmui-button>
<pmui-button pmui-width="150px" pmui-label=${'Mouse Raw Input: ' + (options.mouseRawInput ? 'ON' : 'OFF')} title="Wether to disable any mouse acceleration (MC does it by default)" @pmui-click=${() => {
options.mouseRawInput = !options.mouseRawInput
}
}></pmui-button>
</div>
<div class="wrapper">
<pmui-button title="Auto Fullscreen allows you to use Ctrl+W and Escape without delays" .disabled="${navigator['keyboard'] ? undefined : 'Your browser doesn\'t support keyboard lock API'}" pmui-width="150px" pmui-label=${'Auto Fullscreen: ' + (options.autoFullScreen ? 'ON' : 'OFF')} @pmui-click=${() => {
options.autoFullScreen = !options.autoFullScreen
}
}></pmui-button>
<!-- todo also allow to remap f11 -->
<pmui-button title="Exit fullscreen (not recommended, also you can always do it with F11)" pmui-width="150px" pmui-label=${'Auto Exit Fullscreen: ' + (options.autoExitFullscreen ? 'ON' : 'OFF')} @pmui-click=${() => {
options.autoExitFullscreen = !options.autoExitFullscreen
}
}></pmui-button>
</div>
<div class="wrapper">
<pmui-button pmui-width="150px" pmui-label=${'Resource Pack: ' + (resourcePackState.resourcePackInstalled ? 'ON' : 'OFF')} @pmui-click=${async () => {
if (resourcePackState.resourcePackInstalled) {
const resourcePackName = await getResourcePackName()
if (confirm(`Uninstall ${resourcePackName} resource pack?`)) {
// todo make hidable
setLoadingScreenStatus('Uninstalling texturepack...')
await uninstallTexturePack()
setLoadingScreenStatus(undefined)
}
} else {
if (!fsState.inMemorySave && isGameActive(false)) {
alert('Unable to install resource pack in loaded save for now')
return
}
openFilePicker('resourcepack')
}
}}></pmui-button>
</div>
<pmui-button pmui-width="200px" pmui-label="Done" @pmui-click=${() => hideCurrentModal()}></pmui-button>
</main>
`
}
}
window.customElements.define('pmui-optionsscreen', OptionsScreen)

View file

@ -2,12 +2,11 @@
const { LitElement, html, css } = require('lit')
const { subscribe } = require('valtio')
const { subscribeKey } = require('valtio/utils')
const { hideCurrentModal, showModal, miscUiState } = require('../globalState')
const { hideCurrentModal, showModal, miscUiState, notification, openOptionsMenu } = require('../globalState')
const { fsState } = require('../loadSave')
const { saveWorld } = require('../builtinCommands')
const { disconnect } = require('../utils')
const { closeWan, openToWanAndCopyJoinLink, getJoinLink } = require('../localServerMultiplayer')
const { notification } = require('./notification')
const { openURL } = require('./components/common')
class PauseScreen extends LitElement {
@ -77,7 +76,7 @@ class PauseScreen extends LitElement {
<pmui-button pmui-width="98px" pmui-label="GitHub" @pmui-click=${() => openURL(process.env.GITHUB_URL)}></pmui-button>
<pmui-button pmui-width="98px" pmui-label="Discord" @pmui-click=${() => openURL('https://discord.gg/4Ucm684Fq3')}></pmui-button>
</div>
<pmui-button pmui-width="204px" pmui-label="Options" @pmui-click=${() => showModal(document.getElementById('options-screen'))}></pmui-button>
<pmui-button pmui-width="204px" pmui-label="Options" @pmui-click=${() => openOptionsMenu('main')}></pmui-button>
<!-- todo use qr icon (full pixelarticons package) -->
<!-- todo also display copy link button when opened -->
${joinButton ? html`

View file

@ -96,7 +96,6 @@ class PlayScreen extends LitElement {
const getParam = (localStorageKey, qs = localStorageKey) => {
const qsValue = qs ? params.get(qs) : undefined
if (qsValue) {
document.getElementById('title-screen').style.display = 'none'
this.style.display = 'block'
}
return qsValue || window.localStorage.getItem(localStorageKey)
@ -195,7 +194,6 @@ class PlayScreen extends LitElement {
const server = `${this.server}${this.serverport && `:${this.serverport}`}`
const proxy = this.proxy && `${this.proxy}${this.proxyport && `:${this.proxyport}`}`
document.getElementById('title-screen').style.display = 'none'
window.localStorage.setItem('username', this.username)
window.localStorage.setItem('password', this.password)
window.localStorage.setItem('server', server)

View file

@ -1,198 +0,0 @@
const fs = require('fs')
const { LitElement, html, css, unsafeCSS } = require('lit')
const mcImage = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/title/minecraft.png')
const { fsState } = require('../loadSave')
const { showModal } = require('../globalState')
const { openWorldDirectory, openWorldZip } = require('../browserfs')
const { options } = require('../optionsStorage')
const defaultLocalServerOptions = require('../defaultLocalServerOptions')
const { openFilePicker } = require('../utils')
const { openURL } = require('./components/common')
// const SUPPORT_WORLD_LOADING = !!window.showDirectoryPicker
const SUPPORT_WORLD_LOADING = true
class TitleScreen extends LitElement {
static get styles () {
return css`
.game-title {
position: absolute;
top: 30px;
left: calc(50% - 137px);
}
.game-title .minec {
display: block;
position: absolute;
top: 0;
left: 0;
background-image: url('${unsafeCSS(mcImage)}');
background-size: 256px;
width: 155px;
height: 44px;
}
.game-title .raft {
display: block;
position: absolute;
top: 0;
left: 155px;
background-image: url('${unsafeCSS(mcImage)}');
background-size: 256px;
width: 155px;
height: 44px;
background-position-y: -45px;
}
.game-title .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;
}
`
}
static get properties () {
return {
versionStatus: {
type: String
},
versionTitle: {
type: String
},
isOutdated: {
type: Boolean
}
}
}
constructor () {
super()
this.versionStatus = ''
this.versionTitle = ''
this.isOutdated = false
if (process.env.NODE_ENV === 'development') {
this.versionStatus = '(dev)'
} else {
fetch('./version.txt').then(async (f) => {
if (f.status === 404) return
const contents = await f.text()
this.isOutdated = contents === process.env.BUILD_VERSION
this.versionStatus = `(${this.isOutdated ? 'latest' : 'new version available'})`
this.versionTitle = `Loaded: ${process.env.BUILD_VERSION}. Remote: ${contents}`
}, () => { })
}
}
reload () {
navigator.serviceWorker.getRegistration().then(registration => {
registration.unregister().then(() => {
window.location.reload()
})
})
}
render () {
return html`
<div class="game-title">
<div class="minec"></div>
<div class="raft"></div>
<div class="edition"></div>
<span class="splash">Prismarine is a beautiful block</span>
</div>
<div class="menu">
<pmui-button pmui-width="200px" pmui-label="Connect to server" pmui-test-id="connect-screen-button" @pmui-click=${() => showModal(document.getElementById('play-screen'))}></pmui-button>
<div style="display:flex;justify-content: space-between;">
<pmui-button pmui-width="${SUPPORT_WORLD_LOADING ? '170px' : '200px'}" pmui-test-id="singleplayer-button" pmui-label="Singleplayer" @pmui-click=${() => {
this.style.display = 'none'
fsState.isReadonly = false
fsState.syncFs = true
fsState.inMemorySave = true
const notFirstTime = fs.existsSync('./world/level.dat')
if (notFirstTime && !options.localServerOptions.version) {
options.localServerOptions.version = '1.16.1' // legacy version
} else {
options.localServerOptions.version ??= defaultLocalServerOptions.version
}
this.dispatchEvent(new window.CustomEvent('singleplayer', {}))
}}></pmui-button>
${SUPPORT_WORLD_LOADING ? html`<pmui-button pmui-test-id="select-file-folder" pmui-icon="pixelarticons:folder" pmui-width="20px" pmui-label="" @pmui-click=${({ detail: e }) => {
if (!!window.showDirectoryPicker && !e.shiftKey) {
openWorldDirectory()
} else {
openFilePicker()
}
}}></pmui-button>` : ''}
</div>
<pmui-button pmui-width="200px" pmui-label="Options" @pmui-click=${() => showModal(document.getElementById('options-screen'))}></pmui-button>
<div class="menu-row">
<pmui-button pmui-width="98px" pmui-label="GitHub" @pmui-click=${() => openURL(process.env.GITHUB_URL)}></pmui-button>
<pmui-button pmui-width="98px" pmui-label="Discord" @pmui-click=${() => openURL('https://discord.gg/4Ucm684Fq3')}></pmui-button>
</div>
</div>
<div class="bottom-info">
<span title="${this.versionTitle} (click to reload)" @click=${this.reload}>Prismarine Web Client ${this.versionStatus}</span>
<span>A Minecraft client in the browser!</span>
</div>
`
}
}
window.customElements.define('pmui-titlescreen', TitleScreen)

View file

@ -1,84 +0,0 @@
// todo implement async options storage
import { proxy, subscribe } from 'valtio/vanilla'
// weird webpack configuration bug: it cant import valtio/utils in this file
import { subscribeKey } from 'valtio/utils'
const mergeAny: <T>(arg1: T, arg2: any) => T = Object.assign
const defaultOptions = {
renderDistance: 4,
closeConfirmation: true,
autoFullScreen: false,
mouseRawInput: false,
autoExitFullscreen: false,
localUsername: 'wanderer',
mouseSensX: 50,
mouseSensY: 50 as number | true,
// mouseInvertX: false,
chatWidth: 320,
chatHeight: 180,
chatScale: 100,
volume: 50,
// fov: 70,
fov: 75,
guiScale: 3,
autoRequestCompletions: true,
touchButtonsSize: 40,
highPerformanceGpu: false,
showChunkBorders: false,
frameLimit: false as number | false,
alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null,
alwaysShowMobileControls: false,
maxMultiplayerRenderDistance: 6,
excludeCommunicationDebugEvents: [],
preventDevReloadWhilePlaying: false,
numWorkers: 4,
localServerOptions: {},
preferLoadReadonly: false,
disableLoadPrompts: false,
guestUsername: 'guest',
askGuestName: true,
// advanced bot options
autoRespawn: false
}
export type AppOptions = typeof defaultOptions
export const options = proxy(
mergeAny(defaultOptions, JSON.parse(localStorage.options || '{}'))
)
window.options = window.settings = options
subscribe(options, () => {
localStorage.options = JSON.stringify(options)
})
type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T) => void) => void
export const watchValue: WatchValue = (proxy, callback) => {
const watchedProps = new Set<string>()
callback(new Proxy(proxy, {
get (target, p, receiver) {
watchedProps.add(p.toString())
return Reflect.get(target, p, receiver)
},
}))
for (const prop of watchedProps) {
subscribeKey(proxy, prop, () => {
callback(proxy)
})
}
}
watchValue(options, o => {
globalThis.excludeCommunicationDebugEvents = o.excludeCommunicationDebugEvents
})
export const useOptionValue = (setting, valueCallback) => {
valueCallback(setting)
subscribe(setting, valueCallback)
}

208
src/optionsStorage.tsx Normal file
View file

@ -0,0 +1,208 @@
// todo implement async options storage
import { proxy, subscribe } from 'valtio/vanilla'
// weird webpack configuration bug: it cant import valtio/utils in this file
import { subscribeKey } from 'valtio/utils'
import { useState } from 'react'
import { useSnapshot } from 'valtio'
import { OptionMeta, OptionSlider } from './react/OptionsItems'
import Button from './react/Button'
import { openOptionsMenu } from './globalState'
import { openURL } from './menus/components/common'
import Slider from './react/Slider'
import { getScreenRefreshRate } from './utils'
const mergeAny: <T>(arg1: T, arg2: any) => T = Object.assign
const defaultOptions = {
renderDistance: 4,
closeConfirmation: true,
autoFullScreen: false,
mouseRawInput: false,
autoExitFullscreen: false,
localUsername: 'wanderer',
mouseSensX: 50,
mouseSensY: 50 as number | true,
// mouseInvertX: false,
chatWidth: 320,
chatHeight: 180,
chatScale: 100,
volume: 50,
// fov: 70,
fov: 75,
guiScale: 3,
autoRequestCompletions: true,
touchButtonsSize: 40,
highPerformanceGpu: false,
showChunkBorders: false,
frameLimit: false as number | false,
alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null,
alwaysShowMobileControls: false,
maxMultiplayerRenderDistance: null as number | null,
excludeCommunicationDebugEvents: [],
preventDevReloadWhilePlaying: false,
numWorkers: 4,
localServerOptions: {} as any,
preferLoadReadonly: false,
disableLoadPrompts: false,
guestUsername: 'guest',
askGuestName: true,
// advanced bot options
autoRespawn: false
}
export type AppOptions = typeof defaultOptions
export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced'
// todo refactor to separate file like optionsGUI.tsx
export const guiOptionsScheme: {
[t in OptionsGroupType]: Array<{ [k in keyof AppOptions]?: Partial<OptionMeta> } & { custom?}>
} = {
render: [
{
renderDistance: {
unit: '',
min: 2,
max: 16
},
},
{
custom () {
const frameLimitValue = useSnapshot(options).frameLimit
const [frameLimitMax, setFrameLimitMax] = useState(null as number | null)
return <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Slider style={{ width: 130 }} label='Frame Limit' disabledReason={frameLimitMax ? undefined : 'press lock button first'} unit={frameLimitValue ? 'fps' : ''} valueDisplay={frameLimitValue || 'VSync'} value={frameLimitValue || frameLimitMax + 1} min={20} max={frameLimitMax + 1} updateValue={(newVal) => {
options.frameLimit = newVal > frameLimitMax ? false : newVal
}} />
<Button style={{ width: 20 }} icon='pixelarticons:lock-open' onClick={async () => {
const rate = await getScreenRefreshRate()
setFrameLimitMax(rate)
}} />
</div>
}
},
{
highPerformanceGpu: {
text: 'Use Dedicated GPU',
// willHaveNoEffect: isIos
},
},
{
custom () {
return <Button label='Guide: Disable VSync' onClick={() => openURL('https://gist.github.com/zardoy/6e5ce377d2b4c1e322e660973da069cd')} inScreen />
},
}
],
main: [
// renderDistance
{
fov: {
min: 30,
max: 110,
unit: '',
}
},
{
custom () {
return <Button label='Interface...' onClick={() => openOptionsMenu('interface')} inScreen />
},
},
{
custom () {
return <Button label='Render...' onClick={() => openOptionsMenu('render')} inScreen />
},
},
{
custom () {
return <Button label='Sound...' onClick={() => openOptionsMenu('sound')} inScreen />
},
},
{
custom () {
return <Button label='Controls...' onClick={() => openOptionsMenu('controls')} inScreen />
},
}
],
interface: [
{
guiScale: {
max: 4,
unit: '',
delayApply: true,
},
chatWidth: {
max: 320,
unit: 'px',
},
chatHeight: {
max: 180,
unit: 'px',
},
}
],
controls: [
{
// keybindings
mouseSensX: {},
mouseSensY: {},
mouseRawInput: {},
alwaysShowMobileControls: {
text: 'Always Mobile Controls',
},
autoFullScreen: {
tooltip: 'Auto Fullscreen allows you to use Ctrl+W and Escape having to wait/click on screen again.',
},
autoExitFullscreen: {
tooltip: 'Exit fullscreen on escape (pause menu open). But note you can always do it with F11.',
},
touchButtonsSize: {
min: 40
}
}
],
sound: [
{ volume: {} }
],
advanced: [
],
}
export const options = proxy(
mergeAny(defaultOptions, JSON.parse(localStorage.options || '{}'))
)
window.options = window.settings = options
subscribe(options, () => {
localStorage.options = JSON.stringify(options)
})
type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T) => void) => void
export const watchValue: WatchValue = (proxy, callback) => {
const watchedProps = new Set<string>()
callback(new Proxy(proxy, {
get (target, p, receiver) {
watchedProps.add(p.toString())
return Reflect.get(target, p, receiver)
},
}))
for (const prop of watchedProps) {
subscribeKey(proxy, prop, () => {
callback(proxy)
})
}
}
watchValue(options, o => {
globalThis.excludeCommunicationDebugEvents = o.excludeCommunicationDebugEvents
})
export const useOptionValue = (setting, valueCallback) => {
valueCallback(setting)
subscribe(setting, valueCallback)
}

View file

@ -7,6 +7,7 @@ import { fromTexturePackPath, resourcePackState } from './texturePack'
import { options } from './optionsStorage'
let panoramaCubeMap
let shouldDisplayPanorama = false
let panoramaUsesResourePack = false
let viewer
@ -57,8 +58,7 @@ subscribeKey(resourcePackState, 'resourcePackInstalled', async () => {
// Menu panorama background
export async function addPanoramaCubeMap () {
if (panoramaCubeMap) return
// remove all existing object in the viewer.scene
// viewer.scene.children = []
shouldDisplayPanorama = true
let time = 0
viewer.camera = new THREE.PerspectiveCamera(85, window.innerWidth / window.innerHeight, 0.05, 1000)
@ -78,6 +78,8 @@ export async function addPanoramaCubeMap () {
}))
}
if (!shouldDisplayPanorama) return
const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials)
panoramaBox.onBeforeRender = () => {
@ -90,7 +92,8 @@ export async function addPanoramaCubeMap () {
group.add(panoramaBox)
const Entity = require('prismarine-viewer/viewer/lib/entity/Entity')
for (let i = 0; i < 42; i++) {
// should be rewritten entirely
for (let i = 0; i < 20; 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')
@ -112,4 +115,5 @@ export function removePanorama () {
viewer.camera.updateProjectionMatrix()
viewer.scene.remove(panoramaCubeMap)
panoramaCubeMap = null
shouldDisplayPanorama = false
}

View file

@ -1,4 +1,4 @@
import { playSound } from '../menus/components/button'
import { loadSound, playSound } from '../basicSounds'
import buttonCss from './button.module.css'
// testing in storybook from deathscreen
@ -7,15 +7,22 @@ interface Props extends React.ComponentProps<'button'> {
label?: string
icon?: string
children?: React.ReactNode
inScreen?: boolean
}
export default ({ label, icon, children, ...args }: Props) => {
void loadSound('button_click.mp3')
export default ({ label, icon, children, inScreen, ...args }: Props) => {
const onClick = (e) => {
void playSound('button_click.mp3')
args.onClick(e)
}
if (inScreen) {
args.style ??= {}
args.style.width = 150
}
return <button className={buttonCss.button} onClick={onClick} {...args}>
return <button className={buttonCss.button} {...args} onClick={onClick}>
{icon && <iconify-icon class={buttonCss.icon} icon={icon}></iconify-icon>}
{label}
{children}

104
src/react/MainMenu.tsx Normal file
View file

@ -0,0 +1,104 @@
import React, { useEffect, useState } from 'react'
import styles from './mainMenu.module.css'
import Button from './Button'
type Action = (e: React.MouseEvent<HTMLButtonElement>) => void
interface Props {
connectToServerAction?: Action
singleplayerAction?: Action
optionsAction?: Action
githubAction?: Action
discordAction?: Action
openFileAction?: Action
}
const refreshApp = async () => {
const registration = await navigator.serviceWorker.getRegistration()
await registration.unregister()
window.location.reload()
}
export default ({ connectToServerAction, singleplayerAction, optionsAction, githubAction, discordAction, openFileAction }: Props) => {
const [versionStatus, setVersionStatus] = useState('')
const [versionTitle, setVersionTitle] = useState('')
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
setVersionStatus('(dev)')
} else {
fetch('./version.txt').then(async (f) => {
if (f.status === 404) return
const contents = await f.text()
setVersionStatus(`(${contents === process.env.BUILD_VERSION ? 'latest' : 'new version available'})`)
setVersionTitle(`Loaded: ${process.env.BUILD_VERSION}. Remote: ${contents}`)
}, () => { })
}
}, [])
return (
<div>
<div className={styles['game-title']}>
<div className={styles.minec}></div>
<div className={styles.raft}></div>
<div className={styles.edition}></div>
<span className={styles.splash}>Prismarine is a beautiful block</span>
</div>
<div className={styles.menu}>
<Button
onClick={connectToServerAction}
data-test-id='connect-screen-button'
>
Connect to server
</Button>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button
style={{ width: 170 }}
onClick={singleplayerAction}
data-test-id='singleplayer-button'
>
Singleplayer
</Button>
<Button
style={{ width: '20px' }}
data-test-id='select-file-folder'
icon='pixelarticons:folder'
onClick={openFileAction}
/>
</div>
<Button
onClick={optionsAction}
>
Options
</Button>
<div className={styles['menu-row']}>
<Button
style={{ width: '98px' }}
onClick={githubAction}
>
GitHub
</Button>
<Button
style={{ width: '98px' }}
onClick={discordAction}
>
Discord
</Button>
</div>
</div>
<div className={styles['bottom-info']}>
<span
title={`${versionTitle} (click to reload)`}
onClick={refreshApp}
>
Prismarine Web Client {versionStatus}
</span>
<span>A Minecraft client in the browser!</span>
</div>
</div>
)
}

View file

@ -0,0 +1,42 @@
import fs from 'fs'
import { useSnapshot } from 'valtio'
import { activeModalStack, miscUiState, openOptionsMenu, showModal } from '../globalState'
import { openURL } from '../menus/components/common'
import { fsState } from '../loadSave'
import { options } from '../optionsStorage'
import defaultLocalServerOptions from '../defaultLocalServerOptions'
import { openFilePicker } from '../utils'
import { openWorldDirectory } from '../browserfs'
import MainMenu from './MainMenu'
export default () => {
const haveModals = useSnapshot(activeModalStack).length
const { gameLoaded } = useSnapshot(miscUiState)
if (haveModals || gameLoaded) return
return <MainMenu
connectToServerAction={() => showModal(document.getElementById('play-screen'))}
singleplayerAction={() => {
fsState.isReadonly = false
fsState.syncFs = true
fsState.inMemorySave = true
const notFirstTime = fs.existsSync('./world/level.dat')
if (notFirstTime && !options.localServerOptions.version) {
options.localServerOptions.version = '1.16.1' // legacy version
} else {
options.localServerOptions.version ??= defaultLocalServerOptions.version
}
window.dispatchEvent(new window.CustomEvent('singleplayer', {}))
}}
githubAction={() => openURL(process.env.GITHUB_URL)}
optionsAction={() => openOptionsMenu('main')}
discordAction={() => openURL('https://discord.gg/4Ucm684Fq3')}
openFileAction={e => {
if (!!window.showDirectoryPicker && !e.shiftKey) {
openWorldDirectory()
} else {
openFilePicker()
}
}}
/>
}

View file

@ -0,0 +1,18 @@
import type { Meta, StoryObj } from '@storybook/react'
import OptionsGroup from './OptionsGroup'
const meta: Meta<typeof OptionsGroup> = {
component: OptionsGroup,
// render: () => <OptionsGroup />
}
export default meta
type Story = StoryObj<typeof OptionsGroup>;
export const Primary: Story = {
args: {
group: 'controls',
backButtonAction () { }
},
}

View file

@ -0,0 +1,38 @@
import { titleCase } from 'title-case'
import { OptionsGroupType, guiOptionsScheme, options } from '../optionsStorage'
import OptionsItems, { OptionMeta } from './OptionsItems'
const optionValueToType = (optionValue: any) => {
if (typeof optionValue === 'boolean') return 'toggle'
if (typeof optionValue === 'number') return 'slider'
if (typeof optionValue === 'string') return 'element'
}
const finalItemsScheme: Record<keyof typeof guiOptionsScheme, OptionMeta[]> = Object.fromEntries(Object.entries(guiOptionsScheme).map(([groupName, optionsArr]) => {
return [groupName, optionsArr.flatMap((optionsObj) => {
return Object.entries(optionsObj).map(([optionKey, metaMerge]) => {
const optionValue = options[optionKey]
const type = optionValueToType(optionValue)
const meta: OptionMeta = {
id: optionKey === 'custom' ? undefined : optionKey,
type,
// todo I don't like the whole idea of custom. Why it is even here?
...optionKey === 'custom' ? {
type: 'element',
render: metaMerge
} : {
...metaMerge,
}
}
return meta
})
})]
}))
export default ({ group, backButtonAction }: { group: OptionsGroupType, backButtonAction?}) => {
const items = finalItemsScheme[group]
const title = group === 'main' ? 'Settings' : `${titleCase(group)} Settings`
return <OptionsItems items={items} title={title} backButtonAction={backButtonAction} />
}

View file

@ -0,0 +1,93 @@
import { useSnapshot } from 'valtio'
import { noCase } from 'change-case'
import { titleCase } from 'title-case'
import { useMemo } from 'react'
import { options } from '../optionsStorage'
import Button from './Button'
import Slider from './Slider'
import Screen from './Screen'
type GeneralItem = {
id?: string
text?: string,
disabledReason?: string,
tooltip?: string
willHaveNoEffect?: boolean
}
export type OptionMeta = GeneralItem & ({
type: 'toggle',
} | {
type: 'slider'
min?: number,
max?: number,
valueText?: (value: number) => string,
unit?: string,
delayApply?: boolean,
} | {
type: 'element'
render: () => React.ReactNode,
})
export const OptionButton = ({ item }: { item: Extract<OptionMeta, { type: 'toggle' }> }) => {
const optionValue = useSnapshot(options)[item.id]
return <Button
label={`${item.text}: ${optionValue ? 'ON' : 'OFF'}`}
onClick={() => {
options[item.id] = !options[item.id]
}}
title={item.disabledReason ? `${item.disabledReason} | ${item.tooltip}` : item.tooltip}
disabled={!!item.disabledReason}
style={{
width: 150,
}}
/>
}
export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slider' }> }) => {
const optionValue = useSnapshot(options)[item.id]
const valueDisplay = useMemo(() => {
if (item.valueText) return item.valueText(optionValue)
return undefined // default display
}, [optionValue])
return <Slider label={item.text} value={options[item.id]} min={item.min} max={item.max} updateValue={(value) => {
options[item.id] = value
}} unit={item.unit} valueDisplay={valueDisplay} updateOnDragEnd={item.delayApply} />
}
const OptionElement = ({ item }: { item: Extract<OptionMeta, { type: 'element' }> }) => {
return item.render()
}
const RenderOption = ({ item }: { item: OptionMeta }) => {
if (item.id) {
item.text ??= titleCase(noCase(item.id))
}
if (item.type === 'toggle') return <OptionButton item={item} />
if (item.type === 'slider') return <OptionSlider item={item} />
if (item.type === 'element') return <OptionElement item={item} />
}
interface Props {
readonly items: OptionMeta[]
title: string
backButtonAction?: () => void
}
export default ({ items, title, backButtonAction }: Props) => {
return <Screen
title={title}
>
<div className='screen-items'>
{items.map((element, i) => {
// make sure its unique!
return <RenderOption key={element.id ?? `${title}-${i}`} item={element} />
})}
</div>
{backButtonAction && <Button onClick={() => backButtonAction()}>Back</Button>}
</Screen>
}

View file

@ -0,0 +1,12 @@
import { useSnapshot } from 'valtio'
import { activeModalStack, hideCurrentModal } from '../globalState'
import { OptionsGroupType } from '../optionsStorage'
import OptionsGroup from './OptionsGroup'
export default () => {
const { reactType } = useSnapshot(activeModalStack).at(-1) ?? {}
if (!reactType?.startsWith('options-')) return
const settingsGroup = reactType.slice('options-'.length) as OptionsGroupType
return <OptionsGroup group={settingsGroup} backButtonAction={hideCurrentModal} />
}

View file

@ -6,7 +6,9 @@ import Button from './Button'
const meta: Meta<typeof Screen> = {
component: Screen,
render: () => <Screen title='test'>
{Array.from({ length: 10 }).map((_, i) => <Button key={i}>test {i}</Button>)}
<div className="screen-items">
{Array.from({ length: 10 }).map((_, i) => <Button key={i} inScreen>test {i}</Button>)}
</div>
</Screen>
}

View file

@ -1,15 +1,19 @@
interface Props {
title: string
children: React.ReactNode
backdrop?: boolean
}
export default ({ title, children }: Props) => {
export default ({ title, children, backdrop = true }: Props) => {
return (
<div className='fullscreen' style={{ overflow: 'auto' }}>
<div className="screen-content">
<div className="screen-title">{title}</div>
{children}
<>
{backdrop && <div className="backdrop"></div>}
<div className='fullscreen' style={{ overflow: 'auto' }}>
<div className="screen-content">
<div className="screen-title">{title}</div>
{children}
</div>
</div>
</div>
</>
)
}

View file

@ -0,0 +1,30 @@
import type { Meta, StoryObj } from '@storybook/react'
import Slider from './Slider'
const meta: Meta<typeof Slider> = {
component: Slider,
args: {
label: 'hapiness',
value: 0,
updateValue (value) {
console.log('updateValue', value)
},
},
}
export default meta
type Story = StoryObj<typeof Slider>;
export const Primary: Story = {
args: {
updateOnDragEnd: true,
disabledReason: undefined,
},
}
export const Disabled: Story = {
args: {
disabledReason: 'you are not happy enough',
},
}

83
src/react/Slider.tsx Normal file
View file

@ -0,0 +1,83 @@
// Slider.tsx
import React, { useState, useEffect } from 'react'
import styles from './slider.module.css'
interface Props extends React.ComponentProps<'div'> {
label: string;
value: number;
unit?: string;
width?: number;
valueDisplay?: string | number;
min?: number;
max?: number;
disabledReason?: string;
updateValue?: (value: number) => void;
updateOnDragEnd?: boolean;
}
const Slider: React.FC<Props> = ({
label,
unit = '%',
width,
value: valueProp,
valueDisplay,
min = 1,
max = 100,
disabledReason,
updateOnDragEnd = false,
updateValue,
...divProps
}) => {
const [value, setValue] = useState(valueProp)
const getRatio = (v = value) => Math.max(Math.min((v - min) / (max - min), 1), 0)
const [ratio, setRatio] = useState(getRatio())
useEffect(() => {
setValue(valueProp)
}, [valueProp])
useEffect(() => {
setRatio(getRatio())
}, [value, min, max])
const fireValueUpdate = (dragEnd: boolean, v = value) => {
if (updateOnDragEnd !== dragEnd) return
updateValue?.(v)
}
return (
<div className={styles['slider-container']} style={{ width }} {...divProps}>
<input
type="range"
className={styles.slider}
min={min}
max={max}
value={value}
disabled={!!disabledReason}
onChange={(e) => {
const newValue = Number(e.target.value)
setValue(newValue)
fireValueUpdate(false, newValue)
}}
// todo improve correct handling of drag end
onLostPointerCapture={() => {
fireValueUpdate(true)
}}
onPointerUp={() => {
fireValueUpdate(true)
}}
onKeyUp={() => {
fireValueUpdate(true)
}}
/>
<div className={styles.disabled} title={disabledReason}></div>
<div className={styles['slider-thumb']} style={{ left: `calc((100% * ${ratio}) - (8px * ${ratio}))` }}></div>
<label className={styles.label}>
{label}: {valueDisplay ?? value} {unit}
</label>
</div>
)
}
export default Slider

View file

@ -0,0 +1,88 @@
.game-title {
position: absolute;
top: 30px;
left: calc(50% - 137px);
}
.game-title .minec {
display: block;
position: absolute;
top: 0;
left: 0;
background-image: var(--title-gui);
background-size: 256px;
width: 155px;
height: 44px;
}
.game-title .raft {
display: block;
position: absolute;
top: 0;
left: 155px;
background-image: var(--title-gui);
background-size: 256px;
width: 155px;
height: 44px;
background-position-y: -45px;
}
.game-title .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;
}

View file

@ -0,0 +1,9 @@
.wrapper {
display: flex;
flex-direction: row;
width: 100%;
gap: 0 10px;
height: 20px;
}

115
src/react/slider.module.css Normal file
View file

@ -0,0 +1,115 @@
.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:not(:disabled)~.slider-thumb {
--txrV: -86px;
}
.slider-container::after,
.slider-thumb::after {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 20px;
background: var(--widgets-gui-atlas);
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: var(--widgets-gui-atlas);
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:disabled {
cursor: not-allowed;
}
.slider~.disabled {
display: none;
}
/* .disabled after .slider selector */
.slider:disabled~.disabled {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
background: rgba(0, 0, 0, .5);
}
.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;
}

View file

@ -7,9 +7,11 @@ import { useSnapshot } from 'valtio'
import { QRCodeSVG } from 'qrcode.react'
import { createPortal } from 'react-dom'
import { contro } from './controls'
import { activeModalStack, isGameActive, miscUiState } from './globalState'
import { miscUiState } from './globalState'
import { options, watchValue } from './optionsStorage'
import DeathScreenProvider from './react/DeathScreenProvider'
import OptionsRenderApp from './react/OptionsRenderApp'
import MainMenuRenderApp from './react/MainMenuRenderApp'
// todo
useInterfaceState.setState({
@ -77,12 +79,6 @@ const TouchControls = () => {
)
}
function useIsBotAvailable () {
const stack = useSnapshot(activeModalStack)
return isGameActive(false)
}
const Portal = ({ children, to }) => {
return createPortal(children, to)
}
@ -112,17 +108,27 @@ const DisplayQr = () => {
</div>
}
const App = () => {
const isBotAvailable = useIsBotAvailable()
if (!isBotAvailable) return null
const InGameUi = () => {
const { gameLoaded } = useSnapshot(miscUiState)
if (!gameLoaded) return
return <div>
return <>
<Portal to={document.querySelector('#ui-root')}>
{/* apply scaling */}
<DeathScreenProvider />
</Portal>
<DisplayQr />
<TouchControls />
</>
}
const App = () => {
return <div>
<InGameUi />
<Portal to={document.querySelector('#ui-root')}>
<OptionsRenderApp />
<MainMenuRenderApp />
</Portal>
</div>
}

43
src/screens.css Normal file
View file

@ -0,0 +1,43 @@
/* screen styles eg main menu and options are screens */
.title, .screen-title {
font-size: 10px;
color: white;
text-align: center;
text-shadow: 1px 1px #222;
}
.screen-title {
margin-bottom: 10px;
}
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
}
.fullscreen {
position: fixed;
inset: 0;
display: flex;
justify-content: center;
}
.screen-content {
display: flex;
flex-direction: column;
margin-top: 10px;
/* todo I'm not sure about it */
/* margin-top: calc(100% / 6 - 16px); */
width: 310px;
align-items: center;
gap: 10px;
}
.screen-items {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 10px;
}

View file

@ -1,3 +1,5 @@
@import url(./screens.css);
:root {
--guiScaleFactor: 3;
--guiScale: 3;
@ -21,6 +23,7 @@ html {
height: 100vh;
overflow: hidden;
--widgets-gui-atlas: url('minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png');
--title-gui: url('minecraft-assets/minecraft-assets/data/1.17.1/gui/title/minecraft.png');
color: white;
}
@ -121,7 +124,7 @@ body {
}
}
@media only screen and (max-width: 561px) {
@media only screen and (max-width: 590px) {
#ui-root {
transform: scale(1);
width: calc(100% / 1);

View file

@ -5,10 +5,10 @@ import type { Viewer } from 'prismarine-viewer/viewer/lib/viewer'
import { subscribeKey } from 'valtio/utils'
import { proxy, ref } from 'valtio'
import blocksFileNames from '../generated/blocks.json'
import { showNotification } from './menus/notification'
import type { BlockStates } from './inventory'
import { removeFileRecursiveAsync } from './browserfs'
import { setLoadingScreenStatus } from './utils'
import { showNotification } from './globalState'
export const resourcePackState = proxy({
resourcePackInstalled: false,

View file

@ -1,7 +1,4 @@
import * as crypto from 'crypto'
import UUID from 'uuid-1345'
import { activeModalStack, hideModal, miscUiState, showModal } from './globalState'
import { notification } from './menus/notification'
import { activeModalStack, hideModal, miscUiState, notification, showModal } from './globalState'
import { options } from './optionsStorage'
import { saveWorld } from './builtinCommands'
import { openWorldZip } from './browserfs'
@ -118,20 +115,6 @@ export const isCypress = () => {
return localStorage.cypress === 'true'
}
// https://github.com/PrismarineJS/node-minecraft-protocol/blob/cf1f67117d586b5e6e21f0d9602da12e9fcf46b6/src/server/login.js#L170
function javaUUID (s: string) {
const hash = crypto.createHash('md5')
hash.update(s, 'utf8')
const buffer = hash.digest()
buffer[6] = (buffer[6] & 0x0f) | 0x30
buffer[8] = (buffer[8] & 0x3f) | 0x80
return buffer
}
export function nameToMcOfflineUUID (name) {
return (new UUID(javaUUID('OfflinePlayer:' + name))).toString()
}
export const setLoadingScreenStatus = function (status: string | undefined, isError = false, hideDots = false) {
const loadingScreen = document.getElementById('loading-error-screen')
@ -179,10 +162,10 @@ export const setRenderDistance = () => {
}
prevRenderDistance = options.renderDistance
}
export const reloadChunks = () => {
export const reloadChunks = async () => {
if (!worldView) return
setRenderDistance()
worldView.updatePosition(bot.entity.position, true)
await worldView.updatePosition(bot.entity.position, true)
}
export const openFilePicker = (specificCase?: 'resourcepack') => {

View file

@ -6,6 +6,13 @@ import { reloadChunks } from './utils'
subscribeKey(options, 'renderDistance', reloadChunks)
watchValue(options, o => {
document.documentElement.style.setProperty('--chatScale', `${o.chatScale / 100}`)
document.documentElement.style.setProperty('--chatWidth', `${o.chatWidth}px`)
document.documentElement.style.setProperty('--chatHeight', `${o.chatHeight}px`)
document.documentElement.style.setProperty('--guiScale', `${o.guiScale}`)
})
export const watchOptionsAfterViewerInit = () => {
watchValue(options, o => {
viewer.world.showChunkBorders = o.showChunkBorders

View file

@ -7,7 +7,7 @@
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"noEmit": true,
"resolveJsonModule": true,
"resolveJsonModule": false,
"strictFunctionTypes": true,
"noImplicitAny": false,
"noFallthroughCasesInSwitch": true,
@ -22,6 +22,7 @@
},
"include": [
"src",
"cypress"
"cypress",
"prismarine-viewer/viewer"
]
}