add storybook & react button component, refactor styles a bit

fix death screen was on top of other screens
This commit is contained in:
Vitaly Turovsky 2023-10-10 17:06:23 +03:00
commit d77484c966
16 changed files with 4019 additions and 430 deletions

View file

@ -7,7 +7,7 @@
"rules": {
"space-infix-ops": "error",
"no-multi-spaces": "error",
"space-after-function-name": "error",
"space-before-function-paren": "error",
"space-in-parens": [
"error",
"never"

14
.storybook/main.ts Normal file
View file

@ -0,0 +1,14 @@
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: "tag",
},
};
export default config;

27
.storybook/preview.tsx Normal file
View file

@ -0,0 +1,27 @@
import React from 'react'
import type { Preview } from "@storybook/react";
import '../src/styles.css'
import './storybook.css'
const preview: Preview = {
decorators: [
(Story) => (
<div id='ui-root'>
<Story />
</div>
),
],
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

18
.storybook/storybook.css Normal file
View file

@ -0,0 +1,18 @@
#storybook-root::before {
content: "";
position: fixed;
inset: 0;
background-image: url("../assets/storybook-bg.jpg");
background-size: cover;
background-position: center;
}
@font-face {
font-family: minecraft;
src: url(../assets/minecraftia.woff);
}
@font-face {
font-family: mojangles;
src: url(../assets/mojangles.ttf);
}

View file

@ -12,7 +12,9 @@
"prod-start": "node server.js",
"postinstall": "node scripts/gen-texturepack-files.mjs",
"test-mc-server": "tsx cypress/minecraft-server.mjs",
"lint": "eslint \"{src,cypress}/**/*.{ts,js,jsx,tsx}\""
"lint": "eslint \"{src,cypress}/**/*.{ts,js,jsx,tsx}\"",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"keywords": [
"prismarine",
@ -57,6 +59,13 @@
"workbox-build": "^7.0.0"
},
"devDependencies": {
"@storybook/addon-essentials": "^7.4.6",
"@storybook/addon-links": "^7.4.6",
"@storybook/blocks": "^7.4.6",
"@storybook/react": "^7.4.6",
"@storybook/react-vite": "^7.4.6",
"@storybook/web-components": "^7.4.6",
"@storybook/web-components-vite": "^7.4.6",
"@types/lodash-es": "^4.17.9",
"@types/stats.js": "^0.17.1",
"@types/three": "0.128.0",
@ -86,6 +95,7 @@
"prismarine-viewer": "link:prismarine-viewer",
"process": "github:PrismarineJS/node-process",
"rimraf": "^5.0.1",
"storybook": "^7.4.6",
"stream-browserify": "^3.0.0",
"three": "0.128.0",
"timers-browserify": "^2.0.12",

3852
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

18
src/globals.d.ts vendored
View file

@ -9,7 +9,7 @@ declare const worldView: import('prismarine-viewer/viewer/lib/worldDataEmitter')
declare const localServer: any
declare interface Document {
getElementById(id): any
getElementById (id): any
exitPointerLock?(): void
}
@ -20,8 +20,8 @@ declare namespace JSX {
}
declare interface DocumentFragment {
getElementById(id): HTMLElement & Record<string, any>
querySelector(id): HTMLElement & Record<string, any>
getElementById (id): HTMLElement & Record<string, any>
querySelector (id): HTMLElement & Record<string, any>
}
declare interface Window extends Record<string, any> {
@ -32,13 +32,17 @@ 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]]>
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
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

View file

@ -1,7 +1,7 @@
//@ts-check
const { LitElement, html, css, unsafeCSS } = require('lit')
const widgetsGui = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png')
const { options } = require('../../optionsStorage')
import { LitElement, html, css, unsafeCSS } from 'lit'
import widgetsGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png'
import { options } from '../../optionsStorage'
let audioContext
/** @type {Record<string, any>} */

21
src/react/Button.tsx Normal file
View file

@ -0,0 +1,21 @@
import { playSound } from '../menus/components/button'
import buttonCss from './button.module.css'
// testing in storybook from deathscreen
interface Props extends React.ComponentProps<'button'> {
label: string
icon?: string
}
export default ({ label, icon, ...args }: Props) => {
const onClick = (e) => {
void playSound('button_click.mp3')
args.onClick(e)
}
return <button className={buttonCss.button} onClick={onClick} {...args}>
{icon && <iconify-icon class={buttonCss.icon} icon={icon}></iconify-icon>}
{label}
</button>
}

View file

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react'
import DeathScreen from './DeathScreen'
const meta: Meta<typeof DeathScreen> = {
component: DeathScreen,
}
export default meta
type Story = StoryObj<typeof DeathScreen>;
export const Primary: Story = {
args: {
dieReasonMessage: [
{
text: 'test',
}
],
respawnCallback () {
},
disconnectCallback () {
},
},
}

View file

@ -1,59 +1,15 @@
import { useEffect } from 'react'
import './deathScreen.css'
import { proxy, useSnapshot } from 'valtio'
import { disconnect } from '../utils'
import { MessageFormatPart, formatMessage } from '../botUtils'
import { options } from '../optionsStorage'
import { hideModal, showModal } from '../globalState'
import type { MessageFormatPart } from '../botUtils'
import MessageFormatted from './MessageFormatted'
import Button from './Button'
const dieReasonProxy = proxy({ value: null as MessageFormatPart[] | null })
export default () => {
const { value: dieReasonMessage } = useSnapshot(dieReasonProxy)
useEffect(() => {
type DeathEvent = {
playerId: number
entityId: number
message: string
}
bot._client.on('death_combat_event', (data: DeathEvent) => {
try {
if (data.playerId !== bot.entity.id) return
const messageParsed = JSON.parse(data.message)
const parts = formatMessage(messageParsed)
dieReasonProxy.value = parts
} catch (err) {
console.error(err)
}
})
bot.on('death', () => {
if (dieReasonProxy.value) return
dieReasonProxy.value = []
})
bot.on('respawn', () => {
// todo don't close too early, instead wait for health event and make button disabled?
dieReasonProxy.value = null
})
if (bot.health === 0) {
dieReasonProxy.value = []
}
}, [])
useEffect(() => {
if (dieReasonProxy.value) {
showModal({ reactType: 'death-screen' })
} else {
hideModal({ reactType: 'death-screen' })
}
}, [dieReasonMessage])
if (!dieReasonMessage || options.autoRespawn) return null
type Props = {
dieReasonMessage: readonly MessageFormatPart[]
respawnCallback: () => void
disconnectCallback: () => void
}
export default ({ dieReasonMessage, respawnCallback, disconnectCallback }: Props) => {
return (
<div className='deathScreen-container'>
<div className="deathScreen">
@ -62,12 +18,11 @@ export default () => {
<MessageFormatted parts={dieReasonMessage} />
</h5>
<div className='deathScreen-buttons-grouped'>
<pmui-button pmui-label="Respawn" onClick={() => {
console.log('respawn')
bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 0 } : { actionId: 0 })
<Button label="Respawn" onClick={() => {
respawnCallback()
}} />
<pmui-button pmui-label="Disconnnect" onClick={() => {
disconnect()
<Button label="Disconnnect" onClick={() => {
disconnectCallback()
}} />
</div>
</div>

View file

@ -0,0 +1,66 @@
import { useEffect } from 'react'
import { proxy, useSnapshot } from 'valtio'
import { disconnect } from '../utils'
import { MessageFormatPart, formatMessage } from '../botUtils'
import { showModal, hideModal, activeModalStack } from '../globalState'
import { options } from '../optionsStorage'
import DeathScreen from './DeathScreen'
const dieReasonProxy = proxy({ value: null as MessageFormatPart[] | null })
export default () => {
const { value: dieReasonMessage } = useSnapshot(dieReasonProxy)
const activeModals = useSnapshot(activeModalStack)
useEffect(() => {
type DeathEvent = {
playerId: number
entityId: number
message: string
}
bot._client.on('death_combat_event', (data: DeathEvent) => {
try {
if (data.playerId !== bot.entity.id) return
const messageParsed = JSON.parse(data.message)
const parts = formatMessage(messageParsed)
dieReasonProxy.value = parts
} catch (err) {
console.error(err)
}
})
bot.on('death', () => {
if (dieReasonProxy.value) return
dieReasonProxy.value = []
})
bot.on('respawn', () => {
// todo don't close too early, instead wait for health event and make button disabled?
dieReasonProxy.value = null
})
if (bot.health === 0) {
dieReasonProxy.value = []
}
}, [])
useEffect(() => {
if (dieReasonProxy.value) {
showModal({ reactType: 'death-screen' })
} else {
hideModal({ reactType: 'death-screen' })
}
}, [dieReasonMessage])
if (!dieReasonMessage || options.autoRespawn || activeModals.length) return null
return <DeathScreen
dieReasonMessage={dieReasonMessage}
respawnCallback={() => {
bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 0 } : { actionId: 0 })
}}
disconnectCallback={() => {
disconnect()
}}
/>
}

View file

@ -38,13 +38,13 @@ export function getColorShadow (hex, dim = 0.25) {
return `#${f(r)}${f(g)}${f(b)}`
}
function parseInlineStyle(style: string): Record<string, any> {
function parseInlineStyle (style: string): Record<string, any> {
const template = document.createElement('template')
template.setAttribute('style', style)
return Object.fromEntries(Object.entries(template.style)
.filter(([ key ]) => !/^\d+$/.test(key))
.filter(([ , value ]) => Boolean(value))
.map(([ key, value ]) => [key, value]))
.filter(([key]) => !/^\d+$/.test(key))
.filter(([, value]) => Boolean(value))
.map(([key, value]) => [key, value]))
}
export const messageFormatStylesMap = {

View file

@ -0,0 +1,65 @@
.button {
--txrV: 66px;
position: relative;
width: 200px;
height: 20px;
font-family: minecraft, mojangles, monospace;
font-size: 10px;
color: white;
text-shadow: 1px 1px #222;
border: none;
z-index: 1;
outline: none;
display: inline-flex;
justify-content: center;
align-items: center;
}
.button:hover,
.button:focus-visible {
--txrV: 86px;
}
.button:disabled {
--txrV: 46px;
color: #A0A0A0;
text-shadow: 1px 1px #111;
}
.button::before,
.button::after {
content: '';
display: block;
position: absolute;
top: 0;
width: 50%;
height: 20px;
background: var(--widgets-gui-atlas);
background-size: 256px;
background-position-y: calc(var(--txrV) * -1);
z-index: -1;
}
.button::before {
left: 50%;
background-position-x: calc(-200px + 100%);
}
.button::after {
left: 0;
width: calc(50% + 1px);
}
.icon {
position: absolute;
top: 3px;
left: 3px;
font-size: 14px;
}
/* @media (pointer: coarse) {
.button {
height: 30px;
}
} */

View file

@ -6,10 +6,10 @@ import { css } from '@emotion/css'
import { useSnapshot } from 'valtio'
import { QRCodeSVG } from 'qrcode.react'
import { createPortal } from 'react-dom'
import DeathScreen from './react/DeathScreen'
import { contro } from './controls'
import { activeModalStack, isGameActive, miscUiState } from './globalState'
import { options, watchValue } from './optionsStorage'
import DeathScreenProvider from './react/DeathScreenProvider'
// todo
useInterfaceState.setState({
@ -17,7 +17,7 @@ useInterfaceState.setState({
uiCustomization: {
touchButtonSize: 40,
},
updateCoord([coord, state]) {
updateCoord ([coord, state]) {
const coordToAction = [
['z', -1, 'KeyW'],
['z', 1, 'KeyS'],
@ -77,7 +77,7 @@ const TouchControls = () => {
)
}
function useIsBotAvailable() {
function useIsBotAvailable () {
const stack = useSnapshot(activeModalStack)
return isGameActive(false)
@ -119,7 +119,7 @@ const App = () => {
return <div>
<Portal to={document.querySelector('#ui-root')}>
{/* apply scaling */}
<DeathScreen />
<DeathScreenProvider />
</Portal>
<DisplayQr />
<TouchControls />

View file

@ -12,11 +12,6 @@
box-sizing: border-box;
}
#react-root {
z-index: 9;
position: fixed;
}
a {
color: white;
}
@ -25,6 +20,30 @@ html {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
height: 100vh;
overflow: hidden;
--widgets-gui-atlas: url('minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png')
}
body {
overflow: hidden;
position: relative;
margin:0;
padding:0;
height: 100vh;
/* font-family: sans-serif; */
background: #333;
/* background: linear-gradient(#141e30, #243b55); */
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
font-family: minecraft, mojangles, monospace;
}
#react-root {
z-index: 9;
position: fixed;
}
.dirt-bg {
@ -51,34 +70,6 @@ html {
src: url(mojangles.ttf);
}
body {
overflow: hidden;
position: relative;
margin:0;
padding:0;
height: 100vh;
font-family: sans-serif;
background: #333;
/* background: linear-gradient(#141e30, #243b55); */
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#viewer-canvas {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
font-size: 0;
margin: 0;
padding: 0;
}
#ui-root {
position: fixed;
top: 0;
@ -94,7 +85,17 @@ body {
image-rendering: -o-crisp-edges;
image-rendering: pixelated;
-ms-interpolation-mode: nearest-neighbor;
font-family: minecraft, mojangles, monospace;
}
#viewer-canvas {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
font-size: 0;
margin: 0;
padding: 0;
}
@media only screen and (max-width: 971px) {