diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b08045be2..0663fce70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,7 @@ jobs: - run: pnpm check-build - run: pnpm test-unit - run: pnpm lint + - run: pnpm tsx scripts/buildNpmReact.ts - run: nohup pnpm prod-start & - run: nohup pnpm test-mc-server & - uses: cypress-io/github-action@v5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cbc09b432..79bc5ffdf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,9 +33,16 @@ jobs: pnpx zardoy-release node --footer "This release URL: ${{ steps.deploy.outputs.stdout }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: | + pnpx zardoy-release npm + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: cp vercel.json .vercel/output/static/vercel.json - uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: .vercel/output/static force_orphan: true + - run: pnpm tsx scripts/buildNpmReact.ts + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index c44348775..240b751ae 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ package-lock.json Thumbs.db build localSettings.mjs -dist +dist* .DS_Store .idea/ world @@ -17,3 +17,5 @@ out .vercel generated storybook-static + +src/react/npmReactComponents.ts diff --git a/README.MD b/README.MD index 7a93ce7e3..1b8b84ac3 100644 --- a/README.MD +++ b/README.MD @@ -4,7 +4,7 @@ A true Minecraft client running in your browser! A port of the original game to the web, written in JavaScript using modern web technologies. -This project is a work in progress, but I consider it to be usable. If you encounter any bugs or usability issues, please report them! +If you encounter any bugs or usability issues, please report them! You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.gg) (short link) [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy. Every commit from the `develop` (default) branch is deployed to [s.mcraft.fun](https://s.mcraft.fun/) - so it's usually newer, but might be less stable. @@ -19,6 +19,8 @@ You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm. - Resource pack support - even even more! +All components that are in [Storybook](https://mcraft.fun/storybook) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react) + ### Recommended Settings - Controls -> **Raw Input** -> **On** - This will make the controls more precise @@ -65,7 +67,6 @@ To open the console, press `F12`, or if you are on mobile, you can type `#debug` It should be easy to build/start the project locally. See [CONTRIBUTING.MD](./CONTRIBUTING.md) for more info. -There is storybook for fast UI development. Run `pnpm storybook` to start it. There is world renderer playground ([link](https://mcon.vercel.app/playground.html)). However, there are many things that can be done in online version. You can access some global variables in the console and useful examples: diff --git a/README.NPM.MD b/README.NPM.MD new file mode 100644 index 000000000..24c90bc98 --- /dev/null +++ b/README.NPM.MD @@ -0,0 +1,32 @@ +# Minecraft React + +```bash +yarn add minecraft-react +``` + +## Usage + +```jsx +import { Scoreboard } from 'minecraft-react' + +const App = () => { + return ( + + ) +} +``` + +See [Storybook](https://mcraft.fun/storybook/) or [Storybook (Mirror link)](https://mcon.vercel.app/storybook/) for more examples and full components list. Also take a look at the full [standalone example](https://github.com/zardoy/prismarine-web-client/tree/experiments/UiStandaloneExample.tsx). + +There are two types of components: + +- Small UI components or HUD components +- Full screen components (like sign editor, worlds selector) diff --git a/experiments/UiStandaloneExample.tsx b/experiments/UiStandaloneExample.tsx new file mode 100644 index 000000000..80f68e8d6 --- /dev/null +++ b/experiments/UiStandaloneExample.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react' +import { createRoot } from 'react-dom/client' +import { + Button, + Slider, + ArmorBar, + BreathBar, + Chat, + HealthBar, + PlayerListOverlay, + Scoreboard, + MessageFormattedString, + XPBar, + FoodBar +} from '../dist-npm' + +const ExampleDemo = () => { + const [sliderValue, setSliderValue] = useState(0) + + return ( +
+ + setSliderValue(value)} /> + + { + console.log('typed', message) + // close + }} + /> + + + + + "§bRed" displays as + + +
+ ) +} + +createRoot(document.body as Element).render() diff --git a/package.json b/package.json index d1f4c7eba..25a874725 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,12 @@ "web", "client" ], - "author": "PrismarineJS", + "publish": { + "preset": { + "publishOnlyIfChanged": true, + "runBuild": false + } + }, "license": "MIT", "dependencies": { "@dimaka/interface": "0.0.3-alpha.0", @@ -48,6 +53,7 @@ "adm-zip": "^0.5.12", "browserfs": "github:zardoy/browserfs#build", "change-case": "^5.1.2", + "classnames": "^2.5.1", "compression": "^1.7.4", "cors": "^2.8.5", "cypress-plugin-snapshots": "^1.4.4", @@ -78,11 +84,15 @@ "react-dom": "^18.2.0", "react-transition-group": "^4.4.5", "remark": "^15.0.1", + "filesize": "^10.0.12", "sanitize-filename": "^1.6.3", "skinview3d": "^3.0.1", "source-map-js": "^1.0.2", "stats-gl": "^1.0.5", "stats.js": "^0.17.0", + "use-typed-event-listener": "^4.0.2", + "mojangson": "^2.0.4", + "prosemirror-menu": "^1.2.4", "tabbable": "^6.2.0", "title-case": "3.x", "ua-parser-js": "^1.0.37", @@ -113,7 +123,6 @@ "eslint": "^8.50.0", "eslint-config-zardoy": "^0.2.17", "events": "^3.3.0", - "filesize": "^10.0.12", "http-browserify": "^1.7.0", "http-server": "^14.1.1", "https-browserify": "^1.0.0", @@ -132,7 +141,6 @@ "three": "0.154.0", "timers-browserify": "^2.0.12", "typescript": "5.5.0-beta", - "use-typed-event-listener": "^4.0.2", "vitest": "^0.34.6", "yaml": "^2.3.2" }, diff --git a/package.npm.json b/package.npm.json new file mode 100644 index 000000000..bae8b60f0 --- /dev/null +++ b/package.npm.json @@ -0,0 +1,36 @@ +{ + "name": "minecraft-react", + "description": "A Minecraft-like React UI library", + "keywords": [ + "minecraft", + "minecraft style" + ], + "license": "MIT", + "sideEffects": false, + "files": [ + "**" + ], + "exports": { + ".": { + "default": "./dist/react/npmReactComponents.js", + "types": "./dist/react/npmReactComponents.d.ts" + }, + "./*": { + "default": "./dist/react/*", + "types": "./dist/react/*" + }, + "./dist": { + "default": "./dist/*", + "types": "./dist/*" + } + }, + "module": "./dist/react/npmReactComponents.js", + "types": "./dist/react/npmReactComponents.d.ts", + "repository": "zardoy/prismarine-web-client", + "version": "0.0.0-dev", + "dependencies": {}, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d1b69cc2..054539d52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: change-case: specifier: ^5.1.2 version: 5.1.2 + classnames: + specifier: ^2.5.1 + version: 2.5.1 compression: specifier: ^1.7.4 version: 1.7.4 @@ -92,6 +95,9 @@ importers: express: specifier: ^4.18.2 version: 4.18.2 + filesize: + specifier: ^10.0.12 + version: 10.0.12 flying-squid: specifier: npm:@zardoy/flying-squid@^0.0.19 version: '@zardoy/flying-squid@0.0.19(encoding@0.1.13)' @@ -116,6 +122,9 @@ importers: minecraft-data: specifier: 3.62.0 version: 3.62.0 + mojangson: + specifier: ^2.0.4 + version: 2.0.4 net-browserify: specifier: github:zardoy/prismarinejs-net-browserify version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/7d827dba61bd2f9ac9a6086fe2079a0fccadd070 @@ -137,6 +146,9 @@ importers: prosemirror-markdown: specifier: ^1.12.0 version: 1.12.0 + prosemirror-menu: + specifier: ^1.2.4 + version: 1.2.4 prosemirror-state: specifier: ^1.4.3 version: 1.4.3 @@ -182,6 +194,9 @@ importers: ua-parser-js: specifier: ^1.0.37 version: 1.0.37 + use-typed-event-listener: + specifier: ^4.0.2 + version: 4.0.2(react@18.2.0)(typescript@5.5.0-beta) valtio: specifier: ^1.11.1 version: 1.11.2(@types/react@18.2.20)(react@18.2.0) @@ -262,9 +277,6 @@ importers: events: specifier: ^3.3.0 version: 3.3.0 - filesize: - specifier: ^10.0.12 - version: 10.0.12 http-browserify: specifier: ^1.7.0 version: 1.7.0 @@ -319,9 +331,6 @@ importers: typescript: specifier: 5.5.0-beta version: 5.5.0-beta - use-typed-event-listener: - specifier: ^4.0.2 - version: 4.0.2(react@18.2.0)(typescript@5.5.0-beta) vitest: specifier: ^0.34.6 version: 0.34.6(terser@5.19.2) @@ -3635,8 +3644,8 @@ packages: cipher-base@1.0.4: resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} - classnames@2.3.2: - resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} clean-regexp@1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} @@ -9968,7 +9977,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.2 - '@types/node': 20.8.0 + '@types/node': 20.12.8 '@types/yargs': 17.0.28 chalk: 4.1.2 @@ -11459,11 +11468,11 @@ snapshots: '@types/cors@2.8.15': dependencies: - '@types/node': 20.8.0 + '@types/node': 20.12.8 '@types/cross-spawn@6.0.3': dependencies: - '@types/node': 20.8.10 + '@types/node': 20.12.8 '@types/debug@4.1.12': dependencies: @@ -11512,7 +11521,7 @@ snapshots: '@types/graceful-fs@4.1.7': dependencies: - '@types/node': 20.8.10 + '@types/node': 20.12.8 '@types/http-cache-semantics@4.0.2': {} @@ -11621,7 +11630,7 @@ snapshots: '@types/resolve@1.17.1': dependencies: - '@types/node': 20.8.10 + '@types/node': 20.12.8 '@types/sat@0.0.31': {} @@ -11632,7 +11641,7 @@ snapshots: '@types/send@0.17.2': dependencies: '@types/mime': 1.3.3 - '@types/node': 20.8.10 + '@types/node': 20.12.8 '@types/serve-static@1.15.3': dependencies: @@ -11941,7 +11950,7 @@ snapshots: '@zardoy/react-util@0.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - classnames: 2.3.2 + classnames: 2.5.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -12674,7 +12683,7 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 - classnames@2.3.2: {} + classnames@2.5.1: {} clean-regexp@1.0.0: dependencies: @@ -13373,7 +13382,7 @@ snapshots: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.15 - '@types/node': 20.8.0 + '@types/node': 20.12.8 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.4.2 @@ -14976,7 +14985,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.7 - '@types/node': 20.8.0 + '@types/node': 20.12.8 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -14993,7 +15002,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.8.0 + '@types/node': 20.12.8 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.11 @@ -15001,13 +15010,13 @@ snapshots: jest-worker@26.6.2: dependencies: - '@types/node': 20.8.10 + '@types/node': 20.12.8 merge-stream: 2.0.0 supports-color: 7.2.0 jest-worker@29.7.0: dependencies: - '@types/node': 20.8.10 + '@types/node': 20.12.8 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 diff --git a/scripts/buildNpmReact.ts b/scripts/buildNpmReact.ts new file mode 100644 index 000000000..71513cb3a --- /dev/null +++ b/scripts/buildNpmReact.ts @@ -0,0 +1,148 @@ +import fs from 'fs' +import path from 'path' +import { build, transform } from 'esbuild' +import { execSync } from 'child_process' +// import { copy } from 'fs-extra' +import { glob } from 'glob' + +const isAbsolute = (path: string) => path.startsWith('/') || /^[A-Z]:/i.test(path) + +fs.promises.readdir(path.resolve(__dirname, '../src/react')).then(async (files) => { + const components = files + .filter((file) => { + if (file.startsWith('Concept')) return false + return file.endsWith('.stories.tsx'); + }) + .map((file) => { + return file.replace('.stories.tsx', '') + }) + + const content = components.map((component) => { + return `export { default as ${component} } from './${component}'` + }).join('\n') + + await fs.promises.writeFile( + path.resolve(__dirname, '../src/react/npmReactComponents.ts'), + content + ) + + execSync('pnpm tsc -p tsconfig.npm.json', { + cwd: path.resolve(__dirname, '../'), + stdio: 'inherit', + }) + + const packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.npm.json'), 'utf-8')) + const packageJsonRoot = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8')) + const external = Object.keys(packageJson.peerDependencies) + const dependencies = new Set() + const version = packageJsonRoot.version + packageJson.version = version + + const externalize = ['minecraft-assets', 'prismarine-viewer'] + const { metafile } = await build({ + entryPoints: [path.resolve(__dirname, '../src/react/npmReactComponents.ts')], + bundle: true, + outfile: path.resolve(__dirname, '../dist-npm/bundle.esm.js'), + format: 'esm', + platform: 'browser', + target: 'es2020', + external: external, + metafile: true, + minify: true, + write: false, // todo + loader: { + '.png': 'dataurl', + }, + plugins: [ + // on external module resolve + { + name: 'collect-imports', + setup (build) { + build.onResolve({ filter: /.*/ }, (args) => { + if (args.importer.includes('node_modules') || external.some(x => args.path.startsWith(x)) || isAbsolute(args.path)) { + return undefined + } + if (args.path.startsWith('./') || args.path.startsWith('../')) { + if (args.path.endsWith('.png') || args.path.endsWith('.css') || args.path.endsWith('.jpg') || args.path.endsWith('.jpeg')) { + const absoluteImporting = path.join(path.dirname(args.importer), args.path) + const absoluteRoot = path.resolve(__dirname, '../src') + const relativeToRoot = path.relative(absoluteRoot, absoluteImporting) + fs.copyFileSync(absoluteImporting, path.resolve(__dirname, '../dist-npm/dist-pre', relativeToRoot)) + } + // default behavior + return undefined + } + const dep = args.path.startsWith('@') ? args.path.split('/').slice(0, 2).join('/') : args.path.split('/')[0] + if (!dependencies.has(dep)) { + dependencies.add(dep) + console.log('Adding dependency:', dep, 'from', args.importer) + } + // return { external: true } + }) + }, + }, + ], + }) + for (const dependency of dependencies) { + if (externalize.includes(dependency)) continue + if (!packageJsonRoot.dependencies[dependency]) throw new Error(`Dependency ${dependency} not found in package.json`) + packageJson.dependencies[dependency] = packageJsonRoot.dependencies[dependency] + } + fs.writeFileSync(path.resolve(__dirname, '../dist-npm/package.json'), JSON.stringify(packageJson, null, 2)) + // fs.promises.writeFile('./dist-npm/metafile.json', JSON.stringify(metafile, null, 2)) + + await build({ + entryPoints: ['dist-npm/dist-pre/**/*.js'], + outdir: 'dist-npm/dist', + // allowOverwrite: true, + jsx: 'preserve', + bundle: true, + target: 'esnext', + platform: 'browser', + format: 'esm', + loader: { + '.css': 'copy', + '.module.css': 'copy', + '.png': 'copy', + }, + minifyWhitespace: false, + logOverride: { + // 'ignored-bare-import': "info" + }, + plugins: [ + { + name: 'all-external', + setup (build) { + build.onResolve({ filter: /.*/ }, (args) => { + // todo use workspace deps + if (externalize.some(x => args.path.startsWith(x))) { + return undefined // bundle + } + if (args.path.endsWith('.css') || args.path.endsWith('.png') || args.path.endsWith('.jpg') || args.path.endsWith('.jpeg')) { + return undefined // loader action + } + return { + path: args.path, + external: true, + } + }) + }, + } + ], + }) + + const paths = await glob('dist-npm/dist-pre/**/*.d.ts') + // copy to dist + for (const p of paths) { + const relative = path.relative('dist-npm/dist-pre', p) + const target = path.resolve('dist-npm/dist', relative) + fs.copyFileSync(p, target) + } + // rm dist-pre + fs.rmSync('dist-npm/dist-pre', { recursive: true }) + fs.copyFileSync(path.resolve(__dirname, '../README.NPM.MD'), path.resolve(__dirname, '../dist-npm/README.md')) + + if (version !== '0.0.0-dev') { + execSync('npm publish', { cwd: path.resolve(__dirname, '../dist-npm') }) + } +}) diff --git a/src/botUtils.test.ts b/src/botUtils.test.ts index 915311384..99aa07b45 100644 --- a/src/botUtils.test.ts +++ b/src/botUtils.test.ts @@ -2,8 +2,8 @@ import { test, expect } from 'vitest' import mcData from 'minecraft-data' import { formatMessage } from './botUtils' -globalThis.window ??= {} as any -globalThis.window.loadedData ??= mcData('1.20.1') +//@ts-expect-error +globalThis.loadedData ??= mcData('1.20.1') const mapIncludeDefined = (props) => { return (x) => { diff --git a/src/botUtils.ts b/src/botUtils.ts index a76f889b3..fb4000a19 100644 --- a/src/botUtils.ts +++ b/src/botUtils.ts @@ -1,6 +1,7 @@ // this should actually be moved to mineflayer / prismarine-viewer import { fromFormattedString, TextComponent } from '@xmcl/text-component' +import type { IndexedData } from 'minecraft-data' export type MessageFormatPart = Pick & { text: string @@ -26,8 +27,10 @@ type MessageInput = { json?: any } -// todo move to sign-renderer, replace with prismarine-chat -export const formatMessage = (message: MessageInput) => { +const global = globalThis as any + +// todo move to sign-renderer, replace with prismarine-chat, fix mcData issue! +export const formatMessage = (message: MessageInput, mcData: IndexedData = global.loadedData) => { let msglist: MessageFormatPart[] = [] const readMsg = (msg: MessageInput) => { @@ -47,7 +50,7 @@ export const formatMessage = (message: MessageInput) => { ...styles }) } else if (msg.translate) { - const tText = window.loadedData.language[msg.translate] ?? msg.translate + const tText = mcData?.language[msg.translate] ?? msg.translate if (msg.with) { const splitted = tText.split(/%s|%\d+\$s/g) @@ -114,6 +117,6 @@ const blockToItemRemaps = { } export const getItemFromBlock = (block: import('prismarine-block').Block) => { - const item = loadedData.itemsByName[blockToItemRemaps[block.name] ?? block.name] + const item = global.mcData.itemsByName[blockToItemRemaps[block.name] ?? block.name] return item } diff --git a/src/controls.ts b/src/controls.ts index 264050829..f5e18ae71 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -10,7 +10,7 @@ import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCur import { goFullscreen, pointerLock, reloadChunks } from './utils' import { options } from './optionsStorage' import { openPlayerInventory } from './inventoryWindows' -import { chatInputValueGlobal } from './react/ChatContainer' +import { chatInputValueGlobal } from './react/Chat' import { fsState } from './loadSave' import { showOptionsModal } from './react/SelectOption' import widgets from './react/widgets' diff --git a/src/entities.ts b/src/entities.ts index 3e075f718..dbb59bed2 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -58,7 +58,11 @@ customEvents.on('gameLoaded', () => { } } + let lastCall = 0 bot.on('physicsTick', () => { + // throttle, tps: 6 + if (Date.now() - lastCall < 166) return + lastCall = Date.now() for (const [id, { tracking, info }] of Object.entries(bot.tracker.trackingData)) { if (!tracking) continue const e = bot.entities[id] diff --git a/src/flyingSquidEvents.ts b/src/flyingSquidEvents.ts index fbab268bf..fb5b00e3c 100644 --- a/src/flyingSquidEvents.ts +++ b/src/flyingSquidEvents.ts @@ -1,5 +1,5 @@ import { showModal } from './globalState' -import { chatInputValueGlobal } from './react/ChatContainer' +import { chatInputValueGlobal } from './react/Chat' import { showNotification } from './react/NotificationProvider' export default () => { diff --git a/src/globals.d.ts b/src/globals.d.ts index 7d2a478c4..6a38a21d4 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -30,46 +30,4 @@ declare interface Document { exitPointerLock?(): void } -declare namespace JSX { - interface IntrinsicElements { - [elemName: string]: any - } -} - -declare interface Window extends Record {} - -type StringKeys = Extract - - -interface ObjectConstructor { - keys (obj: T): Array> - entries (obj: T): Array<[StringKeys, T[keyof T]]> - // todo review https://stackoverflow.com/questions/57390305/trying-to-get-fromentries-type-right - fromEntries> (obj: T): Record - assign, K extends Record> (target: T, source: K): asserts target is T & K -} - -declare module '*.module.css' { - const css: Record - export default css -} -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 -} - -interface PromiseConstructor { - withResolvers (): { - resolve: (value: T) => void; - reject: (reason: any) => void; - promise: Promise; - } -} +declare interface Window extends Record { } diff --git a/src/react/AppStatusProvider.tsx b/src/react/AppStatusProvider.tsx index 52c95383f..6ae39308d 100644 --- a/src/react/AppStatusProvider.tsx +++ b/src/react/AppStatusProvider.tsx @@ -6,7 +6,8 @@ import { fsState } from '../loadSave' import { guessProblem } from '../guessProblem' import AppStatus from './AppStatus' import DiveTransition from './DiveTransition' -import { useDidUpdateEffect, useIsModalActive } from './utils' +import { useDidUpdateEffect } from './utils' +import { useIsModalActive } from './utilsApp' import Button from './Button' const initialState = { diff --git a/src/react/ArmorBar.css b/src/react/ArmorBar.css index ae13935eb..ca579a72a 100644 --- a/src/react/ArmorBar.css +++ b/src/react/ArmorBar.css @@ -9,6 +9,7 @@ --bg-x: calc(-1 * 16px); --bg-y: calc(-1 * 9px); pointer-events: none; + image-rendering: pixelated; } .armor { diff --git a/src/react/ArmorBar.tsx b/src/react/ArmorBar.tsx index cc9c66cd8..777f753da 100644 --- a/src/react/ArmorBar.tsx +++ b/src/react/ArmorBar.tsx @@ -40,7 +40,7 @@ export default ( { Array.from({ length: 10 }, () => 0) .map( - (num, index) =>
) @@ -48,5 +48,3 @@ export default (
} - - diff --git a/src/react/BarsCommon.tsx b/src/react/BarsCommon.tsx new file mode 100644 index 000000000..1ddc89ccf --- /dev/null +++ b/src/react/BarsCommon.tsx @@ -0,0 +1,26 @@ +const getEffectClass = (effect) => { + switch (effect.id) { + case 19: + return 'poisoned' + case 20: + return 'withered' + case 22: + return 'absorption' + default: + return '' + } +} + +export const barEffectAdded = (htmlElement, effect) => { + const effectClass = getEffectClass(effect) + if (effectClass) { + htmlElement.classList.add(effectClass) + } +} + +export const barEffectEnded = (htmlElement, effect) => { + const effectClass = getEffectClass(effect) + if (effectClass) { + htmlElement.classList.remove(effectClass) + } +} diff --git a/src/react/BossBarOverlay.css b/src/react/BossBarOverlay.css index fdbeb594e..95ce44ecd 100644 --- a/src/react/BossBarOverlay.css +++ b/src/react/BossBarOverlay.css @@ -18,7 +18,7 @@ color: #fff; } .bossbar { - background-image: url("textures/1.18.1/gui/bars.png"); + background-image: url("minecraft-assets/minecraft-assets/data/1.18.1/gui/bars.png"); width: 182px; height: 5px; position: relative; @@ -30,5 +30,5 @@ left: 0; height: 5px; width: 0; - background-image: url("textures/1.18.1/gui/bars.png"); + background-image: url("minecraft-assets/minecraft-assets/data/1.18.1/gui/bars.png"); } diff --git a/src/react/BreathBar.css b/src/react/BreathBar.css index a7e38c7c2..e98cf7c21 100644 --- a/src/react/BreathBar.css +++ b/src/react/BreathBar.css @@ -10,6 +10,7 @@ --bg-x: calc(-1 * 16px); --bg-y: calc(-1 * 18px); pointer-events: none; + image-rendering: pixelated; } .breath { diff --git a/src/react/Button.stories.tsx b/src/react/Button.stories.tsx new file mode 100644 index 000000000..88ad0074c --- /dev/null +++ b/src/react/Button.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import Button from './Button' + +const meta: Meta = { + component: Button, +} + +export default meta +type Story = StoryObj; + +export const Primary: Story = { + args: { + label: 'Hello!', + icon: 'pixelarticons:lock-open', + inScreen: false, + }, +} diff --git a/src/react/Button.tsx b/src/react/Button.tsx index db7e575ac..20dc0801c 100644 --- a/src/react/Button.tsx +++ b/src/react/Button.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames' -import { FC, Ref } from 'react' -import { loadSound, playSound } from '../basicSounds' +import { createContext, FC, Ref, useContext } from 'react' import buttonCss from './button.module.css' +import SharedHudVars from './SharedHudVars' // testing in storybook from deathscreen @@ -13,11 +13,19 @@ interface Props extends React.ComponentProps<'button'> { rootRef?: Ref } -void loadSound('button_click.mp3') +const ButtonContext = createContext({ + onClick () { }, +}) + +export const ButtonProvider: FC<{children, onClick}> = ({ children, onClick }) => { + return {children} +} export default (({ label, icon, children, inScreen, rootRef, type = 'button', ...args }) => { + const ctx = useContext(ButtonContext) + const onClick = (e) => { - void playSound('button_click.mp3') + ctx.onClick() args.onClick?.(e) } if (inScreen) { @@ -29,9 +37,11 @@ export default (({ label, icon, children, inScreen, rootRef, type = 'button', .. args.style.width = 20 } - return + return + + }) satisfies FC diff --git a/src/react/ButtonAppProvider.tsx b/src/react/ButtonAppProvider.tsx new file mode 100644 index 000000000..206b52a2b --- /dev/null +++ b/src/react/ButtonAppProvider.tsx @@ -0,0 +1,10 @@ +import { loadSound, playSound } from '../basicSounds' +import { ButtonProvider } from './Button' + +void loadSound('button_click.mp3') + +export default ({ children }) => { + return { + void playSound('button_click.mp3') + }}>{children} +} diff --git a/src/react/ChatContainer.css b/src/react/Chat.css similarity index 100% rename from src/react/ChatContainer.css rename to src/react/Chat.css diff --git a/src/react/Chat.stories.tsx b/src/react/Chat.stories.tsx index c568f23b4..901368d50 100644 --- a/src/react/Chat.stories.tsx +++ b/src/react/Chat.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react' import { useEffect, useState } from 'react' import { formatMessage } from '../botUtils' -import Chat, { fadeMessage, chatInputValueGlobal } from './ChatContainer' +import Chat, { fadeMessage, chatInputValueGlobal } from './Chat' import Button from './Button' window.spamMessage = window.spamMessage ?? '' diff --git a/src/react/ChatContainer.tsx b/src/react/Chat.tsx similarity index 99% rename from src/react/ChatContainer.tsx rename to src/react/Chat.tsx index 86a3641a1..b05f99306 100644 --- a/src/react/ChatContainer.tsx +++ b/src/react/Chat.tsx @@ -3,9 +3,8 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { isCypress } from '../standaloneUtils' import { MessageFormatPart } from '../botUtils' import { MessagePart } from './MessageFormatted' -import './ChatContainer.css' -import { isIos } from './utils' -import { reactKeyForMessage } from './Scoreboard' +import './Chat.css' +import { isIos, reactKeyForMessage } from './utils' export type Message = { parts: MessageFormatPart[], diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index 1a4d88b67..0322c60fe 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -4,8 +4,8 @@ import { formatMessage } from '../botUtils' import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinCommands' import { hideCurrentModal, miscUiState } from '../globalState' import { options } from '../optionsStorage' -import ChatContainer, { Message, fadeMessage } from './ChatContainer' -import { useIsModalActive } from './utils' +import Chat, { Message, fadeMessage } from './Chat' +import { useIsModalActive } from './utilsApp' import { hideNotification, showNotification } from './NotificationProvider' import { updateLoadedServerData } from './ServersListProvider' @@ -37,7 +37,7 @@ export default () => { }) }, []) - return { diff --git a/src/react/DeathScreenProvider.tsx b/src/react/DeathScreenProvider.tsx index 930ed070e..4e68dbec5 100644 --- a/src/react/DeathScreenProvider.tsx +++ b/src/react/DeathScreenProvider.tsx @@ -5,7 +5,7 @@ import { MessageFormatPart, formatMessage } from '../botUtils' import { showModal, hideModal } from '../globalState' import { options } from '../optionsStorage' import DeathScreen from './DeathScreen' -import { useIsModalActive } from './utils' +import { useIsModalActive } from './utilsApp' const dieReasonProxy = proxy({ value: null as MessageFormatPart[] | null }) diff --git a/src/react/diveAnimation.module.css b/src/react/DiveTransition.module.css similarity index 100% rename from src/react/diveAnimation.module.css rename to src/react/DiveTransition.module.css diff --git a/src/react/Dive.stories.tsx b/src/react/DiveTransition.stories.tsx similarity index 100% rename from src/react/Dive.stories.tsx rename to src/react/DiveTransition.stories.tsx diff --git a/src/react/DiveTransition.tsx b/src/react/DiveTransition.tsx index a3188305f..cd78dd66e 100644 --- a/src/react/DiveTransition.tsx +++ b/src/react/DiveTransition.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { Transition } from 'react-transition-group' -import styles from './diveAnimation.module.css' +import styles from './DiveTransition.module.css' // dive animation from framework7 diff --git a/src/react/EnterFullscreenButton.tsx b/src/react/EnterFullscreenButton.tsx index 12c47f407..ad78ddad7 100644 --- a/src/react/EnterFullscreenButton.tsx +++ b/src/react/EnterFullscreenButton.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import Button from './Button' -import { useUsingTouch } from './utils' +import { useUsingTouch } from './utilsApp' export default () => { const [fullScreen, setFullScreen] = useState(false) diff --git a/src/react/FoodBar.css b/src/react/FoodBar.css index bd31e8182..ca3629cdf 100644 --- a/src/react/FoodBar.css +++ b/src/react/FoodBar.css @@ -12,6 +12,7 @@ --bg-x: calc(-1 * (16px + 9px * var(--lightened))); --bg-y: calc(-1 * 27px); pointer-events: none; + image-rendering: pixelated; } .foodbar.poisoned { diff --git a/src/react/FoodBar.stories.tsx b/src/react/FoodBar.stories.tsx new file mode 100644 index 000000000..abf351e38 --- /dev/null +++ b/src/react/FoodBar.stories.tsx @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import FoodBar from './FoodBar' + +const meta: Meta = { + component: FoodBar +} + +export default meta +type Story = StoryObj; + +export const Primary: Story = { + args: { + gameMode: 'survival', + food: 10, + effectToAdd: 19, + effectToRemove: 20, + } +} diff --git a/src/react/FoodBar.tsx b/src/react/FoodBar.tsx index c7680b687..d4141e453 100644 --- a/src/react/FoodBar.tsx +++ b/src/react/FoodBar.tsx @@ -1,15 +1,16 @@ import { useRef, useState, useEffect } from 'react' import SharedHudVars from './SharedHudVars' import './FoodBar.css' +import { barEffectAdded, barEffectEnded } from './BarsCommon' export type FoodBarProps = { - gameMode: string, + gameMode?: string, food: number, - effectToAdd: number | null, - effectToRemove: number | null, - effectAdded: (htmlElement: HTMLDivElement | null, effect: number | null) => void, - effectEnded: (htmlElement: HTMLDivElement | null, effect: number | null) => void, + effectToAdd?: number | null, + effectToRemove?: number | null, + resetEffects?: () => void, + style?: React.CSSProperties } export default ( @@ -18,8 +19,8 @@ export default ( food, effectToAdd, effectToRemove, - effectAdded, - effectEnded + resetEffects, + style }: FoodBarProps) => { const foodRef = useRef(null) @@ -54,15 +55,17 @@ export default ( }, [food]) useEffect(() => { - effectAdded(foodRef.current, effectToAdd) - }, [effectToAdd]) - - useEffect(() => { - effectEnded(foodRef.current, effectToRemove) - }, [effectToRemove]) + if (effectToAdd) { + barEffectAdded(foodRef.current, effectToAdd) + } + if (effectToRemove) { + barEffectEnded(foodRef.current, effectToRemove) + } + resetEffects?.() + }, [effectToAdd, effectToRemove]) - return -
+ return +
{ Array.from({ length: 10 }, () => 0) .map( diff --git a/src/react/FullScreenWidget.tsx b/src/react/FullScreenWidget.tsx index ecec1813a..798694461 100644 --- a/src/react/FullScreenWidget.tsx +++ b/src/react/FullScreenWidget.tsx @@ -1,5 +1,5 @@ import Screen from './Screen' -import { useIsWidgetActive } from './utils' +import { useIsWidgetActive } from './utilsApp' export default ({ name, title, children }) => { const isWidgetActive = useIsWidgetActive(name) diff --git a/src/react/HealthBar.css b/src/react/HealthBar.css index bbf0f2056..a6fefe097 100644 --- a/src/react/HealthBar.css +++ b/src/react/HealthBar.css @@ -12,6 +12,7 @@ --bg-x: calc(-1 * (16px + 9px * var(--lightened))); --bg-y: calc(-1 * var(--hardcore) * 45px); pointer-events: none; + image-rendering: pixelated; } .health.creative { diff --git a/src/react/HealthBar.stories.tsx b/src/react/HealthBar.stories.tsx index bcec2e0a0..69e84429b 100644 --- a/src/react/HealthBar.stories.tsx +++ b/src/react/HealthBar.stories.tsx @@ -9,19 +9,6 @@ const meta: Meta = { export default meta type Story = StoryObj; -const getEffectClass = (effect) => { - switch (effect.id) { - case 19: - return 'poisoned' - case 20: - return 'withered' - case 22: - return 'absorption' - default: - return '' - } -} - export const Primary: Story = { args: { gameMode: 'survival', @@ -30,16 +17,5 @@ export const Primary: Story = { healthValue: 10, effectToAdd: 19, effectToRemove: 20, - effectAdded (htmlElement, effect) { - const effectClass = getEffectClass(effect) - if (!effectClass) return - if (htmlElement) htmlElement.classList.add(effectClass) - }, - effectEnded (htmlElement, effect) { - const effectClass = getEffectClass(effect) - if (!effectClass) return - if (htmlElement) htmlElement.classList.remove(effectClass) - } - } } diff --git a/src/react/HealthBar.tsx b/src/react/HealthBar.tsx index 46ed8a34e..14e27fe62 100644 --- a/src/react/HealthBar.tsx +++ b/src/react/HealthBar.tsx @@ -1,41 +1,36 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useEffect } from 'react' import SharedHudVars from './SharedHudVars' import './HealthBar.css' +import { barEffectAdded, barEffectEnded } from './BarsCommon' export type HealthBarProps = { - gameMode: string, + gameMode?: string, isHardcore: boolean, damaged: boolean, healthValue: number, - effectToAdd: number | null, - effectToRemove: number | null, - effectAdded: (htmlElement: HTMLDivElement | null, effect: number | null) => void, - effectEnded: (htmlElement: HTMLDivElement | null, effect: number | null) => void, + effectToAdd?: number | null, + effectToRemove?: number | null, + resetEffects?: () => void + style?: React.CSSProperties } export default ( { - gameMode, - isHardcore, - damaged, - healthValue, + gameMode, + isHardcore, + damaged, + healthValue, effectToAdd, effectToRemove, - effectAdded, - effectEnded + resetEffects, + style }: HealthBarProps) => { const healthRef = useRef(null) - const [className, setClassName] = useState('') useEffect(() => { if (healthRef.current) { healthRef.current.classList.toggle('creative', gameMode === 'creative' || gameMode === 'spectator') - // if (gameMode === 'creative' || gameMode === 'spectator') { - // healthRef.current.classList.add('creative') - // } else { - // healthRef.current.classList.remove('creative') - // } } }, [gameMode]) @@ -89,24 +84,24 @@ export default ( }, [healthValue]) useEffect(() => { - effectAdded(healthRef.current, effectToAdd) - }, [effectToAdd]) - - useEffect(() => { - effectEnded(healthRef.current, effectToRemove) - }, [effectToRemove]) + if (effectToAdd) { + barEffectAdded(healthRef.current, effectToAdd) + } + if (effectToRemove) { + barEffectEnded(healthRef.current, effectToRemove) + } + resetEffects?.() + }, [effectToAdd, effectToRemove]) return -
+
{ Array.from({ length: 10 }, () => 0) .map( - (num, index) =>
) }
} - - diff --git a/src/react/HudBarsProvider.tsx b/src/react/HudBarsProvider.tsx index 0b0cec246..746d4819e 100644 --- a/src/react/HudBarsProvider.tsx +++ b/src/react/HudBarsProvider.tsx @@ -32,22 +32,6 @@ export default () => { } } - const effectAdded = (htmlElement, effect) => { - const effectClass = getEffectClass(effect) - if (effectClass) { - htmlElement.classList.add(effectClass) - } - setEffectToAdd(null) - } - - const effectEnded = (htmlElement, effect) => { - const effectClass = getEffectClass(effect) - if (effectClass) { - htmlElement.classList.remove(effectClass) - } - setEffectToRemove(null) - } - const onDamage = () => { setDamaged(prev => true) if (hurtTimeout.current) clearTimeout(hurtTimeout.current) @@ -114,8 +98,10 @@ export default () => { healthValue={healthValue} effectToAdd={effectToAdd} effectToRemove={effectToRemove} - effectAdded={effectAdded} - effectEnded={effectEnded} + resetEffects={() => { + setEffectToAdd(null) + setEffectToRemove(null) + }} /> { food={food} effectToAdd={effectToAdd} effectToRemove={effectToRemove} - effectAdded={effectAdded} - effectEnded={effectEnded} + resetEffects={() => { + setEffectToAdd(null) + setEffectToRemove(null) + }} /> { rootStyles?: React.CSSProperties @@ -10,11 +10,10 @@ interface Props extends React.ComponentProps<'input'> { export default ({ autoFocus, rootStyles, inputRef, ...inputProps }: Props) => { const ref = useRef(null!) - const isTouch = useUsingTouch() useEffect(() => { if (inputRef) (inputRef as any).current = ref.current - if (!autoFocus || isTouch) return // Don't make screen keyboard popup on mobile + if (!autoFocus || isMobile()) return // Don't make screen keyboard popup on mobile ref.current.focus() }, []) diff --git a/src/react/MessageFormatted.tsx b/src/react/MessageFormatted.tsx index baab8171c..6b9620caf 100644 --- a/src/react/MessageFormatted.tsx +++ b/src/react/MessageFormatted.tsx @@ -4,7 +4,7 @@ import { noCase } from 'change-case' import mojangson from 'mojangson' import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils' import { MessageFormatPart } from '../botUtils' -import { chatInputValueGlobal } from './ChatContainer' +import { chatInputValueGlobal } from './Chat' import './MessageFormatted.css' const hoverItemToText = (hoverEvent: MessageFormatPart['hoverEvent']) => { diff --git a/src/react/PauseScreen.tsx b/src/react/PauseScreen.tsx index 92e6c8c79..a224f9e63 100644 --- a/src/react/PauseScreen.tsx +++ b/src/react/PauseScreen.tsx @@ -15,7 +15,7 @@ import { disconnect } from '../flyingSquidUtils' import { pointerLock, setLoadingScreenStatus } from '../utils' import { closeWan, openToWanAndCopyJoinLink, getJoinLink } from '../localServerMultiplayer' import { copyFilesAsyncWithProgress, mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs' -import { useIsModalActive } from './utils' +import { useIsModalActive } from './utilsApp' import { showOptionsModal } from './SelectOption' import Button from './Button' import Screen from './Screen' diff --git a/src/react/PixelartIcon.tsx b/src/react/PixelartIcon.tsx index a54cd40e6..919b83097 100644 --- a/src/react/PixelartIcon.tsx +++ b/src/react/PixelartIcon.tsx @@ -3,5 +3,5 @@ import { CSSProperties } from 'react' // names: https://pixelarticons.com/free/ export default ({ iconName, width = undefined as undefined | number, styles = {} as CSSProperties, className = undefined }) => { if (width !== undefined) styles = { width, height: width, ...styles } - return + return } diff --git a/src/react/PlayerListOverlay.css b/src/react/PlayerListOverlay.css index b36801c0f..aa141181b 100644 --- a/src/react/PlayerListOverlay.css +++ b/src/react/PlayerListOverlay.css @@ -3,8 +3,9 @@ position: absolute; background-color: rgba(0, 0, 0, 0.3); top: 9px; - left: 50%; - transform: translate(-50%); + left: 0; + right: 0; + margin: auto; width: fit-content; padding: 1px; display: flex; diff --git a/src/react/PlayerListOverlay.stories.tsx b/src/react/PlayerListOverlay.stories.tsx index cb2d972f4..0353e32e7 100644 --- a/src/react/PlayerListOverlay.stories.tsx +++ b/src/react/PlayerListOverlay.stories.tsx @@ -11,8 +11,14 @@ type Story = StoryObj; export const Primary: Story = { args: { - playersLists: [], - clientId: '', + playersLists: [ + [ + { username: 'Player 1', ping: 10, uuid: '1' }, + { username: 'Player 2', ping: 20, uuid: '2' }, + { username: 'Player 3', ping: 30, uuid: '3' }, + ] + ], + clientId: '2', tablistHeader: 'Header', tablistFooter: 'Footer', serverIP: '95.163.228.101', diff --git a/src/react/PlayerListOverlay.tsx b/src/react/PlayerListOverlay.tsx index ee43ae2c2..a1479c14b 100644 --- a/src/react/PlayerListOverlay.tsx +++ b/src/react/PlayerListOverlay.tsx @@ -2,7 +2,7 @@ import MessageFormattedString from './MessageFormattedString' import './PlayerListOverlay.css' -type PlayersLists = Array> +type PlayersLists = Array>> type PlayerListOverlayProps = { playersLists: PlayersLists, @@ -10,11 +10,12 @@ type PlayerListOverlayProps = { tablistHeader: string | Record | null, tablistFooter: string | Record | null, serverIP: string + style?: React.CSSProperties } -export default ({ playersLists, clientId, tablistHeader, tablistFooter, serverIP }: PlayerListOverlayProps) => { +export default ({ playersLists, clientId, tablistHeader, tablistFooter, serverIP, style }: PlayerListOverlayProps) => { - return
+ return
Server IP: {serverIP}
@@ -23,7 +24,7 @@ export default ({ playersLists, clientId, tablistHeader, tablistFooter, serverIP {playersLists.map((list, index) => (
{list.map(player => ( -
+

{player.ping}

diff --git a/src/react/Scoreboard.tsx b/src/react/Scoreboard.tsx index 546a0f586..8970f0048 100644 --- a/src/react/Scoreboard.tsx +++ b/src/react/Scoreboard.tsx @@ -1,5 +1,6 @@ import './Scoreboard.css' import MessageFormattedString from './MessageFormattedString' +import { reactKeyForMessage } from './utils' export type ScoreboardItems = Array<{name: string, value: number, displayName?: any}> @@ -8,17 +9,14 @@ type ScoreboardProps = { title: string, items: ScoreboardItems, open: boolean + style?: React.CSSProperties } -export const reactKeyForMessage = (message) => { - return typeof message === 'string' ? message : JSON.stringify(message) -} - -export default function Scoreboard ({ title, items, open }: ScoreboardProps) { +export default function Scoreboard ({ title, items, open, style }: ScoreboardProps) { if (!open) return null return ( -
+
diff --git a/src/react/SelectOption.tsx b/src/react/SelectOption.tsx index b74434e19..e2879e109 100644 --- a/src/react/SelectOption.tsx +++ b/src/react/SelectOption.tsx @@ -1,7 +1,7 @@ import { proxy, useSnapshot } from 'valtio' import { hideCurrentModal, showModal } from '../globalState' import Screen from './Screen' -import { useIsModalActive } from './utils' +import { useIsModalActive } from './utilsApp' import Button from './Button' const state = proxy({ diff --git a/src/react/ServersListProvider.tsx b/src/react/ServersListProvider.tsx index ecd749162..0de15e1ef 100644 --- a/src/react/ServersListProvider.tsx +++ b/src/react/ServersListProvider.tsx @@ -5,7 +5,8 @@ import { ConnectOptions } from '../connect' import { hideCurrentModal, miscUiState, showModal } from '../globalState' import ServersList from './ServersList' import AddServer from './AddServer' -import { useDidUpdateEffect, useIsModalActive } from './utils' +import { useDidUpdateEffect } from './utils' +import { useIsModalActive } from './utilsApp' interface StoreServerItem { ip: string, diff --git a/src/react/SharedHudVars.tsx b/src/react/SharedHudVars.tsx index acdcc2376..ca48453a6 100644 --- a/src/react/SharedHudVars.tsx +++ b/src/react/SharedHudVars.tsx @@ -1,5 +1,6 @@ import { CSSProperties, useEffect } from 'react' import icons from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/icons.png' +import widgets from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png' export default ({ children }) => { useEffect(() => { @@ -8,7 +9,9 @@ export default ({ children }) => { // 2. Easier application to globally override icons with custom image (eg from resourcepacks) const css = /* css */` :root { + --widgets-gui-atlas: url(${widgets}); --gui-icons: url(${icons}), url(${icons}); + --safe-area-inset-bottom: calc(env(safe-area-inset-bottom) / 2); } ` const style = document.createElement('style') @@ -17,11 +20,5 @@ export default ({ children }) => { document.head.appendChild(style) }, []) - const customVars = { - '--safe-area-inset-bottom': 'calc(env(safe-area-inset-bottom) / 2)' - } as CSSProperties - - return
{children}
+ return children } diff --git a/src/react/SignEditorProvider.tsx b/src/react/SignEditorProvider.tsx index b6ade6255..666935b69 100644 --- a/src/react/SignEditorProvider.tsx +++ b/src/react/SignEditorProvider.tsx @@ -2,7 +2,7 @@ import { useMemo, useEffect, useState, useRef } from 'react' import { showModal, hideModal } from '../globalState' import { setDoPreventDefault } from '../controls' import { options } from '../optionsStorage' -import { useIsModalActive } from './utils' +import { useIsModalActive } from './utilsApp' import SignEditor, { ResultType } from './SignEditor' diff --git a/src/react/SingleplayerProvider.tsx b/src/react/SingleplayerProvider.tsx index 35704e8ed..83d28637c 100644 --- a/src/react/SingleplayerProvider.tsx +++ b/src/react/SingleplayerProvider.tsx @@ -9,7 +9,7 @@ import { haveDirectoryPicker, setLoadingScreenStatus } from '../utils' import { exportWorld } from '../builtinCommands' import { googleProviderState, useGoogleLogIn, GoogleDriveProvider, isGoogleDriveAvailable, APP_ID } from '../googledrive' import Singleplayer, { WorldProps } from './Singleplayer' -import { useIsModalActive } from './utils' +import { useIsModalActive } from './utilsApp' import { showOptionsModal } from './SelectOption' import Input from './Input' import GoogleButton from './GoogleButton' diff --git a/src/react/Slider.tsx b/src/react/Slider.tsx index bc6c5890a..34cc27177 100644 --- a/src/react/Slider.tsx +++ b/src/react/Slider.tsx @@ -1,6 +1,7 @@ // Slider.tsx import React, { useState, useEffect } from 'react' import styles from './slider.module.css' +import SharedHudVars from './SharedHudVars' interface Props extends React.ComponentProps<'div'> { label: string; @@ -47,36 +48,38 @@ const Slider: React.FC = ({ } 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) - }} - /> -
-
- -
+ +
+ { + 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) + }} + /> +
+
+ +
+
) } diff --git a/src/react/SoundMuffler.tsx b/src/react/SoundMuffler.tsx index 869abc1cf..ecbae14d7 100644 --- a/src/react/SoundMuffler.tsx +++ b/src/react/SoundMuffler.tsx @@ -5,7 +5,7 @@ import { lastPlayedSounds } from '../soundSystem' import { options } from '../optionsStorage' import Button from './Button' import Screen from './Screen' -import { useIsModalActive } from './utils' +import { useIsModalActive } from './utilsApp' const SoundRow = ({ sound, children }) => { const { mutedSounds } = useSnapshot(options) diff --git a/src/react/TouchAreasControlsProvider.tsx b/src/react/TouchAreasControlsProvider.tsx index 7de8eacaa..24479e893 100644 --- a/src/react/TouchAreasControlsProvider.tsx +++ b/src/react/TouchAreasControlsProvider.tsx @@ -2,7 +2,7 @@ import { useSnapshot } from 'valtio' import { activeModalStack, hideModal } from '../globalState' import { options } from '../optionsStorage' import TouchAreasControls from './TouchAreasControls' -import { useIsModalActive, useUsingTouch } from './utils' +import { useIsModalActive, useUsingTouch } from './utilsApp' export default () => { const usingTouch = useUsingTouch() diff --git a/src/react/TouchControls.tsx b/src/react/TouchControls.tsx index db71ae1fc..b49d57c4f 100644 --- a/src/react/TouchControls.tsx +++ b/src/react/TouchControls.tsx @@ -4,7 +4,7 @@ import { useSnapshot } from 'valtio' import { contro } from '../controls' import { miscUiState, activeModalStack } from '../globalState' import { watchValue, options } from '../optionsStorage' -import { useUsingTouch } from './utils' +import { useUsingTouch } from './utilsApp' // todo useInterfaceState.setState({ diff --git a/src/react/globals.d.ts b/src/react/globals.d.ts new file mode 100644 index 000000000..842d27c4c --- /dev/null +++ b/src/react/globals.d.ts @@ -0,0 +1,45 @@ +type StringKeys = Extract + + +interface ObjectConstructor { + keys (obj: T): Array> + entries (obj: T): Array<[StringKeys, T[keyof T]]> + // todo review https://stackoverflow.com/questions/57390305/trying-to-get-fromentries-type-right + fromEntries> (obj: T): Record + assign, K extends Record> (target: T, source: K): asserts target is T & K +} + +declare module '*.module.css' { + const css: Record + export default css +} +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 +} + +interface PromiseConstructor { + withResolvers (): { + resolve: (value: T) => void; + reject: (reason: any) => void; + promise: Promise; + } +} + +declare namespace JSX { + interface IntrinsicElements { + 'iconify-icon': { + icon: string + style?: React.CSSProperties + class?: string + } + } +} diff --git a/src/react/npmReactEntrypoint.ts b/src/react/npmReactEntrypoint.ts new file mode 100644 index 000000000..643930d70 --- /dev/null +++ b/src/react/npmReactEntrypoint.ts @@ -0,0 +1 @@ +// export * from './npmReactComponents' diff --git a/src/react/utils.ts b/src/react/utils.ts index 900b279ff..b8caeddfe 100644 --- a/src/react/utils.ts +++ b/src/react/utils.ts @@ -1,16 +1,5 @@ -import { useSnapshot } from 'valtio' import { useEffect, useRef } from 'react' import { UAParser } from 'ua-parser-js' -import { activeModalStack, miscUiState } from '../globalState' - -export const useIsModalActive = (modal: string, useIncludes = false) => { - const allStack = useSnapshot(activeModalStack) - return useIncludes ? allStack.some(x => x.reactType === modal) : allStack.at(-1)?.reactType === modal -} - -export const useIsWidgetActive = (name: string) => { - return useIsModalActive(`widget-${name}`) -} export function useDidUpdateEffect (fn, inputs) { const isMountingRef = useRef(false) @@ -28,10 +17,10 @@ export function useDidUpdateEffect (fn, inputs) { }, inputs) } -export const useUsingTouch = () => { - return useSnapshot(miscUiState).currentTouch -} - export const ua = new UAParser(navigator.userAgent) export const isIos = ua.getOS().name === 'iOS' + +export const reactKeyForMessage = (message) => { + return typeof message === 'string' ? message : JSON.stringify(message) +} diff --git a/src/react/utilsApp.ts b/src/react/utilsApp.ts new file mode 100644 index 000000000..3fd185159 --- /dev/null +++ b/src/react/utilsApp.ts @@ -0,0 +1,15 @@ +import { useSnapshot } from 'valtio' +import { activeModalStack, miscUiState } from '../globalState' + + +export const useUsingTouch = () => { + return useSnapshot(miscUiState).currentTouch +} +export const useIsModalActive = (modal: string, useIncludes = false) => { + const allStack = useSnapshot(activeModalStack) + return useIncludes ? allStack.some(x => x.reactType === modal) : allStack.at(-1)?.reactType === modal +} + +export const useIsWidgetActive = (name: string) => { + return useIsModalActive(`widget-${name}`) +} diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 419d8dff9..760a1a7b0 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -27,12 +27,13 @@ import PauseScreen from './react/PauseScreen' import SoundMuffler from './react/SoundMuffler' import TouchControls from './react/TouchControls' import widgets from './react/widgets' -import { useIsWidgetActive } from './react/utils' +import { useIsWidgetActive } from './react/utilsApp' import GlobalSearchInput from './GlobalSearchInput' import TouchAreasControlsProvider from './react/TouchAreasControlsProvider' import NotificationProvider, { showNotification } from './react/NotificationProvider' import HotbarRenderApp from './react/HotbarRenderApp' import Crosshair from './react/Crosshair' +import ButtonAppProvider from './react/ButtonAppProvider' import ServersListProvider from './react/ServersListProvider' const RobustPortal = ({ children, to }) => { @@ -112,7 +113,7 @@ const InGameUi = () => { - {/* becaues of z-index */} + {/* because of z-index */} @@ -132,21 +133,23 @@ const WidgetDisplay = ({ name, Component }) => { const App = () => { return
- - - - - - - - - - - - - {/* - */} - + + + + + + + + + + + + + + {/* + */} + +
} diff --git a/src/styles.css b/src/styles.css index 64fc054d9..30abc52b4 100644 --- a/src/styles.css +++ b/src/styles.css @@ -26,7 +26,6 @@ html { } body { - --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'); } diff --git a/src/worldInteractions.ts b/src/worldInteractions.ts index 544027a58..c3263f2f7 100644 --- a/src/worldInteractions.ts +++ b/src/worldInteractions.ts @@ -89,6 +89,8 @@ class WorldInteraction { if (entity && e.button === 2) { bot.attack(entity) + } else { + // bot } }) document.addEventListener('blur', (e) => { diff --git a/tsconfig.npm.json b/tsconfig.npm.json new file mode 100644 index 000000000..0aec498f3 --- /dev/null +++ b/tsconfig.npm.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist-npm/dist-pre", + "noEmit": false, + "declaration": true + }, + "include": [ + "src/react/npmReactComponents.ts", + "src/react/globals.d.ts" + ] +}