diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cebea32..fe7c8ba6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/cypress/integration/index.spec.ts b/cypress/integration/index.spec.ts index 4ad44728..4db13ed2 100644 --- a/cypress/integration/index.spec.ts +++ b/cypress/integration/index.spec.ts @@ -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() }) diff --git a/index.html b/index.html index c81891b8..7f8efd2e 100644 --- a/index.html +++ b/index.html @@ -44,9 +44,6 @@ - - - diff --git a/package.json b/package.json index 3a7df837..20e98b82 100644 --- a/package.json +++ b/package.json @@ -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" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e449f3e..9b93e455 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/scripts/esbuildPlugins.mjs b/scripts/esbuildPlugins.mjs index af6d296c..ed5a092f 100644 --- a/scripts/esbuildPlugins.mjs +++ b/scripts/esbuildPlugins.mjs @@ -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, diff --git a/src/basicSounds.ts b/src/basicSounds.ts new file mode 100644 index 00000000..cceba38c --- /dev/null +++ b/src/basicSounds.ts @@ -0,0 +1,43 @@ +import { options } from './optionsStorage' + +let audioContext: AudioContext +const sounds: Record = {} + +// 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) +} diff --git a/src/chat.js b/src/chat.js index aa7b362e..3aa6abec 100644 --- a/src/chat.js +++ b/src/chat.js @@ -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' diff --git a/src/dragndrop.ts b/src/dragndrop.ts index 68f4288c..2ad929ae 100644 --- a/src/dragndrop.ts +++ b/src/dragndrop.ts @@ -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 diff --git a/src/flyingSquidUtils.ts b/src/flyingSquidUtils.ts new file mode 100644 index 00000000..d4a0f489 --- /dev/null +++ b/src/flyingSquidUtils.ts @@ -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() +} diff --git a/src/globalState.ts b/src/globalState.ts index c8887794..559ae6e1 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -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} */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 diff --git a/src/globals.d.ts b/src/globals.d.ts index b1b2fd57..770020c6 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -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 diff --git a/src/index.ts b/src/index.ts index 20ab0e6e..eecb04e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 diff --git a/src/loadSave.ts b/src/loadSave.ts index 6c06cad7..1a09437a 100644 --- a/src/loadSave.ts +++ b/src/loadSave.ts @@ -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, diff --git a/src/menus/advanced_options_screen.js b/src/menus/advanced_options_screen.js deleted file mode 100644 index ce7762de..00000000 --- a/src/menus/advanced_options_screen.js +++ /dev/null @@ -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` -
- -

Advanced Options

-
-
- { - options.alwaysShowMobileControls = !options.alwaysShowMobileControls - } - }> - - openURL('https://gist.github.com/zardoy/6e5ce377d2b4c1e322e660973da069cd')}> -
-
- { - const newVal = e.target.value - options.frameLimit = newVal > this.frameLimitMax ? false : newVal - this.requestUpdate() - }}> - { - const rate = await getScreenRefreshRate() - this.frameLimitMax = rate - this.requestUpdate() - }}> -
-
- { - options.touchButtonsSize = +e.target.value - }}> - { - options.highPerformanceGpu = !options.highPerformanceGpu - }}> -
- - hideCurrentModal()}> -
- ` - } -} - -window.customElements.define('pmui-advanced-optionsscreen', AdvancedOptionsScreen) diff --git a/src/menus/components/button.js b/src/menus/components/button.js index 4e755388..1f726821 100644 --- a/src/menus/components/button.js +++ b/src/menus/components/button.js @@ -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} */ -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 () { diff --git a/src/menus/notification.js b/src/menus/notification.js index 4982fd6f..0ac6db16 100644 --- a/src/menus/notification.js +++ b/src/menus/notification.js @@ -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} */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 () { diff --git a/src/menus/options_screen.js b/src/menus/options_screen.js deleted file mode 100644 index 208ed0ca..00000000 --- a/src/menus/options_screen.js +++ /dev/null @@ -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` -
- -

Options

- -
-
- { - options.mouseSensX = +e.target.value - }}> - { - options.mouseSensY = +e.target.value - }}> -
-
- { - options.chatWidth = +e.target.value - }}> - { - options.chatHeight = +e.target.value - }}> -
-
- { - options.chatScale = +e.target.value - }}> - { - options.volume = +e.target.value - }}> -
-
- showModal(document.getElementById('keybinds-screen'))}> - { - options.guiScale = +e.target.value - }}> -
-
- { - options.renderDistance = +e.target.value - }}> - { - options.fov = +e.target.value - }}> -
- -
- { - showModal(document.querySelector('pmui-advanced-optionsscreen')) - } - }> - { - options.mouseRawInput = !options.mouseRawInput - } - }> -
-
- { - options.autoFullScreen = !options.autoFullScreen - } - }> - - { - options.autoExitFullscreen = !options.autoExitFullscreen - } - }> -
-
- { - 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') - } - }}> -
- - hideCurrentModal()}> -
- ` - } -} - -window.customElements.define('pmui-optionsscreen', OptionsScreen) diff --git a/src/menus/pause_screen.js b/src/menus/pause_screen.js index 10d7860d..964bda9f 100644 --- a/src/menus/pause_screen.js +++ b/src/menus/pause_screen.js @@ -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 { openURL(process.env.GITHUB_URL)}> openURL('https://discord.gg/4Ucm684Fq3')}> - showModal(document.getElementById('options-screen'))}> + openOptionsMenu('main')}> ${joinButton ? html` diff --git a/src/menus/play_screen.js b/src/menus/play_screen.js index 9f12d868..058ed624 100644 --- a/src/menus/play_screen.js +++ b/src/menus/play_screen.js @@ -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) diff --git a/src/menus/title_screen.js b/src/menus/title_screen.js deleted file mode 100644 index 6847b01f..00000000 --- a/src/menus/title_screen.js +++ /dev/null @@ -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` -
-
-
-
- Prismarine is a beautiful block -
- - - -
- Prismarine Web Client ${this.versionStatus} - A Minecraft client in the browser! -
- ` - } -} - -window.customElements.define('pmui-titlescreen', TitleScreen) diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts deleted file mode 100644 index 427c5f3e..00000000 --- a/src/optionsStorage.ts +++ /dev/null @@ -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: (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 = >(proxy: T, callback: (p: T) => void) => void - -export const watchValue: WatchValue = (proxy, callback) => { - const watchedProps = new Set() - 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) -} diff --git a/src/optionsStorage.tsx b/src/optionsStorage.tsx new file mode 100644 index 00000000..cf58b676 --- /dev/null +++ b/src/optionsStorage.tsx @@ -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: (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 } & { custom?}> +} = { + render: [ + { + renderDistance: { + unit: '', + min: 2, + max: 16 + }, + }, + { + custom () { + const frameLimitValue = useSnapshot(options).frameLimit + const [frameLimitMax, setFrameLimitMax] = useState(null as number | null) + + return
+ { + options.frameLimit = newVal > frameLimitMax ? false : newVal + }} /> +
+ } + }, + { + highPerformanceGpu: { + text: 'Use Dedicated GPU', + // willHaveNoEffect: isIos + }, + }, + { + custom () { + return +
+ + +
+ +
+ + +
+ + +
+ + Prismarine Web Client {versionStatus} + + A Minecraft client in the browser! +
+ + ) +} diff --git a/src/react/MainMenuRenderApp.tsx b/src/react/MainMenuRenderApp.tsx new file mode 100644 index 00000000..28b472f8 --- /dev/null +++ b/src/react/MainMenuRenderApp.tsx @@ -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 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() + } + }} + /> +} diff --git a/src/react/OptionsGroup.stories.tsx b/src/react/OptionsGroup.stories.tsx new file mode 100644 index 00000000..bc1aed23 --- /dev/null +++ b/src/react/OptionsGroup.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import OptionsGroup from './OptionsGroup' + +const meta: Meta = { + component: OptionsGroup, + // render: () => +} + +export default meta +type Story = StoryObj; + +export const Primary: Story = { + args: { + group: 'controls', + backButtonAction () { } + }, +} diff --git a/src/react/OptionsGroup.tsx b/src/react/OptionsGroup.tsx new file mode 100644 index 00000000..dc5a8cb7 --- /dev/null +++ b/src/react/OptionsGroup.tsx @@ -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 = 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 +} diff --git a/src/react/OptionsItems.tsx b/src/react/OptionsItems.tsx new file mode 100644 index 00000000..332b6d36 --- /dev/null +++ b/src/react/OptionsItems.tsx @@ -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 }) => { + const optionValue = useSnapshot(options)[item.id] + + return } + +} diff --git a/src/react/OptionsRenderApp.tsx b/src/react/OptionsRenderApp.tsx new file mode 100644 index 00000000..c1eef1b2 --- /dev/null +++ b/src/react/OptionsRenderApp.tsx @@ -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 +} diff --git a/src/react/Screen.stories.tsx b/src/react/Screen.stories.tsx index 45bc3b5d..1268c1f3 100644 --- a/src/react/Screen.stories.tsx +++ b/src/react/Screen.stories.tsx @@ -6,7 +6,9 @@ import Button from './Button' const meta: Meta = { component: Screen, render: () => - {Array.from({ length: 10 }).map((_, i) => )} +
+ {Array.from({ length: 10 }).map((_, i) => )} +
} diff --git a/src/react/Screen.tsx b/src/react/Screen.tsx index 029601f1..a6241fe6 100644 --- a/src/react/Screen.tsx +++ b/src/react/Screen.tsx @@ -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 ( -
-
-
{title}
- {children} + <> + {backdrop &&
} +
+
+
{title}
+ {children} +
-
+ ) } diff --git a/src/react/Slider.stories.tsx b/src/react/Slider.stories.tsx new file mode 100644 index 00000000..5ad55fca --- /dev/null +++ b/src/react/Slider.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import Slider from './Slider' + +const meta: Meta = { + component: Slider, + args: { + label: 'hapiness', + value: 0, + updateValue (value) { + console.log('updateValue', value) + }, + }, +} + +export default meta +type Story = StoryObj; + +export const Primary: Story = { + args: { + updateOnDragEnd: true, + disabledReason: undefined, + }, +} + +export const Disabled: Story = { + args: { + disabledReason: 'you are not happy enough', + }, +} diff --git a/src/react/Slider.tsx b/src/react/Slider.tsx new file mode 100644 index 00000000..b78d3cf4 --- /dev/null +++ b/src/react/Slider.tsx @@ -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 = ({ + 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 ( +
+ { + 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) + }} + /> +
+
+ +
+ ) +} + +export default Slider diff --git a/src/react/mainMenu.module.css b/src/react/mainMenu.module.css new file mode 100644 index 00000000..dd7276a2 --- /dev/null +++ b/src/react/mainMenu.module.css @@ -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; +} diff --git a/src/react/options.module.css b/src/react/options.module.css new file mode 100644 index 00000000..5487273b --- /dev/null +++ b/src/react/options.module.css @@ -0,0 +1,9 @@ + + +.wrapper { + display: flex; + flex-direction: row; + width: 100%; + gap: 0 10px; + height: 20px; +} diff --git a/src/react/slider.module.css b/src/react/slider.module.css new file mode 100644 index 00000000..d568dd4a --- /dev/null +++ b/src/react/slider.module.css @@ -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; +} diff --git a/src/reactUi.jsx b/src/reactUi.jsx index e2334383..7d103de2 100644 --- a/src/reactUi.jsx +++ b/src/reactUi.jsx @@ -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 = () => {
} -const App = () => { - const isBotAvailable = useIsBotAvailable() - if (!isBotAvailable) return null +const InGameUi = () => { + const { gameLoaded } = useSnapshot(miscUiState) + if (!gameLoaded) return - return
+ return <> {/* apply scaling */} + +} + +const App = () => { + return
+ + + + +
} diff --git a/src/screens.css b/src/screens.css new file mode 100644 index 00000000..ebf37c96 --- /dev/null +++ b/src/screens.css @@ -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; +} diff --git a/src/styles.css b/src/styles.css index c2b6f972..472e022d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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); diff --git a/src/texturePack.ts b/src/texturePack.ts index 805c9a05..81f75043 100644 --- a/src/texturePack.ts +++ b/src/texturePack.ts @@ -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, diff --git a/src/utils.ts b/src/utils.ts index 5a08ce88..26891ef6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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') => { diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 026c43cb..1d505952 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -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 diff --git a/tsconfig.json b/tsconfig.json index 1665bf34..68f59ef0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" ] }