feat: publish all stories UI components to npm! (#111)

This commit is contained in:
Vitaly 2024-05-05 03:25:08 +03:00 committed by GitHub
commit 1e7153c2e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 688 additions and 289 deletions

View file

@ -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

View file

@ -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 }}

4
.gitignore vendored
View file

@ -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

View file

@ -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:

32
README.NPM.MD Normal file
View file

@ -0,0 +1,32 @@
# Minecraft React
```bash
yarn add minecraft-react
```
## Usage
```jsx
import { Scoreboard } from 'minecraft-react'
const App = () => {
return (
<Scoreboard
open
title="Scoreboard"
items={[
{ name: 'Player 1', value: 10 },
{ name: 'Player 2', value: 20 },
{ name: 'Player 3', value: 30 },
]}
/>
)
}
```
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)

View file

@ -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 (
<div style={{ scale: '2', transformOrigin: 'top left', width: '50%', height: '50dvh', fontFamily: 'mojangles, sans-serif', background: 'gray' }}>
<Button>Button</Button>
<Slider label="Slider" value={sliderValue} updateValue={value => setSliderValue(value)} />
<ArmorBar armorValue={10} />
<Chat
messages={[
{ id: 0, parts: [{ text: 'A formmated message in the chat', color: 'blue' }] },
{ id: 1, parts: [{ text: 'An other message in the chat', color: 'red' }] },
]}
usingTouch={false}
opened
sendMessage={message => {
console.log('typed', message)
// close
}}
/>
<BreathBar oxygen={10} />
<HealthBar isHardcore={false} healthValue={10} damaged={false} />
<FoodBar food={10} />
<PlayerListOverlay
style={{
position: 'static',
}}
clientId="" // needed for current player highlight
serverIP="Server IP"
tablistHeader="Tab §aHeader"
tablistFooter="Tab §bFooter"
playersLists={[
[
{ username: 'Player 1', ping: 10, uuid: undefined },
{ username: 'Player 2', ping: 20, uuid: undefined },
{ username: 'Player 3', ping: 30, uuid: undefined },
],
]}
/>
"§bRed" displays as <MessageFormattedString message="§bRed" />
<Scoreboard
open
title="Scoreboard"
items={[
{ name: 'Player 1', value: 10 },
{ name: 'Player 2', value: 20 },
{ name: 'Player 3', value: 30 },
]}
/>
<XPBar gamemode="survival" level={10} progress={0.5} />
</div>
)
}
createRoot(document.body as Element).render(<ExampleDemo />)

View file

@ -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"
},

36
package.npm.json Normal file
View file

@ -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"
}
}

51
pnpm-lock.yaml generated
View file

@ -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

148
scripts/buildNpmReact.ts Normal file
View file

@ -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<string>()
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') })
}
})

View file

@ -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) => {

View file

@ -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<TextComponent, 'hoverEvent' | 'clickEvent'> & {
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
}

View file

@ -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'

View file

@ -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]

View file

@ -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 () => {

44
src/globals.d.ts vendored
View file

@ -30,46 +30,4 @@ declare interface Document {
exitPointerLock?(): void
}
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any
}
}
declare interface Window extends Record<string, any> {}
type StringKeys<T extends object> = Extract<keyof T, string>
interface ObjectConstructor {
keys<T extends object> (obj: T): Array<StringKeys<T>>
entries<T extends object> (obj: T): Array<[StringKeys<T>, T[keyof T]]>
// todo review https://stackoverflow.com/questions/57390305/trying-to-get-fromentries-type-right
fromEntries<T extends Array<[string, any]>> (obj: T): Record<T[number][0], T[number][1]>
assign<T extends Record<string, any>, K extends Record<string, any>> (target: T, source: K): asserts target is T & K
}
declare module '*.module.css' {
const css: Record<string, string>
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<T> (): {
resolve: (value: T) => void;
reject: (reason: any) => void;
promise: Promise<T>;
}
}
declare interface Window extends Record<string, any> { }

View file

@ -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 = {

View file

@ -9,6 +9,7 @@
--bg-x: calc(-1 * 16px);
--bg-y: calc(-1 * 9px);
pointer-events: none;
image-rendering: pixelated;
}
.armor {

View file

@ -40,7 +40,7 @@ export default (
{
Array.from({ length: 10 }, () => 0)
.map(
(num, index) => <div
(num, index) => <div
key={`armor-${index}`}
className='armor'></div>
)
@ -48,5 +48,3 @@ export default (
</div>
</SharedHudVars>
}

26
src/react/BarsCommon.tsx Normal file
View file

@ -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)
}
}

View file

@ -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");
}

View file

@ -10,6 +10,7 @@
--bg-x: calc(-1 * 16px);
--bg-y: calc(-1 * 18px);
pointer-events: none;
image-rendering: pixelated;
}
.breath {

View file

@ -0,0 +1,18 @@
import type { Meta, StoryObj } from '@storybook/react'
import Button from './Button'
const meta: Meta<typeof Button> = {
component: Button,
}
export default meta
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
label: 'Hello!',
icon: 'pixelarticons:lock-open',
inScreen: false,
},
}

View file

@ -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<HTMLButtonElement>
}
void loadSound('button_click.mp3')
const ButtonContext = createContext({
onClick () { },
})
export const ButtonProvider: FC<{children, onClick}> = ({ children, onClick }) => {
return <ButtonContext.Provider value={{ onClick }}>{children}</ButtonContext.Provider>
}
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 <button ref={rootRef} {...args} className={classNames(buttonCss.button, args.className)} onClick={onClick} type={type}>
{icon && <iconify-icon class={buttonCss.icon} icon={icon}></iconify-icon>}
{label}
{children}
</button>
return <SharedHudVars>
<button ref={rootRef} {...args} className={classNames(buttonCss.button, args.className)} onClick={onClick} type={type}>
{icon && <iconify-icon class={buttonCss.icon} icon={icon}></iconify-icon>}
{label}
{children}
</button>
</SharedHudVars>
}) satisfies FC<Props>

View file

@ -0,0 +1,10 @@
import { loadSound, playSound } from '../basicSounds'
import { ButtonProvider } from './Button'
void loadSound('button_click.mp3')
export default ({ children }) => {
return <ButtonProvider onClick={() => {
void playSound('button_click.mp3')
}}>{children}</ButtonProvider>
}

View file

@ -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 ?? ''

View file

@ -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[],

View file

@ -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 <ChatContainer
return <Chat
allowSelection={chatSelect}
usingTouch={!!usingTouch}
opacity={(isChatActive ? chatOpacityOpened : chatOpacity) / 100}

View file

@ -3,7 +3,7 @@ import { hideCurrentModal, showModal } from '../globalState'
import defaultLocalServerOptions from '../defaultLocalServerOptions'
import { mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs'
import CreateWorld, { WorldCustomize, creatingWorldState } from './CreateWorld'
import { useIsModalActive } from './utils'
import { useIsModalActive } from './utilsApp'
import { getWorldsPath } from './SingleplayerProvider'
export default () => {

View file

@ -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 })

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import FoodBar from './FoodBar'
const meta: Meta<typeof FoodBar> = {
component: FoodBar
}
export default meta
type Story = StoryObj<typeof FoodBar>;
export const Primary: Story = {
args: {
gameMode: 'survival',
food: 10,
effectToAdd: 19,
effectToRemove: 20,
}
}

View file

@ -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<HTMLDivElement | null>(null)
@ -54,15 +55,17 @@ export default (
}, [food])
useEffect(() => {
effectAdded(foodRef.current, effectToAdd)
}, [effectToAdd])
if (effectToAdd) {
barEffectAdded(foodRef.current, effectToAdd)
}
if (effectToRemove) {
barEffectEnded(foodRef.current, effectToRemove)
}
resetEffects?.()
}, [effectToAdd, effectToRemove])
useEffect(() => {
effectEnded(foodRef.current, effectToRemove)
}, [effectToRemove])
return <SharedHudVars>
<div ref={foodRef} className='foodbar' >
return <SharedHudVars>
<div ref={foodRef} className='foodbar' style={style}>
{
Array.from({ length: 10 }, () => 0)
.map(

View file

@ -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)

View file

@ -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 {

View file

@ -9,19 +9,6 @@ const meta: Meta<typeof HealthBar> = {
export default meta
type Story = StoryObj<typeof HealthBar>;
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)
}
}
}

View file

@ -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<HTMLDivElement | null>(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 <SharedHudVars>
<div ref={healthRef} className='health' >
<div ref={healthRef} className='health' style={style}>
{
Array.from({ length: 10 }, () => 0)
.map(
(num, index) => <div
(num, index) => <div
key={`heart-${index}`}
className='heart'></div>
)
}
</div>
</SharedHudVars>}

View file

@ -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)
}}
/>
<ArmorBar
armorValue={armorValue}
@ -126,8 +112,10 @@ export default () => {
food={food}
effectToAdd={effectToAdd}
effectToRemove={effectToRemove}
effectAdded={effectAdded}
effectEnded={effectEnded}
resetEffects={() => {
setEffectToAdd(null)
setEffectToRemove(null)
}}
/>
<BreathBar
oxygen={gameMode !== 'survival' && gameMode !== 'adventure' ? 0 : oxygen}

View file

@ -1,6 +1,6 @@
import React, { useEffect, useRef } from 'react'
import { isMobile } from 'prismarine-viewer/viewer/lib/simpleUtils'
import styles from './input.module.css'
import { useUsingTouch } from './utils'
interface Props extends React.ComponentProps<'input'> {
rootStyles?: React.CSSProperties
@ -10,11 +10,10 @@ interface Props extends React.ComponentProps<'input'> {
export default ({ autoFocus, rootStyles, inputRef, ...inputProps }: Props) => {
const ref = useRef<HTMLInputElement>(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()
}, [])

View file

@ -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']) => {

View file

@ -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'

View file

@ -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 <iconify-icon icon={`pixelarticons:${iconName}`} style={styles} className={className} />
return <iconify-icon icon={`pixelarticons:${iconName}`} style={styles} class={className} />
}

View file

@ -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;

View file

@ -11,8 +11,14 @@ type Story = StoryObj<typeof PlayerListOverlay>;
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',

View file

@ -2,7 +2,7 @@ import MessageFormattedString from './MessageFormattedString'
import './PlayerListOverlay.css'
type PlayersLists = Array<Array<import('mineflayer').Player>>
type PlayersLists = Array<Array<Pick<import('mineflayer').Player, 'uuid'|'username'|'ping'>>>
type PlayerListOverlayProps = {
playersLists: PlayersLists,
@ -10,11 +10,12 @@ type PlayerListOverlayProps = {
tablistHeader: string | Record<string, any> | null,
tablistFooter: string | Record<string, any> | null,
serverIP: string
style?: React.CSSProperties
}
export default ({ playersLists, clientId, tablistHeader, tablistFooter, serverIP }: PlayerListOverlayProps) => {
export default ({ playersLists, clientId, tablistHeader, tablistFooter, serverIP, style }: PlayerListOverlayProps) => {
return <div className="playerlist-container" id="playerlist-container" >
return <div className="playerlist-container" id="playerlist-container" style={style}>
<span className="playerlist-title">Server IP: {serverIP}</span>
<div className='playerlist-header'>
<MessageFormattedString message={tablistHeader} />
@ -23,7 +24,7 @@ export default ({ playersLists, clientId, tablistHeader, tablistFooter, serverIP
{playersLists.map((list, index) => (
<div key={index} className="player-list">
{list.map(player => (
<div key={player.uuid} className={`playerlist-entry${clientId === player.uuid ? ' active-player' : ''}`} id={`plist-player-${player.uuid}`}>
<div key={player.uuid ?? player.username} className={`playerlist-entry${clientId === player.uuid ? ' active-player' : ''}`} id={`plist-player-${player.uuid}`}>
<MessageFormattedString message={player.username} />
<div className="playerlist-ping">
<p className="playerlist-ping-value">{player.ping}</p>

View file

@ -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 (
<div className='scoreboard-container'>
<div className='scoreboard-container' style={style}>
<div className='scoreboard-title'>
<MessageFormattedString message={title} />
</div>

View file

@ -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({

View file

@ -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,

View file

@ -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 <div
style={customVars}
>{children}</div>
return children
}

View file

@ -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'

View file

@ -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'

View file

@ -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<Props> = ({
}
return (
<div className={styles['slider-container']} style={{ width }} {...divProps}>
<input
type="range"
className={styles.slider}
min={min}
max={max}
value={value}
disabled={!!disabledReason}
onChange={(e) => {
const newValue = Number(e.target.value)
setValue(newValue)
fireValueUpdate(false, newValue)
}}
// todo improve correct handling of drag end
onLostPointerCapture={() => {
fireValueUpdate(true)
}}
onPointerUp={() => {
fireValueUpdate(true)
}}
onKeyUp={() => {
fireValueUpdate(true)
}}
/>
<div className={styles.disabled} title={disabledReason}></div>
<div className={styles['slider-thumb']} style={{ left: `calc((100% * ${ratio}) - (8px * ${ratio}))` }}></div>
<label className={styles.label}>
{label}: {valueDisplay ?? value} {unit}
</label>
</div>
<SharedHudVars>
<div className={styles['slider-container']} style={{ width }} {...divProps}>
<input
type="range"
className={styles.slider}
min={min}
max={max}
value={value}
disabled={!!disabledReason}
onChange={(e) => {
const newValue = Number(e.target.value)
setValue(newValue)
fireValueUpdate(false, newValue)
}}
// todo improve correct handling of drag end
onLostPointerCapture={() => {
fireValueUpdate(true)
}}
onPointerUp={() => {
fireValueUpdate(true)
}}
onKeyUp={() => {
fireValueUpdate(true)
}}
/>
<div className={styles.disabled} title={disabledReason}></div>
<div className={styles['slider-thumb']} style={{ left: `calc((100% * ${ratio}) - (8px * ${ratio}))` }}></div>
<label className={styles.label}>
{label}: {valueDisplay ?? value} {unit}
</label>
</div>
</SharedHudVars>
)
}

View file

@ -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)

View file

@ -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()

View file

@ -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({

45
src/react/globals.d.ts vendored Normal file
View file

@ -0,0 +1,45 @@
type StringKeys<T extends object> = Extract<keyof T, string>
interface ObjectConstructor {
keys<T extends object> (obj: T): Array<StringKeys<T>>
entries<T extends object> (obj: T): Array<[StringKeys<T>, T[keyof T]]>
// todo review https://stackoverflow.com/questions/57390305/trying-to-get-fromentries-type-right
fromEntries<T extends Array<[string, any]>> (obj: T): Record<T[number][0], T[number][1]>
assign<T extends Record<string, any>, K extends Record<string, any>> (target: T, source: K): asserts target is T & K
}
declare module '*.module.css' {
const css: Record<string, string>
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<T> (): {
resolve: (value: T) => void;
reject: (reason: any) => void;
promise: Promise<T>;
}
}
declare namespace JSX {
interface IntrinsicElements {
'iconify-icon': {
icon: string
style?: React.CSSProperties
class?: string
}
}
}

View file

@ -0,0 +1 @@
// export * from './npmReactComponents'

View file

@ -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)
}

15
src/react/utilsApp.ts Normal file
View file

@ -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}`)
}

View file

@ -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 = () => {
<DisplayQr />
</PerComponentErrorBoundary>
<RobustPortal to={document.body}>
{/* becaues of z-index */}
{/* because of z-index */}
<TouchControls />
<GlobalSearchInput />
</RobustPortal>
@ -132,21 +133,23 @@ const WidgetDisplay = ({ name, Component }) => {
const App = () => {
return <div>
<EnterFullscreenButton />
<InGameUi />
<RobustPortal to={document.querySelector('#ui-root')}>
<AllWidgets />
<SingleplayerProvider />
<CreateWorldProvider />
<AppStatusProvider />
<SelectOption />
<ServersListProvider />
<OptionsRenderApp />
<MainMenuRenderApp />
<NotificationProvider />
{/* <GameHud>
</GameHud> */}
</RobustPortal>
<ButtonAppProvider>
<EnterFullscreenButton />
<InGameUi />
<RobustPortal to={document.querySelector('#ui-root')}>
<AllWidgets />
<SingleplayerProvider />
<CreateWorldProvider />
<AppStatusProvider />
<SelectOption />
<ServersListProvider />
<OptionsRenderApp />
<MainMenuRenderApp />
<NotificationProvider />
{/* <GameHud>
</GameHud> */}
</RobustPortal>
</ButtonAppProvider>
</div>
}

View file

@ -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');
}

View file

@ -89,6 +89,8 @@ class WorldInteraction {
if (entity && e.button === 2) {
bot.attack(entity)
} else {
// bot
}
})
document.addEventListener('blur', (e) => {

12
tsconfig.npm.json Normal file
View file

@ -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"
]
}