This commit is contained in:
Vitaly 2024-08-15 03:12:32 +03:00 committed by GitHub
commit 71289e3ef4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 664 additions and 366 deletions

View file

@ -103,6 +103,47 @@
// "@stylistic/newline-per-chained-call": "error", // not sure if needed
"@stylistic/new-parens": "error",
"@stylistic/no-confusing-arrow": "error",
"@stylistic/wrap-iife": "error",
"@stylistic/space-before-blocks": "error",
"@stylistic/type-generic-spacing": "error",
"@stylistic/template-tag-spacing": "error",
"@stylistic/template-curly-spacing": "error",
"@stylistic/type-annotation-spacing": "error",
"@stylistic/jsx-child-element-spacing": "error",
// buggy
// "@stylistic/jsx-closing-bracket-location": "error",
// "@stylistic/jsx-closing-tag-location": "error",
"@stylistic/jsx-curly-brace-presence": "error",
"@stylistic/jsx-curly-newline": "error",
"@stylistic/jsx-curly-spacing": "error",
"@stylistic/jsx-equals-spacing": "error",
"@stylistic/jsx-first-prop-new-line": "error",
"@stylistic/jsx-function-call-newline": "error",
"@stylistic/jsx-max-props-per-line": [
"error",
{
"maximum": 7
}
],
"@stylistic/jsx-pascal-case": "error",
"@stylistic/jsx-props-no-multi-spaces": "error",
"@stylistic/jsx-self-closing-comp": "error",
// "@stylistic/jsx-sort-props": [
// "error",
// {
// "callbacksLast": false,
// "shorthandFirst": true,
// "shorthandLast": false,
// "multiline": "ignore",
// "ignoreCase": true,
// "noSortAlphabetically": true,
// "reservedFirst": [
// "key",
// "className"
// ],
// "locale": "auto"
// }
// ],
// perf
"import/no-deprecated": "off",
// ---

View file

@ -5,6 +5,8 @@ After forking the repository, run the following commands to get started:
0. Ensure you have [Node.js](https://nodejs.org) and `pnpm` installed. To install pnpm run `npm i -g pnpm@9.0.4`.
1. Install dependencies: `pnpm i`
2. Start the project in development mode: `pnpm start`
3. Read the [Tasks Categories](#tasks-categories) and [Workflow](#workflow) sections below
4. Let us know if you are working on something and be sure to open a PR if you got any changes. Happy coding!
## Project Structure
@ -57,6 +59,20 @@ How different modules are used:
- `mineflayer` - provider `bot` variable and as mineflayer states it is a wrapper for the `node-minecraft-protocol` module and is used to connect and interact with real Java Minecraft servers. However not all events & properties are exposed and sometimes you have to use `bot._client.on('packet_name', data => ...)` to handle packets that are not handled via mineflayer API. Also you can use almost any mineflayer plugin.
## Running Main App + Playground
To start the main web app and playground, run `pnpm run-all`. Note is doesn't start storybook and tests.
## Cypress Tests (E2E)
Cypress tests are located in `cypress` folder. To run them, run `pnpm test-mc-server` and then `pnpm test:cypress` when the `pnpm prod-start` is running (or change the port to 3000 to test with the dev server). Usually you don't need to run these until you get issues on the CI.
## Unit Tests
There are not many unit tests for now (which we are trying to improve).
Location of unit tests: `**/*.test.ts` files in `src` folder and `prismarine-viewer` folder.
Start them with `pnpm test-unit`.
## Making protocol-related changes
You can get a description of packets for the latest protocol version from <https://wiki.vg/Protocol> and for previous protocol versions from <https://wiki.vg/Protocol_version_numbers> (look for *Page* links that have *Protocol* in URL).
@ -75,6 +91,84 @@ Also there are [src/generatedClientPackets.ts](src/generatedClientPackets.ts) an
- Use `start-prod` script to start the project in production mode after running the `build` script to build the project.
- If CI is failing on the next branch for some reason, feel free to use the latest commit for release branch. We will update the base branch asap. Please, always make sure to allow maintainers do changes when opening PRs.
## Tasks Categories
(most important for now are on top).
## 1. Client-side Logic (most important right now)
Everything related to the client side packets. Investigate issues when something goes wrong with some server. It's much easier to work on these types of tasks when you have experience in Java with Minecraft, a deep understanding of the original client, and know how to debug it (which is not hard actually). Right now the client is easily detectable by anti-cheat plugins, and the main goal is to fix it (mostly because of wrong physics implementation).
Priority tasks:
- Rewrite or fix the physics logic (Botcraft or Grim can be used as a reference as well)
- Implement basic minecart / boat / horse riding
- Fix auto jump module (false triggers, performance issues)
- Investigate connection issues to some servers
- Setup a platform for automatic cron testing against the latest version of the anti-cheat plugins
- ...
Goals:
- Make more servers playable. Right now on hypixel-like servers (servers with minigames), only tnt run (and probably ) is fully playable.
Notes:
- You can see the incoming/outgoing packets in the console (F12 in Chrome) by enabling `options.debugLogNotFrequentPackets = true`. However, if you need a FULL log of all packets, you can start recording the packets by going into `Settings` > `Advanced` > `Enable Packets Replay` and then you can download the file and use it to replay the packets.
- You can use mcraft-e2e studio to send the same packets over and over again (which is useful for testing) or use the packets replayer (which is useful for debugging).
## 2. Three.js Renderer
Example tasks:
- Improve / fix entity rendering
- Better update entities on specific packets
- Investigate performance issues under different conditions (instructions provided)
- Work on the playground code
Goals:
- Fix a lot of entity rendering issues (including position updates)
- Implement switching camera mode (first person, third person, etc)
- Animated blocks
- Armor rendering
- ...
Note:
- It's useful to know how to use helpers & additional cameras (e.g. setScissor)
## 3. Server-side Logic
Flying squid fork (space-squid).
Example tasks:
- Add missing commands (e.g. /scoreboard)
- Basic physics (player fall damage, falling blocks & entities)
- Basic entities AI (spawning, attacking)
- Pvp
- Emit more packets on some specific events (e.g. when a player uses an item)
- Make more maps playable (e.g. fix when something is not implemented in both server and client and blocking map interaction)
- ...
Long Term Goals:
- Make most adventure maps playable
- Make a way to complete the game from the scratch (crafting, different dimensions, terrain generation, etc)
- Make bedwars playable!
Most of the tasks are straightforward to implement, just be sure to use a debugger ;). If you feel you are stuck, ask for help on Discord. Absolutely any tests / refactor suggestions are welcome!
## 4. Frontend
New React components, improve UI (including mobile support).
## Workflow
1. Locate the problem on the public test server & make an easily reproducible environment (you can also use local packets replay server or your custom server setup). Dm me for details on public test server / replay server
2. Debug the code, find an issue in the code, isolate the problem
3. Develop, try to fix and test. Finally we should find a way to fix it. It's ideal to have an automatic test but it's not necessary for now
3. Repeat step 1 to make sure the task is done and the problem is fixed (or the feature is implemented)
### Would be useful to have
- cleanup folder & modules structure, cleanup playground code

View file

@ -13,7 +13,8 @@ function InnerSearch () {
margin: 'auto',
zIndex: 11,
width: 'min-content',
}}>
}}
>
<Input
autoFocus={currentTouch === false}
width={50}

View file

@ -15,7 +15,6 @@ import 'core-js/features/array/at'
import 'core-js/features/promise/with-resolvers'
import './scaleInterface'
import itemsPng from 'prismarine-viewer/public/textures/items.png'
import { initWithRenderer } from './topRightStats'
import PrismarineBlock from 'prismarine-block'
import PrismarineItem from 'prismarine-item'

View file

@ -23,13 +23,23 @@ export const guiOptionsScheme: {
const [frameLimitMax, setFrameLimitMax] = useState(null as number | null)
return <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Slider style={{ width: 130 }} label='Frame Limit' disabledReason={frameLimitMax ? undefined : 'press lock button first'} unit={frameLimitValue ? 'fps' : ''} valueDisplay={frameLimitValue || 'VSync'} value={frameLimitValue || frameLimitMax! + 1} min={20} max={frameLimitMax! + 1} updateValue={(newVal) => {
options.frameLimit = newVal > frameLimitMax! ? false : newVal
}} />
<Button style={{ width: 20 }} icon='pixelarticons:lock-open' onClick={async () => {
const rate = await getScreenRefreshRate()
setFrameLimitMax(rate)
}} />
<Slider
style={{ width: 130 }}
label='Frame Limit'
disabledReason={frameLimitMax ? undefined : 'press lock button first'}
unit={frameLimitValue ? 'fps' : ''}
valueDisplay={frameLimitValue || 'VSync'}
value={frameLimitValue || frameLimitMax! + 1} min={20}
max={frameLimitMax! + 1} updateValue={(newVal) => {
options.frameLimit = newVal > frameLimitMax! ? false : newVal
}}
/>
<Button
style={{ width: 20 }} icon='pixelarticons:lock-open' onClick={async () => {
const rate = await getScreenRefreshRate()
setFrameLimitMax(rate)
}}
/>
</div>
}
},
@ -94,7 +104,8 @@ export const guiOptionsScheme: {
unit: '',
max: sp ? 16 : 12,
min: 1
}} />
}}
/>
},
},
{
@ -122,39 +133,41 @@ export const guiOptionsScheme: {
const { resourcePackInstalled } = useSnapshot(resourcePackState)
const { usingServerResourcePack } = useSnapshot(loadedGameState)
const { enabledResourcepack } = useSnapshot(options)
return <Button label={`Resource Pack: ${usingServerResourcePack ? 'SERVER ON' : resourcePackInstalled ? enabledResourcepack ? 'ON' : 'OFF' : 'NO'}`} inScreen onClick={async () => {
if (resourcePackState.resourcePackInstalled) {
const names = Object.keys(await getResourcePackNames())
const name = names[0]
const choices = [
options.enabledResourcepack ? 'Disable' : 'Enable',
'Uninstall',
]
const choice = await showOptionsModal(`Resource Pack ${name} action`, choices)
if (!choice) return
if (choice === 'Disable') {
options.enabledResourcepack = null
return
}
if (choice === 'Enable') {
options.enabledResourcepack = name
await completeTexturePackInstall(name, name)
return
}
if (choice === 'Uninstall') {
return <Button
label={`Resource Pack: ${usingServerResourcePack ? 'SERVER ON' : resourcePackInstalled ? enabledResourcepack ? 'ON' : 'OFF' : 'NO'}`} inScreen onClick={async () => {
if (resourcePackState.resourcePackInstalled) {
const names = Object.keys(await getResourcePackNames())
const name = names[0]
const choices = [
options.enabledResourcepack ? 'Disable' : 'Enable',
'Uninstall',
]
const choice = await showOptionsModal(`Resource Pack ${name} action`, choices)
if (!choice) return
if (choice === 'Disable') {
options.enabledResourcepack = null
return
}
if (choice === 'Enable') {
options.enabledResourcepack = name
await completeTexturePackInstall(name, name)
return
}
if (choice === 'Uninstall') {
// todo make hidable
setLoadingScreenStatus('Uninstalling texturepack')
await uninstallTexturePack()
setLoadingScreenStatus(undefined)
}
} else {
setLoadingScreenStatus('Uninstalling texturepack')
await uninstallTexturePack()
setLoadingScreenStatus(undefined)
}
} else {
// if (!fsState.inMemorySave && isGameActive(false)) {
// alert('Unable to install resource pack in loaded save for now')
// return
// }
openFilePicker('resourcepack')
}
}} />
openFilePicker('resourcepack')
}
}}
/>
},
},
{
@ -224,7 +237,8 @@ export const guiOptionsScheme: {
onClick={() => {
showModal({ reactType: 'keybindings' })
}}
>Keybindings</Button>
>Keybindings
</Button>
},
mouseSensX: {},
mouseSensY: {
@ -325,9 +339,12 @@ export const guiOptionsScheme: {
advanced: [
{
custom () {
return <Button inScreen onClick={() => {
if (confirm('Are you sure you want to reset all settings?')) resetLocalStorageWithoutWorld()
}}>Reset all settings</Button>
return <Button
inScreen
onClick={() => {
if (confirm('Are you sure you want to reset all settings?')) resetLocalStorageWithoutWorld()
}}
>Reset all settings</Button>
},
},
{
@ -338,17 +355,24 @@ export const guiOptionsScheme: {
{
custom () {
const { active } = useSnapshot(packetsReplaceSessionState)
return <Button inScreen onClick={() => {
packetsReplaceSessionState.active = !active
}}>{active ? 'Disable' : 'Enable'} Packets Replay</Button>
return <Button
inScreen
onClick={() => {
packetsReplaceSessionState.active = !active
}}
>{active ? 'Disable' : 'Enable'} Packets Replay</Button>
},
},
{
custom () {
const { active } = useSnapshot(packetsReplaceSessionState)
return <Button disabled={!active} inScreen onClick={() => {
void downloadPacketsReplay()
}}>Download Packets Replay</Button>
return <Button
disabled={!active}
inScreen
onClick={() => {
void downloadPacketsReplay()
}}
>Download Packets Replay</Button>
},
}
],

View file

@ -83,7 +83,8 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
display: 'grid',
gap: 3,
gridTemplateColumns: smallWidth ? '1fr' : '1fr 1fr'
}}>
}}
>
{!lockConnect && <>
<div style={{ gridColumn: smallWidth ? '' : 'span 2', display: 'flex', justifyContent: 'center' }}>
<InputWithLabel label="Server Name" value={serverName} onChange={({ target: { value } }) => setServerName(value)} placeholder='Defaults to IP' />
@ -98,7 +99,8 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
<label style={{
display: 'flex',
flexDirection: 'column',
}}>
}}
>
<span style={{ fontSize: 12, marginBottom: 1, color: 'lightgray' }}>Account Override</span>
<select
onChange={({ target: { value } }) => setAccountIndex(Number(value))}
@ -146,7 +148,8 @@ const InputWithLabel = ({ label, span, ...props }: React.ComponentProps<typeof I
display: 'flex',
flexDirection: 'column',
gridRow: span ? 'span 2 / span 2' : undefined,
}}>
}}
>
<label style={{ fontSize: 12, marginBottom: 1, color: 'lightgray' }}>{label}</label>
<Input rootStyles={{ width: ELEMENTS_WIDTH }} {...props} />
</div>

View file

@ -34,7 +34,8 @@ export default ({ status, isError, hideDots = false, lastStatus = '', backAction
<span style={{
userSelect: isError ? 'text' : undefined,
wordBreak: 'break-word',
}}>
}}
>
{status}
</span>
{isError || hideDots ? '' : loadingDots}
@ -48,7 +49,7 @@ export default ({ status, isError, hideDots = false, lastStatus = '', backAction
<>
{backAction && <Button label="Back" onClick={backAction} />}
{actionsSlot}
<Button onClick={() => window.location.reload()} label="Reset App (recommended)"></Button>
<Button onClick={() => window.location.reload()} label="Reset App (recommended)" />
</>
)}
</Screen>

View file

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

View file

@ -14,7 +14,8 @@ export default () => {
right: 0,
display: 'flex',
justifyContent: 'center',
}}>
}}
>
<Button onClick={() => {
void bot.wake()
hideCurrentModal()

View file

@ -236,7 +236,8 @@ const Book: React.FC<BookProps> = ({ textPages, editable, onSign, onEdit, onClos
? styles.titleAnimationReverse
: ''
}`}
alt="Title Icon" />
alt="Title Icon"
/>
<div className={`${styles.inside}`}>
{renderPage(currentPage * (isSinglePage ? 1 : 2))}
{!isSinglePage && (currentPage * 2 + 1) < pages.length && renderPage(currentPage * 2 + 1)}
@ -274,17 +275,19 @@ const Book: React.FC<BookProps> = ({ textPages, editable, onSign, onEdit, onClos
: animateTitleIcon === 2
? styles.titleContentAnimationReverse
: ''
}`}>
}`}
>
{editable ? (
<div className={`${styles.titleContent}`} >
<MessageFormattedString message="Enter Book Title: " />
<form onSubmit={(e) => {
e.preventDefault()
handleSign()
}}>
}}
>
<input
ref={inputRef}
className={''}
className=""
/>
{/* for some reason this is needed to make Enter work on android chrome */}
<button type='submit' style={{ visibility: 'hidden', height: 0, width: 0 }} />

View file

@ -52,11 +52,10 @@ export default ({ bar }: { bar: BossBarType }) => {
<div className="bossbar-container">
<div className="bossbar-title"><MessageFormattedString message={title} /></div>
<div className="bossbar" style={bossBarStyles}>
<div className="fill" style={fillStyles}></div>
<div className="fill" style={div1Styles}></div>
<div className="fill" style={div2Styles}></div>
<div className="fill" style={fillStyles} />
<div className="fill" style={div1Styles} />
<div className="fill" style={div2Styles} />
</div>
</div>
)
}

View file

@ -45,7 +45,7 @@ export default ({
.map((num, index) => <div
key={`breath-${index}`}
className='breath'
></div>)
/>)
}
</div>
</SharedHudVars>

View file

@ -52,20 +52,23 @@ export default ({ initialTooltip, ...args }: Props) => {
return <>
<Button {...args} rootRef={refs.setReference} />
<div ref={refs.setFloating} style={{
...floatingStyles,
background: 'rgba(0, 0, 0, 0.3)',
fontSize: 8,
pointerEvents: 'none',
userSelect: 'text',
padding: '2px 4px',
opacity: showTooltips ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
textShadow: '1px 1px 2px BLACK',
zIndex: 11
}}>
<div
ref={refs.setFloating}
style={{
...floatingStyles,
background: 'rgba(0, 0, 0, 0.3)',
fontSize: 8,
pointerEvents: 'none',
userSelect: 'text',
padding: '2px 4px',
opacity: showTooltips ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
textShadow: '1px 1px 2px BLACK',
zIndex: 11
}}
>
{initialTooltip.content}
<FloatingArrow ref={arrowRef} context={context} style={{ opacity: 0.7 }}></FloatingArrow>
<FloatingArrow ref={arrowRef} context={context} style={{ opacity: 0.7 }} />
</div>
</>
}

View file

@ -73,17 +73,20 @@ const meta: Meta<typeof Chat> = {
return <div style={{
marginTop: args.usingTouch ? 100 : 0
}}>
}}
>
<div style={{ fontSize: 6, userSelect: 'auto', color: 'gray' }}>Hint: you can capture needed message with <code>bot.on('message', console.log)</code>, copy object, and assign it here to <code>window.spamMessage</code> variable (but ensure the correct frame window is selected in devtools)</div>
<Chat {...args} opened={open} messages={messages} onClose={() => setOpen(false)} fetchCompletionItems={async (triggerType, value) => {
console.log('fetchCompletionItems')
await new Promise(resolve => {
setTimeout(resolve, 0)
})
let items = ['test', ...Array.from({ length: 50 }).map((_, i) => `minecraft:hello${i}`)]
if (value === '/') items = items.map(item => `/${item}`)
return items
}} />
<Chat
{...args} opened={open} messages={messages} onClose={() => setOpen(false)} fetchCompletionItems={async (triggerType, value) => {
console.log('fetchCompletionItems')
await new Promise(resolve => {
setTimeout(resolve, 0)
})
let items = ['test', ...Array.from({ length: 50 }).map((_, i) => `minecraft:hello${i}`)]
if (value === '/') items = items.map(item => `/${item}`)
return items
}}
/>
<Button onClick={() => setOpen(s => !s)}>Open: {open ? 'on' : 'off'}</Button>
<Button onClick={() => fadeMessages()}>Fade</Button>
<Button onClick={() => setAutoSpam(s => !s)}>Auto Spam: {autoSpam ? 'on' : 'off'}</Button>

View file

@ -212,9 +212,11 @@ export default ({
return (
<>
<div className={`chat-wrapper chat-messages-wrapper ${usingTouch ? 'display-mobile' : ''}`} style={{
userSelect: opened && allowSelection ? 'text' : undefined,
}}>
<div
className={`chat-wrapper chat-messages-wrapper ${usingTouch ? 'display-mobile' : ''}`} style={{
userSelect: opened && allowSelection ? 'text' : undefined,
}}
>
{opacity && <div ref={chatMessages} className={`chat ${opened ? 'opened' : ''}`} id="chat-messages" style={{ opacity }}>
{messages.map((m) => (
<MessageLine key={reactKeyForMessage(m)} message={m} />
@ -246,7 +248,8 @@ export default ({
onClose?.()
}
}
}}>
}}
>
{isIos && <input
value=''
type="text"

View file

@ -2,7 +2,7 @@
import type { Meta, StoryObj } from '@storybook/react'
import Button from './Button'
const defaultIcon = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 21H5H19H21V3H19H5H3V21ZM19 5V19H5V5H19ZM11 17H13V11H15V9H13V7H11V9H9V11H11V17ZM9 13V11H7V13H9ZM17 13H15V11H17V13Z" fill="currentColor"></path></svg>
const defaultIcon = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 21H5H19H21V3H19H5H3V21ZM19 5V19H5V5H19ZM11 17H13V11H15V9H13V7H11V9H9V11H11V17ZM9 13V11H7V13H9ZM17 13H15V11H17V13Z" fill="currentColor" /></svg>
const Button2 = ({ title, icon }) => {
//@ts-expect-error
@ -23,7 +23,8 @@ const Comp = () => {
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 10
}}>
}}
>
<Button2 title="/give" icon={defaultIcon} />
<Button2 title="/tell" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M4 2h18v16H6v2H4v-2h2v-2h14V4H4v18H2V2h2zm5 7H7v2h2V9zm2 0h2v2h-2V9zm6 0h-2v2h2V9z" fill="currentColor" /> </svg>} />
<Button2 title="/setblock" icon={<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M2 2h20v20H2V2zm2 2v4h4V4H4zm6 0v4h4V4h-4zm6 0v4h4V4h-4zm4 6h-4v4h4v-4zm0 6h-4v4h4v-4zm-6 4v-4h-4v4h4zm-6 0v-4H4v4h4zm-4-6h4v-4H4v4zm6-4v4h4v-4h-4z" fill="currentColor" /> </svg>} />

View file

@ -29,10 +29,12 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
}, [])
return <Screen title="Create world" backdrop="dirt">
<form style={{ display: 'flex' }} onSubmit={(e) => {
e.preventDefault()
createClick()
}}>
<form
style={{ display: 'flex' }} onSubmit={(e) => {
e.preventDefault()
createClick()
}}
>
<Input
autoFocus
value={title}
@ -41,12 +43,14 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
}}
placeholder='World name'
/>
<select value={version} style={{
background: 'gray',
color: 'white'
}} onChange={({ target: { value } }) => {
creatingWorldState.version = value
}}>
<select
value={version} style={{
background: 'gray',
color: 'white'
}} onChange={({ target: { value } }) => {
creatingWorldState.version = value
}}
>
{versions.map(({ version, label }) => {
return <option key={version} value={version}>{label}</option>
})}
@ -56,14 +60,17 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
<Button onClick={() => {
const index = worldTypes.indexOf(type)
creatingWorldState.type = worldTypes[index === worldTypes.length - 1 ? 0 : index + 1]
}}>World Type: {type}</Button>
}}
>World Type: {type}
</Button>
{/* <Button onClick={() => customizeClick()} disabled>
Customize
</Button> */}
<Button onClick={() => {
const index = gameModes.indexOf(gameMode)
creatingWorldState.gameMode = gameModes[index === gameModes.length - 1 ? 0 : index + 1]
}}>
}}
>
Gamemode: {gameMode}
</Button>
</div>
@ -72,7 +79,9 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
<div style={{ display: 'flex' }}>
<Button onClick={() => {
cancelClick()
}}>Cancel</Button>
}}
>Cancel
</Button>
<Button disabled={!title} onClick={createClick}>Create</Button>
</div>
<div className='muted' style={{ fontSize: 9 }}>Note: store important saves in folders on the drive!</div>
@ -85,9 +94,7 @@ export const WorldCustomize = ({ backClick }) => {
return <Screen title='Customize world' backdrop='dirt'>
<div className={styles.world_layers_container}>
<div className="world_layer">
</div>
<div className="world_layer" />
</div>
<Button onClick={backClick}>Back</Button>
</Screen>

View file

@ -60,11 +60,13 @@ export default () => {
return <SharedHudVars>
<div className='crosshair' />
{displayIndicator && <div className='crosshair-indicator' style={{
{displayIndicator && <div
className='crosshair-indicator' style={{
//@ts-expect-error
'--crosshair-indicator-size': `${indicatorSize}px`,
borderLeft: `solid ${indicatorSize * indicatorProgress}px white`,
backgroundColor: alternativeIndicator ? 'dodgerblue' : undefined,
}} />}
'--crosshair-indicator-size': `${indicatorSize}px`,
borderLeft: `solid ${indicatorSize * indicatorProgress}px white`,
backgroundColor: alternativeIndicator ? 'dodgerblue' : undefined,
}}
/>}
</SharedHudVars>
}

View file

@ -18,12 +18,16 @@ export default ({ dieReasonMessage, respawnCallback, disconnectCallback }: Props
<MessageFormatted parts={dieReasonMessage} />
</h5>
<div className='deathScreen-buttons-grouped'>
<Button label="Respawn" onClick={() => {
respawnCallback()
}} />
<Button label="Disconnnect" onClick={() => {
disconnectCallback()
}} />
<Button
label="Respawn" onClick={() => {
respawnCallback()
}}
/>
<Button
label="Disconnnect" onClick={() => {
disconnectCallback()
}}
/>
</div>
</div>
</div>

View file

@ -130,7 +130,7 @@ export default () => {
<p>Prismarine Web Client ({bot.version})</p>
<p>E: {entitiesCount}</p>
<p>{dimension}</p>
<div className={styles.empty}></div>
<div className={styles.empty} />
<p>XYZ: {pos.x.toFixed(3)} / {pos.y.toFixed(3)} / {pos.z.toFixed(3)}</p>
<p>Chunk: {Math.floor(pos.x % 16)} ~ {Math.floor(pos.z % 16)} in {Math.floor(pos.x / 16)} ~ {Math.floor(pos.z / 16)}</p>
<p>Packets: {packetsString}</p>
@ -140,13 +140,13 @@ export default () => {
<p>Biome: minecraft:{loadedData.biomesArray[biomeId]?.name ?? 'unknown biome'}</p>
<p>Day: {day}</p>
<div className={styles.empty}></div>
<div className={styles.empty} />
{Object.entries(customEntries.current).map(([name, value]) => <p key={name}>{name}: {value}</p>)}
</div>
<div className={styles['debug-right-side']}>
<p>Renderer: {rendererDevice} powered by three.js r{THREE.REVISION}</p>
<div className={styles.empty}></div>
<div className={styles.empty} />
{cursorBlock ? (<>
<p>{cursorBlock.name}</p>
{
@ -154,7 +154,7 @@ export default () => {
return <p key={name}>
{name}: {
typeof value === 'boolean' ? (
<span style={{ color: value ? 'lightgreen' : 'red' }}>{value}</span>
<span style={{ color: value ? 'lightgreen' : 'red' }}>{String(value)}</span>
) : value
}
</p>

View file

@ -67,7 +67,8 @@ export const DropdownButton = ({ text, links }: { text: string, links: DropdownB
key={el.text}
style={{ width: '98px', fontSize: '7px' }}
onClick={el.clickHandler}
>{el.text}</Button>
>{el.text}
</Button>
})}
</div>
}

View file

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

View file

@ -1,18 +1,20 @@
import './GoogleButton.css'
export default ({ onClick }) => {
return <button className="gsi-material-button" onClick={onClick} style={{
transform: 'scale(0.55) translate(-20%)',
}}>
return <button
className="gsi-material-button" onClick={onClick} style={{
transform: 'scale(0.55) translate(-20%)',
}}
>
<div className="gsi-material-button-state" />
<div className="gsi-material-button-content-wrapper">
<div className="gsi-material-button-icon">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path>
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path>
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path>
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path>
<path fill="none" d="M0 0h48v48H0z"></path>
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z" />
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z" />
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z" />
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z" />
<path fill="none" d="M0 0h48v48H0z" />
</svg>
</div>
<span className="gsi-material-button-contents">Continue with Google</span>

View file

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

View file

@ -45,11 +45,14 @@ export default () => {
bottom: 20,
left: 8,
pointerEvents: 'none',
}}>
<img src={dataUrl} style={{
width: 92,
height: 92,
imageRendering: 'pixelated',
}} />
}}
>
<img
src={dataUrl} style={{
width: 92,
height: 92,
imageRendering: 'pixelated',
}}
/>
</div> : null
}

View file

@ -205,16 +205,18 @@ export default () => {
return <SharedHudVars>
<ItemName itemKey={itemKey} />
<Portal>
<div className='hotbar' ref={container} style={{
position: 'fixed',
left: 0,
right: 0,
display: 'flex',
justifyContent: 'center',
zIndex: hasModals ? 1 : 8,
pointerEvents: 'none',
bottom: 'var(--hud-bottom-raw)'
}} />
<div
className='hotbar' ref={container} style={{
position: 'fixed',
left: 0,
right: 0,
display: 'flex',
justifyContent: 'center',
zIndex: hasModals ? 1 : 8,
pointerEvents: 'none',
bottom: 'var(--hud-bottom-raw)'
}}
/>
</Portal>
</SharedHudVars>
}

View file

@ -87,10 +87,12 @@ export default ({ indicators, effects }: { indicators: typeof defaultIndicatorsS
return <div className='effectsScreen-container'>
<div className='indicators-container'>
{
indicatorsMapped.map((indicator) => <div key={indicator.icon} style={{
opacity: indicator.state ? 1 : 0,
transition: 'opacity 0.1s',
}}>
indicatorsMapped.map((indicator) => <div
key={indicator.icon} style={{
opacity: indicator.state ? 1 : 0,
transition: 'opacity 0.1s',
}}
>
<PixelartIcon iconName={indicator.icon} />
</div>)
}

View file

@ -18,6 +18,9 @@ export default ({ autoFocus, rootStyles, inputRef, ...inputProps }: Props) => {
}, [])
return <div className={styles.container} style={rootStyles}>
<input ref={ref} className={styles.input} autoComplete='off' autoCapitalize='off' autoCorrect='off' autoSave='off' spellCheck='false' {...inputProps} />
<input
ref={ref} className={styles.input} autoComplete='off' autoCapitalize='off' autoCorrect='off' autoSave='off' spellCheck='false'
{...inputProps}
/>
</div>
}

View file

@ -64,7 +64,7 @@ export default ({
})}
<Button
onClick={() => addNewCommand(group)}
icon={'pixelarticons:add-box'}
icon="pixelarticons:add-box"
style={{
alignSelf: 'center'
}}
@ -103,9 +103,12 @@ const CustomCommandContainer = ({
if (!config) return null
return config.type === 'select'
? <select key={indexInput} onChange={(e) => {
setInputValue(commandKey, indexInput, e.target.value)
}}>{config.options.map((option) => <option key={option} value={option}>{option}</option>)}</select>
? <select
key={indexInput} onChange={(e) => {
setInputValue(commandKey, indexInput, e.target.value)
}}
>{config.options.map((option) => <option key={option} value={option}>{option}</option>)}
</select>
: <Input key={indexInput} rootStyles={{ width: '99%' }} placeholder={config.placeholder} value={inputs[indexInput] ?? ''} onChange={(e) => setInputValue(commandKey, indexInput, e.target.value)} />
})}
<div className={styles.actionBinds}>
@ -116,21 +119,22 @@ const CustomCommandContainer = ({
resetBinding('custom', commandKey, 'keyboard')
}}
className={styles['undo-keyboard']}
icon={'pixelarticons:undo'}
icon="pixelarticons:undo"
/>
: null}
: null
}
{[0, 1].map((key, index) => <ButtonWithMatchesAlert
key={`custom-keyboard-${group}-${commandKey}-${index}`}
group={'custom'}
group="custom"
action={commandKey}
index={index}
inputType={'keyboard'}
inputType="keyboard"
keys={keys}
gamepad={gamepad}
/>)}
<div style={{ marginRight: 'auto' }} ></div>
<div style={{ marginRight: 'auto' }} />
{
userConfig?.['custom']?.[commandKey]?.gamepad ? <Button
@ -139,14 +143,15 @@ const CustomCommandContainer = ({
resetBinding('custom', commandKey, 'gamepad')
}}
className={styles['undo-keyboard']}
icon={'pixelarticons:undo'}
icon="pixelarticons:undo"
/>
: null}
: null
}
<ButtonWithMatchesAlert
group={'custom'}
group="custom"
action={commandKey}
index={0}
inputType={'gamepad'}
inputType="gamepad"
keys={keys}
gamepad={gamepad}
/>
@ -157,9 +162,8 @@ const CustomCommandContainer = ({
return newConfig
})
}}
style={{ color: 'red' }}
icon={'pixelarticons:delete'}
icon="pixelarticons:delete"
/>
</div>
</div>

View file

@ -178,16 +178,19 @@ export default ({
setUserConfig,
handleClick,
bindsMap: bindsMap.current
}}>
}}
>
<Screen title="Keybindings" backdrop>
{awaitingInputType && <AwaitingInputOverlay isGamepad={awaitingInputType === 'gamepad'} />}
<div className={styles.container}
<div
className={styles.container}
ref={containerRef}
>
<Button
onClick={() => { hideModal() }}
style={{ alignSelf: 'center' }}
>Back</Button>
>Back
</Button>
{Object.entries(commands).map(([group, actions], index) => {
if (group === 'custom') return null
@ -198,7 +201,8 @@ export default ({
color: 'rgba(255, 255, 255, 0.7)',
fontSize: '6px',
textAlign: 'center'
}}>
}}
>
Note: Left, right and middle click keybindings are hardcoded and cannot be changed currently.
</div>
) : null}
@ -221,7 +225,7 @@ export default ({
group={group}
action={action}
index={index}
inputType={'keyboard'}
inputType="keyboard"
keys={keys}
gamepad={gamepad}
/>)}
@ -244,7 +248,7 @@ export default ({
group={group}
action={action}
index={0}
inputType={'gamepad'}
inputType="gamepad"
keys={keys}
gamepad={gamepad}
/>
@ -306,7 +310,7 @@ export const ButtonWithMatchesAlert = ({
//@ts-format-ignore-region
<div id={`bind-warning-${group}-${action}-${inputType}-${index}`} className={styles['matched-bind-warning']}>
<PixelartIcon
iconName={'alert'}
iconName="alert"
width={5}
styles={{
display: 'flex',
@ -316,13 +320,12 @@ export const ButtonWithMatchesAlert = ({
}}
/>
<div>
This bind is already in use. <span></span>
This bind is already in use. <span />
</div>
</div>
)
//@ts-format-ignore-endregion
: null
}
: null}
</div>
}

View file

@ -40,7 +40,7 @@ export default ({
<div className={styles.root}>
<div className={styles['game-title']}>
<div className={styles.minecraft}>
<div className={styles.edition}></div>
<div className={styles.edition} />
<span className={styles.splash}>Prismarine is a beautiful block</span>
</div>
</div>
@ -108,10 +108,13 @@ export default ({
Prismarine Web Client {versionStatus}
</span>
<span className={styles['product-description']}>
<a style={{
color: 'lightgray',
fontSize: 9,
}} href='https://privacy.mcraft.fun'>Privacy Policy</a>
<a
style={{
color: 'lightgray',
fontSize: 9,
}} href='https://privacy.mcraft.fun'
>Privacy Policy
</a>
<span>A Minecraft client in the browser!</span>
</span>
</div>

View file

@ -35,24 +35,34 @@ export default () => {
// ios note: just don't use <button>
return <div ref={elRef} className={styles['mobile-top-btns']} id="mobile-top">
<div className={styles['debug-btn']} onPointerDown={(e) => {
window.dispatchEvent(new MouseEvent('mousedown', { button: 1 }))
}}>S</div>
<div className={styles['debug-btn']} onPointerDown={(e) => {
document.dispatchEvent(new KeyboardEvent('keydown', { code: 'F3' }))
document.dispatchEvent(new KeyboardEvent('keyup', { code: 'F3' }))
}} { ...longPressEvent }>F3</div>
<div className={styles['chat-btn']} onPointerDown={(e) => {
e.stopPropagation()
if (activeModalStack.at(-1)?.reactType === 'chat') {
hideCurrentModal()
} else {
showModal({ reactType: 'chat' })
}
}}></div>
<div className={styles['pause-btn']} onPointerDown={(e) => {
e.stopPropagation()
showModal({ reactType: 'pause-screen' })
}}></div>
<div
className={styles['debug-btn']} onPointerDown={(e) => {
window.dispatchEvent(new MouseEvent('mousedown', { button: 1 }))
}}
>S
</div>
<div
className={styles['debug-btn']} onPointerDown={(e) => {
document.dispatchEvent(new KeyboardEvent('keydown', { code: 'F3' }))
document.dispatchEvent(new KeyboardEvent('keyup', { code: 'F3' }))
}} {...longPressEvent}
>F3
</div>
<div
className={styles['chat-btn']} onPointerDown={(e) => {
e.stopPropagation()
if (activeModalStack.at(-1)?.reactType === 'chat') {
hideCurrentModal()
} else {
showModal({ reactType: 'chat' })
}
}}
/>
<div
className={styles['pause-btn']} onPointerDown={(e) => {
e.stopPropagation()
showModal({ reactType: 'pause-screen' })
}}
/>
</div>
}

View file

@ -35,7 +35,7 @@ const AddElem = ({ elem }) => {
}
}, [])
return <div ref={ref}></div>
return <div ref={ref} />
}
// for (const key of Object.keys(viewer.world.sectionObjects)) {

View file

@ -33,8 +33,11 @@ export default () => {
}}
backdrop={false}
>
<Button style={{ marginTop: 30 }} onClick={() => {
hideCurrentModal()
}}>Back</Button>
<Button
style={{ marginTop: 30 }} onClick={() => {
hideCurrentModal()
}}
>Back
</Button>
</Screen>
}

View file

@ -32,29 +32,32 @@ export default ({ type = 'message', message, subMessage = '', open, icon = '', a
{state => {
const addStyles = { ...basicStyle, ...stateStyles[state] }
return <div className={`app-notification ${isError ? 'error-notification' : ''}`} onClick={action} style={{
position: 'fixed',
top: 0,
right: 0,
width: '180px',
whiteSpace: 'nowrap',
fontSize: '9px',
display: 'flex',
gap: 4,
alignItems: 'center',
padding: '3px 5px',
background: isError ? 'rgba(255, 0, 0, 0.7)' : 'rgba(0, 0, 0, 0.7)',
borderRadius: '0 0 0 5px',
pointerEvents: action ? '' : 'none',
zIndex: 1200, // even above stats
...addStyles
}}>
return <div
className={`app-notification ${isError ? 'error-notification' : ''}`} onClick={action} style={{
position: 'fixed',
top: 0,
right: 0,
width: '180px',
whiteSpace: 'nowrap',
fontSize: '9px',
display: 'flex',
gap: 4,
alignItems: 'center',
padding: '3px 5px',
background: isError ? 'rgba(255, 0, 0, 0.7)' : 'rgba(0, 0, 0, 0.7)',
borderRadius: '0 0 0 5px',
pointerEvents: action ? '' : 'none',
zIndex: 1200, // even above stats
...addStyles
}}
>
<PixelartIcon iconName={icon} styles={{ fontSize: 12 }} />
<div style={{
display: 'flex',
flexDirection: 'column',
gap: 2,
}}>
}}
>
<div>
{message}
</div>
@ -62,7 +65,9 @@ export default ({ type = 'message', message, subMessage = '', open, icon = '', a
fontSize: '7px',
whiteSpace: 'nowrap',
color: 'lightgray',
}}>{subMessage}</div>
}}
>{subMessage}
</div>
</div>
</div>
}}

View file

@ -107,7 +107,7 @@ export default () => {
if (!isModalActive) return null
return <Screen title='Game Menu'>
<Button
icon={'pixelarticons:folder'}
icon="pixelarticons:folder"
style={{ position: 'fixed', top: '5px', left: 'calc(env(safe-area-inset-left) + 5px)' }}
onClick={async () => openWorldActions()}
/>
@ -126,14 +126,14 @@ export default () => {
{(navigator.share as typeof navigator.share | undefined) ? (
<Button
className="button"
icon={'pixelarticons:arrow-up'}
icon="pixelarticons:arrow-up"
style={{ width: '20px' }}
onClick={async () => clickWebShareButton()}
/>
) : null}
<Button
className="button"
icon={'pixelarticons:dice'}
icon="pixelarticons:dice"
style={{ width: '20px' }}
onClick={async () => clickJoinLinkButton(true)}
/>

View file

@ -11,9 +11,11 @@ export default ({
if (width !== undefined) styles = { width, height: width, ...styles }
iconName = iconName.replace('pixelarticons:', '')
return <div style={{
...styles
}} onClick={onClick} className={`${`pixelart-icons-font-${iconName}`} ${className ?? ''}`} />
return <div
style={{
...styles
}} onClick={onClick} className={`${`pixelart-icons-font-${iconName}`} ${className ?? ''}`}
/>
}
export const pixelartIcons = new Proxy({} as PixelartIconsGenerated, {

View file

@ -9,7 +9,7 @@ interface Props {
export default ({ title, children, backdrop = true, style, className }: Props) => {
return (
<>
{backdrop === 'dirt' ? <div className='dirt-bg'></div> : backdrop ? <div className="backdrop"></div> : null}
{backdrop === 'dirt' ? <div className='dirt-bg' /> : backdrop ? <div className="backdrop" /> : null}
<div className={`fullscreen ${className}`} style={{ overflow: 'auto', ...style }}>
<div className="screen-content">
<div className="screen-title">{title}</div>

View file

@ -33,13 +33,19 @@ export default () => {
if (!isModalActive) return
return <Screen title={title} backdrop>
{options.map(option => <Button key={option} onClick={() => {
hideCurrentModal()
resolve(option)
}}>{option}</Button>)}
{showCancel && <Button style={{ marginTop: 30 }} onClick={() => {
hideCurrentModal()
resolve(undefined)
}}>Cancel</Button>}
{options.map(option => <Button
key={option} onClick={() => {
hideCurrentModal()
resolve(option)
}}
>{option}
</Button>)}
{showCancel && <Button
style={{ marginTop: 30 }} onClick={() => {
hideCurrentModal()
resolve(undefined)
}}
>Cancel
</Button>}
</Screen>
}

View file

@ -18,7 +18,8 @@ const meta: Meta<typeof ServersList> = {
accounts={['testting']}
onConfirm={(info) => {
console.log('add server', info)
}} /> :
}}
/> :
<ServersList
worldData={[{
name: 'test',

View file

@ -56,45 +56,50 @@ export default ({ initialProxies, updateProxies: updateProxiesProp, joinServer,
const [serverIp, setServerIp] = React.useState('')
const [save, setSave] = React.useState(true)
return <Singleplayer {...props}
firstRowChildrenOverride={<form style={{ width: '100%', display: 'flex', justifyContent: 'center' }} onSubmit={(e) => {
e.preventDefault()
let ip = serverIp
let version
let msAuth = false
const parts = ip.split(':')
if (parts.at(-1) === 'ms') {
msAuth = true
parts.pop()
}
if (parts.length > 1 && parts.at(-1)!.includes('.')) {
version = parts.at(-1)!
ip = parts.slice(0, -1).join(':')
}
joinServer({
ip,
versionOverride: version,
authenticatedAccountOverride: msAuth ? true : undefined, // todo popup selector
}, {
shouldSave: save,
})
}}
return <Singleplayer
{...props}
firstRowChildrenOverride={<form
style={{ width: '100%', display: 'flex', justifyContent: 'center' }} onSubmit={(e) => {
e.preventDefault()
let ip = serverIp
let version
let msAuth = false
const parts = ip.split(':')
if (parts.at(-1) === 'ms') {
msAuth = true
parts.pop()
}
if (parts.length > 1 && parts.at(-1)!.includes('.')) {
version = parts.at(-1)!
ip = parts.slice(0, -1).join(':')
}
joinServer({
ip,
versionOverride: version,
authenticatedAccountOverride: msAuth ? true : undefined, // todo popup selector
}, {
shouldSave: save,
})
}}
>
<div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
{/* todo history */}
<Input required placeholder='Quick Connect IP (:version)' value={serverIp} onChange={({ target: { value } }) => setServerIp(value)} />
<label style={{ fontSize: 10, display: 'flex', alignItems: 'center', gap: 5, height: '100%', marginTop: '-1px' }}>
<input type='checkbox' checked={save}
<input
type='checkbox' checked={save}
style={{ borderRadius: 0 }}
onChange={({ target: { checked } }) => setSave(checked)}
/> Save</label>
/> Save
</label>
<Button style={{ width: 90 }} type='submit'>Join Server</Button>
</div>
</form>}
searchRowChildrenOverride={
<div style={{
// marginTop: 12,
}}>
}}
>
<div style={{ display: 'flex', gap: 3, alignItems: 'center' }}>
<span style={{ color: 'lightgray', fontSize: 14 }}>Proxy:</span>
<div {...autocomplete.getRootProps()} style={{ position: 'relative', width: 130 }}>
@ -104,11 +109,13 @@ export default ({ initialProxies, updateProxies: updateProxiesProp, joinServer,
status='unknown'
ip=''
/>
{autocomplete.groupedOptions && <ul {...autocomplete.getListboxProps()} style={{
position: 'absolute',
zIndex: 1,
{autocomplete.groupedOptions && <ul
{...autocomplete.getListboxProps()} style={{
position: 'absolute',
zIndex: 1,
// marginTop: 10,
}}>
}}
>
{autocomplete.groupedOptions.map((proxy, index) => {
const { itemRef, ...optionProps } = autocomplete.getOptionProps({ option: proxy, index })
return <ProxyRender {...optionProps as any} ip={proxy} disabled />
@ -144,9 +151,11 @@ const ProxyRender = ({ status, ip, inputRef, value, setValue, ...props }: {
success: 'cellular-signal-3',
}
return <div style={{
position: 'relative',
}} {...props}>
return <div
style={{
position: 'relative',
}} {...props}
>
<Input
inputRef={inputRef}
style={{
@ -165,7 +174,8 @@ const ProxyRender = ({ status, ip, inputRef, value, setValue, ...props }: {
display: 'flex',
alignItems: 'center',
gap: 2
}}>
}}
>
<PixelartIcon iconName={iconPerStatus.unknown} />
<div style={{
fontSize: 10,
@ -174,7 +184,8 @@ const ProxyRender = ({ status, ip, inputRef, value, setValue, ...props }: {
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}>
}}
>
{ip.replace(/^https?:\/\//, '')}
</div>
</div>

View file

@ -5,9 +5,11 @@ import SignEditor from './SignEditor'
const meta: Meta<typeof SignEditor> = {
component: SignEditor,
render (args) {
return <SignEditor {...args} handleClick={(result) => {
console.log('handleClick', result)
}} />
return <SignEditor
{...args} handleClick={(result) => {
console.log('handleClick', result)
}}
/>
}
}

View file

@ -48,11 +48,12 @@ export default ({ handleInput, isWysiwyg, handleClick }: Props) => {
const nextElem = elements[focusedElemIndex + dir]
nextElem?.focus()
}
}}>
}}
>
<div className='signs-editor-inner-container'>
<img className='signs-editor-bg-image' src={imageSource} alt='' />
{isWysiwyg ? (
<p ref={prosemirrorContainer} className='wysiwyg-editor'></p>
<p ref={prosemirrorContainer} className='wysiwyg-editor' />
) : [1, 2, 3, 4].map((value, index) => {
return <input
className='sign-editor'
@ -61,23 +62,25 @@ export default ({ handleInput, isWysiwyg, handleClick }: Props) => {
maxLength={15} // overriden by handleInput
onChange={(e) => {
handleInput(e.currentTarget)
}} />
})
}
<Button onClick={async () => {
if (handleClick) {
if (isWysiwyg) {
const text = markdownToFormattedText(editorView.current!.content)
handleClick({ dataText: text })
} else {
const text = [] as string[]
for (const input of document.getElementsByClassName('sign-editor')) {
text.push((input as HTMLInputElement).value)
}}
/>
})}
<Button
onClick={async () => {
if (handleClick) {
if (isWysiwyg) {
const text = markdownToFormattedText(editorView.current!.content)
handleClick({ dataText: text })
} else {
const text = [] as string[]
for (const input of document.getElementsByClassName('sign-editor')) {
text.push((input as HTMLInputElement).value)
}
handleClick({ plainText: text })
}
handleClick({ plainText: text })
}
}
}} className='sign-editor-button' label={'Done'} />
}} className='sign-editor-button' label="Done"
/>
</div>
</div>
}

View file

@ -4,8 +4,7 @@ import SignInMessage from './SignInMessage'
const meta: Meta<{ open }> = {
component: SignInMessage as any,
render ({ open }) {
return <SignInMessage
/>
return <SignInMessage />
},
}

View file

@ -36,7 +36,8 @@ export default ({
height: 213,
color: 'black',
// borderRadius: 8,
}}>
}}
>
<div style={{
// fontFamily: 'monospace',
fontSize: 18,
@ -46,18 +47,22 @@ export default ({
textAlign: 'center',
marginTop: -5,
userSelect: 'all',
}}>{code}</div>
}}
>{code}
</div>
<div style={{
display: 'flex',
justifyContent: 'center',
fontSize: 12,
}}>
}}
>
Waiting... <PixelartIcon iconName='clock' /> {timeLeft}
</div>
<div style={{
fontSize: 12,
marginTop: 10,
}}>
}}
>
To join a Minecraft server {connectingServer} using your Microsoft account, you need to visit{' '}
<a
href={directLink}
@ -67,7 +72,8 @@ export default ({
fontWeight: 600,
}}
target='_blank'
>Direct Link</a>
>Direct Link
</a>
{' '} or {' '}
<a
href={loginLink}
@ -77,7 +83,8 @@ export default ({
fontWeight: 600,
}}
target='_blank'
>{loginLink}</a>
>{loginLink}
</a>
{' '}
and enter the code above.
</div>
@ -85,7 +92,8 @@ export default ({
fontSize: 12,
marginTop: 5,
color: 'gray'
}}>
}}
>
<PixelartIcon iconName='alert' /> Join only <b>vanilla servers</b>! This client is detectable and may result in a ban by anti-cheat plugins.
</div>}
{setSaveToken && <label style={{
@ -94,7 +102,8 @@ export default ({
alignItems: 'center',
gap: 5,
marginTop: 4,
}}>
}}
>
<input type='checkbox' defaultChecked={defaultSaveToken} onChange={e => setSaveToken(e.target.checked)} />{' '}
Save account token in this browser
</label>}
@ -104,6 +113,7 @@ export default ({
marginTop: -5,
}}
onClick={onCancel}
>Cancel</Button>
>Cancel
</Button>
</Screen>
}

View file

@ -45,12 +45,14 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
return filesize(size)
}, [size])
return <div className={classNames(styles.world_root, isFocused ? styles.world_focused : undefined)} tabIndex={0} onFocus={() => onFocus?.(name)} onKeyDown={(e) => {
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault()
onInteraction?.(e.code === 'Enter' ? 'enter' : 'space')
}
}} onDoubleClick={() => onInteraction?.('enter')}>
return <div
className={classNames(styles.world_root, isFocused ? styles.world_focused : undefined)} tabIndex={0} onFocus={() => onFocus?.(name)} onKeyDown={(e) => {
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault()
onInteraction?.(e.code === 'Enter' ? 'enter' : 'space')
}
}} onDoubleClick={() => onInteraction?.('enter')}
>
<img className={`${styles.world_image} ${iconSrc ? '' : styles.image_missing}`} src={iconSrc ?? missingWorldPreview} alt='world preview' />
<div className={styles.world_info}>
<div className={styles.world_title}>
@ -134,18 +136,22 @@ export default ({
<Input autoFocus value={search} onChange={({ target: { value } }) => setSearch(value)} />
</div>}
<div className={classNames(styles.content, !worldData && styles.content_loading)}>
<Tabs tabs={Object.keys(providers)} disabledTabs={disabledProviders} activeTab={activeProvider ?? ''} labels={providers} onTabChange={(tab) => {
setActiveProvider?.(tab as any)
}} fullSize />
<Tabs
tabs={Object.keys(providers)} disabledTabs={disabledProviders} activeTab={activeProvider ?? ''} labels={providers} onTabChange={(tab) => {
setActiveProvider?.(tab as any)
}} fullSize
/>
<div style={{
marginTop: 3,
}}>
}}
>
{
providerActions && <div style={{
display: 'flex',
alignItems: 'center',
// overflow: 'auto',
}}>
}}
>
<span style={{ fontSize: 9, marginRight: 3 }}>Actions: </span> {Object.entries(providerActions).map(([label, action]) => (
typeof action === 'function' ? <Button key={label} onClick={action} style={{ width: 100 }}>{label}</Button> : <Fragment key={label}>{action}</Fragment>
))}
@ -154,15 +160,19 @@ export default ({
{
worldData
? worldData.filter(data => data.title.toLowerCase().includes(search.toLowerCase())).map(({ name, size, detail, ...rest }) => (
<World {...rest} size={size} name={name} onFocus={setFocusedWorld} isFocused={focusedWorld === name} key={name} onInteraction={(interaction) => {
if (interaction === 'enter') onWorldAction('load', name)
else if (interaction === 'space') firstButton.current?.focus()
}} detail={detail} />
<World
{...rest} size={size} name={name} onFocus={setFocusedWorld} isFocused={focusedWorld === name} key={name} onInteraction={(interaction) => {
if (interaction === 'enter') onWorldAction('load', name)
else if (interaction === 'space') firstButton.current?.focus()
}}
detail={detail}
/>
))
: <div style={{
fontSize: 10,
color: error ? 'red' : 'lightgray',
}}>{error || 'Loading (check #dev console if loading too long)...'}</div>
}}>{error || 'Loading (check #dev console if loading too long)...'}
</div>
}
{
warning && <div style={{
@ -170,7 +180,8 @@ export default ({
color: '#ffa500ba',
marginTop: 5,
textAlign: 'center',
}}>
}}
>
{warning} {warningAction && <a onClick={warningAction}>{warningActionLabel}</a>}
</div>
}
@ -186,7 +197,7 @@ export default ({
<Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('delete', focusedWorld)}>Delete</Button>
{serversLayout ?
<Button style={{ width: 100 }} onClick={() => onGeneralAction('create')}>Add</Button> :
<Button style={{ width: 100 }} /* disabled={!focusedWorld} */ onClick={() => onWorldAction('edit', focusedWorld)} disabled>Edit</Button>}
<Button style={{ width: 100 }} onClick={() => onWorldAction('edit', focusedWorld)} disabled>Edit</Button>}
<Button style={{ width: 100 }} onClick={() => onGeneralAction('cancel')}>Cancel</Button>
</div>
</div>

View file

@ -73,8 +73,8 @@ const Slider: React.FC<Props> = ({
fireValueUpdate(true)
}}
/>
<div className={styles.disabled} title={disabledReason}></div>
<div className={styles['slider-thumb']} style={{ left: `calc((100% * ${ratio}) - (8px * ${ratio}))` }}></div>
<div className={styles.disabled} title={disabledReason} />
<div className={styles['slider-thumb']} style={{ left: `calc((100% * ${ratio}) - (8px * ${ratio}))` }} />
<label className={styles.label}>
{label}: {valueDisplay ?? value} {unit}
</label>

View file

@ -17,13 +17,15 @@ const SoundRow = ({ sound, children }) => {
<span style={{ fontSize: 12, marginRight: 2, ...isMuted ? { color: '#af1c1c' } : {} }}>{sound}</span>
{children}
</div>
<Button icon={isMuted ? 'pixelarticons:music' : 'pixelarticons:close'} onClick={() => {
if (isMuted) {
options.mutedSounds.splice(options.mutedSounds.indexOf(sound), 1)
} else {
options.mutedSounds.push(sound)
}
}}></Button>
<Button
icon={isMuted ? 'pixelarticons:music' : 'pixelarticons:close'} onClick={() => {
if (isMuted) {
options.mutedSounds.splice(options.mutedSounds.indexOf(sound), 1)
} else {
options.mutedSounds.push(sound)
}
}}
/>
</div>
}

View file

@ -16,24 +16,30 @@ export default ({ tabs, activeTab, labels, onTabChange, fullSize, style, disable
width: fullSize ? '100%' : undefined,
display: fullSize ? 'flex' : undefined,
...style,
}}>
}}
>
{tabs.map(tab => {
const active = tab === activeTab
return <div key={tab} style={{
position: 'relative',
width: fullSize ? '100%' : 150,
}}>
<Button disabled={active || disabledTabs?.includes(tab)} style={{
width: '100%',
height: '100%',
// background: active ? 'rgb(77, 77, 77)' : 'rgb(114, 114, 114)',
color: 'white',
cursor: 'pointer',
fontSize: 9,
padding: '2px 0px',
}} onClick={() => {
onTabChange(tab)
}}>{labels?.[tab] ?? tab}</Button>
return <div
key={tab} style={{
position: 'relative',
width: fullSize ? '100%' : 150,
}}
>
<Button
disabled={active || disabledTabs?.includes(tab)} style={{
width: '100%',
height: '100%',
// background: active ? 'rgb(77, 77, 77)' : 'rgb(114, 114, 114)',
color: 'white',
cursor: 'pointer',
fontSize: 9,
padding: '2px 0px',
}} onClick={() => {
onTabChange(tab)
}}
>{labels?.[tab] ?? tab}
</Button>
{active && <div style={{
position: 'absolute',
bottom: 0,
@ -43,7 +49,8 @@ export default ({ tabs, activeTab, labels, onTabChange, fullSize, style, disable
background: 'white',
width: '50%',
margin: 'auto',
}} />}
}}
/>}
</div>
})}
</div>

View file

@ -201,7 +201,8 @@ export default ({ touchActive, setupActive, buttonsPositions, closeButtonsSetup
left: `${pointer.x / window.innerWidth * 100}%`,
top: `${pointer.y / window.innerHeight * 100}%`
} : {}
}}>
}}
>
<div
className='movement_joystick_inner'
style={{
@ -232,13 +233,18 @@ export default ({ touchActive, setupActive, buttonsPositions, closeButtonsSetup
display: 'flex',
justifyContent: 'center',
gap: 3
}}>
}}
>
<Button onClick={() => {
closeButtonsSetup()
}}>Cancel</Button>
}}
>Cancel
</Button>
<Button onClick={() => {
closeButtonsSetup(newButtonPositions)
}}>Apply</Button>
}}
>Apply
</Button>
</div>}
</div>
}

View file

@ -8,7 +8,7 @@ export default ({ progress, level, gamemode }: { progress: number; level: number
className={styles['xp-bar-bg']}
style={{ display: gamemode === 'creative' || gamemode === 'spectator' ? 'none' : 'block' }}
>
<div className={styles['xp-bar']} style={{ width: `${182 * progress}px` }}></div>
<div className={styles['xp-bar']} style={{ width: `${182 * progress}px` }} />
<span className={styles['xp-label']} style={{ display: level > 0 ? 'block' : 'none' }}>{level}</span>
</div>
</SharedHudVars>

View file

@ -160,7 +160,7 @@ const App = () => {
<HeldMapUi />
</InGameComponent>
</div>
<div></div>
<div />
</RobustPortal>
<EnterFullscreenButton />
<InGameUi />
@ -186,18 +186,23 @@ const App = () => {
<div className='overlay-top-scaled'>
<GamepadUiCursor />
</div>
<div></div>
<div />
</RobustPortal>
</ButtonAppProvider>
</div>
}
const PerComponentErrorBoundary = ({ children }) => {
return children.map((child, i) => <ErrorBoundary key={i} renderError={(error) => {
const componentNameClean = (child.type.name || child.type.displayName || 'Unknown').replaceAll(/__|_COMPONENT/g, '')
showNotification(`UI component ${componentNameClean} crashed!`, 'Please report this. Use console for more.', true, undefined)
return null
}}>{child}</ErrorBoundary>)
return children.map((child, i) => <ErrorBoundary
key={i}
renderError={(error) => {
const componentNameClean = (child.type.name || child.type.displayName || 'Unknown').replaceAll(/__|_COMPONENT/g, '')
showNotification(`UI component ${componentNameClean} crashed!`, 'Please report this. Use console for more.', true, undefined)
return null
}}
>
{child}
</ErrorBoundary>)
}
renderToDom(<App />, {