feat: Key binds screen (#108)

---------

Co-authored-by: gguio <nikvish150@gmail.com>
Co-authored-by: Vitaly <vital2580@icloud.com>
This commit is contained in:
gguio 2024-05-18 23:51:35 +04:00 committed by GitHub
commit 6bf1085fbe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 857 additions and 71 deletions

View file

@ -79,6 +79,7 @@ const buildOptions = {
loader: {
// todo use external or resolve issues with duplicating
'.png': 'dataurl',
'.svg': 'dataurl',
'.map': 'empty',
'.vert': 'text',
'.frag': 'text',

View file

@ -118,7 +118,7 @@
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
"constants-browserify": "^1.0.0",
"contro-max": "^0.1.6",
"contro-max": "^0.1.7",
"crypto-browserify": "^3.12.0",
"cypress": "^10.11.0",
"cypress-esbuild-preprocessor": "^1.0.2",

10
pnpm-lock.yaml generated
View file

@ -268,8 +268,8 @@ importers:
specifier: ^1.0.0
version: 1.0.0
contro-max:
specifier: ^0.1.6
version: 0.1.6(typescript@5.5.0-beta)
specifier: ^0.1.7
version: 0.1.7(typescript@5.5.0-beta)
crypto-browserify:
specifier: ^3.12.0
version: 3.12.0
@ -3811,8 +3811,8 @@ packages:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
contro-max@0.1.6:
resolution: {integrity: sha512-QsoOcAlbtNgkCGBvwKsh+GUVZ2c5zfMgYQCu+v4MplX5VolkWhMwAcEOBRxt8oENbnRXOKUGQr816Ey1G4/jpg==}
contro-max@0.1.7:
resolution: {integrity: sha512-HIYF1Dl50tUyTKaDsX+mPMDv2OjleNMVedYuBTX0n1wKNm9WxjWu2w74ATjz/8fHVL9GgmziIxAlFStd2je6kg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
convert-source-map@1.9.0:
@ -12851,7 +12851,7 @@ snapshots:
content-type@1.0.5: {}
contro-max@0.1.6(typescript@5.5.0-beta):
contro-max@0.1.7(typescript@5.5.0-beta):
dependencies:
events: 3.3.0
lodash-es: 4.17.21

View file

@ -6,21 +6,24 @@ import { proxy, subscribe } from 'valtio'
import { ControMax } from 'contro-max/build/controMax'
import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types'
import { stringStartsWith } from 'contro-max/build/stringUtils'
import { UserOverridesConfig } from 'contro-max/build/types/store'
import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState } from './globalState'
import { goFullscreen, pointerLock, reloadChunks } from './utils'
import { options } from './optionsStorage'
import { openPlayerInventory } from './inventoryWindows'
import { chatInputValueGlobal } from './react/Chat'
import { fsState } from './loadSave'
import { customCommandsConfig } from './customCommands'
import { CustomCommand } from './react/KeybindingsCustom'
import { showOptionsModal } from './react/SelectOption'
import widgets from './react/widgets'
import { getItemFromBlock } from './botUtils'
import { gamepadUiCursorState, moveGamepadCursorByPx } from './react/GamepadUiCursor'
// todo move this to shared file with component
export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}'))
export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) as UserOverridesConfig
subscribe(customKeymaps, () => {
localStorage.keymap = JSON.parse(customKeymaps)
localStorage.keymap = JSON.stringify(customKeymaps)
})
const controlOptions = {
@ -53,7 +56,8 @@ export const contro = new ControMax({
},
advanced: {
lockUrl: ['KeyY'],
}
},
custom: {} as Record<string, SchemaCommandInput & { type: string, input: any[] }>,
// waila: {
// showLookingBlockRecipe: ['Numpad3'],
// showLookingBlockUsages: ['Numpad4']
@ -81,6 +85,8 @@ export const contro = new ControMax({
window.controMax = contro
export type Command = CommandEventArgument<typeof contro['_commandsRaw']>['command']
// updateCustomBinds()
export const setDoPreventDefault = (state: boolean) => {
controlOptions.preventDefault = state
}
@ -285,6 +291,20 @@ function cycleHotbarSlot (dir: 1 | -1) {
bot.setQuickBarSlot(newHotbarSlot)
}
// custom commands hamdler
const customCommandsHandler = (buttonData: { code?: string, button?: string, state: boolean }) => {
if (!buttonData.state || !isGameActive(true)) return
const codeOrButton = buttonData.code ?? buttonData.button
const inputType = buttonData.code ? 'keys' : 'gamepad'
for (const value of Object.values(contro.userConfig!.custom)) {
if (value[inputType]?.includes(codeOrButton!)) {
customCommandsConfig[(value as CustomCommand).type].handler((value as CustomCommand).inputs)
}
}
}
contro.on('pressedKeyOrButtonChanged', customCommandsHandler)
contro.on('trigger', ({ command }) => {
const willContinue = !isGameActive(true)
alwaysPressedHandledCommand(command)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

91
src/customCommands.ts Normal file
View file

@ -0,0 +1,91 @@
import { guiOptionsScheme, tryFindOptionConfig } from './optionsGuiScheme'
import { options } from './optionsStorage'
export const customCommandsConfig = {
chat: {
input: [
{
type: 'text',
placeholder: 'Command to send e.g. gamemode creative'
}
],
handler ([command]) {
bot.chat(`/${command.replace(/^\//, '')}`)
}
},
setOrToggleSetting: {
input: [
{
type: 'select',
// maybe title case?
options: Object.keys(options)
},
{
type: 'select',
options: ['toggle', 'set']
},
([setting = '', action = ''] = []) => {
const value = options[setting]
if (!action || value === undefined || action === 'toggle') return null
if (action === 'set') {
const getBase = () => {
const config = tryFindOptionConfig(setting as any)
if (config && 'values' in config) {
return {
type: 'select',
options: config.values
}
}
if (config?.type === 'toggle' || typeof value === 'boolean') {
return {
type: 'select',
options: ['true', 'false']
}
}
if (config?.type === 'slider' || value.type === 'number') {
return {
type: 'number',
}
}
return {
type: 'text'
}
}
return {
...getBase(),
placeholder: value
}
}
}
],
handler ([setting, action, value]) {
if (action === 'toggle') {
const value = options[setting]
const config = tryFindOptionConfig(setting)
if (config && 'values' in config && config.values) {
const { values } = config
const currentIndex = values.indexOf(value)
const nextIndex = (currentIndex + 1) % values.length
options[setting] = values[nextIndex]
} else {
options[setting] = typeof value === 'boolean' ? !value : typeof value === 'number' ? value + 1 : value
}
} else {
options[setting] = value
}
}
},
jsScripts: {
input: [
{
type: 'text',
placeholder: 'JavaScript code to run in main thread (sensitive!)'
}
],
handler ([code]) {
// eslint-disable-next-line no-new-func -- this is a feature, not a bug
new Function(code)()
}
},
// openCommandsScreen: {}
}

View file

@ -184,7 +184,16 @@ export const guiOptionsScheme: {
custom () {
return <Category>Keyboard & Mouse</Category>
},
// keybindings
},
{
custom () {
return <Button
inScreen
onClick={() => {
showModal({ reactType: 'keybindings' })
}}
>Keybindings</Button>
},
mouseSensX: {},
mouseSensY: {
min: -1,
@ -282,3 +291,15 @@ const Category = ({ children }) => <div style={{
textAlign: 'center',
gridColumn: 'span 2'
}}>{children}</div>
export const tryFindOptionConfig = (option: keyof AppOptions) => {
for (const group of Object.values(guiOptionsScheme)) {
for (const optionConfig of group) {
if (option in optionConfig) {
return optionConfig[option]
}
}
}
return null
}

View file

@ -0,0 +1,171 @@
import { useEffect, useState, useContext } from 'react'
import { customCommandsConfig } from '../customCommands'
import { ButtonWithMatchesAlert, Context } from './KeybindingsScreen'
import Button from './Button'
import styles from './KeybindingsScreen.module.css'
import Input from './Input'
export type CustomCommand = {
keys: undefined | string[]
gamepad: undefined | string[]
type: string
inputs: any[]
}
export type CustomCommandsMap = Record<string, CustomCommand>
export default (
{
customCommands,
updateCurrBind,
resetBinding,
}: {
customCommands: CustomCommandsMap,
updateCurrBind: (group: string, action: string) => void,
resetBinding: (group: string, action: string, inputType: string) => void,
}
) => {
const { userConfig, setUserConfig } = useContext(Context)
const [customConfig, setCustomConfig] = useState<any>({ ...customCommands })
useEffect(() => {
setUserConfig({ ...userConfig, custom: { ...customConfig } })
}, [customConfig])
const addNewCommand = (type: string) => {
// max key + 1
const newKey = String(Math.max(...Object.keys(customConfig).map(Number).filter(key => !isNaN(key)), 0) + 1)
setCustomConfig(prev => {
const newCustomConf = { ...prev }
newCustomConf[newKey] = {
keys: undefined as string[] | undefined,
gamepad: undefined as string[] | undefined,
type,
inputs: [] as any[]
}
return newCustomConf
})
}
return <>
<div className={styles.group}>
{Object.entries(customCommandsConfig).map(([group, { input }]) => (
<div key={`group-container-${group}`} className={styles.group}>
<div key={`category-${group}`} className={styles['group-category']}>{group}</div>
{Object.entries(customConfig).filter(([key, data]) => data.type === group).map((commandData, indexOption) => {
return <CustomCommandContainer
key={indexOption}
indexOption={indexOption}
commandData={commandData}
updateCurrBind={updateCurrBind}
groupData={[group, { input }]}
setCustomConfig={setCustomConfig}
resetBinding={resetBinding}
/>
})}
<Button
onClick={() => addNewCommand(group)}
icon={'pixelarticons:add-box'}
style={{
alignSelf: 'center'
}}
/>
</div>
))}
</div>
</>
}
const CustomCommandContainer = (
{
indexOption,
commandData,
updateCurrBind,
setCustomConfig,
resetBinding,
groupData
}
) => {
const { userConfig } = useContext(Context)
const [commandKey, { keys, gamepad, inputs }] = commandData
const [group, { input }] = groupData
const setInputValue = (optionKey, indexInput, value) => {
setCustomConfig(prev => {
const newConfig = { ...prev }
newConfig[optionKey].inputs = [...prev[optionKey].inputs]
newConfig[optionKey].inputs[indexInput] = value
return newConfig
})
}
return <div style={{ padding: '10px' }}>
{input.map((obj, indexInput) => {
const config = typeof obj === 'function' ? obj(inputs) : obj
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>
: <Input key={indexInput} rootStyles={{ width: '99%' }} placeholder={config.placeholder} value={inputs[indexInput] ?? ''} onChange={(e) => setInputValue(commandKey, indexInput, e.target.value)} />
})}
<div className={styles.actionBinds}>
{
userConfig?.['custom']?.[commandKey]?.keys ? <Button
onClick={() => {
updateCurrBind(group, commandKey)
resetBinding('custom', commandKey, 'keyboard')
}}
className={styles['undo-keyboard']}
icon={'pixelarticons:undo'}
/>
: null}
{[0, 1].map((key, index) => <ButtonWithMatchesAlert
key={`custom-keyboard-${group}-${commandKey}-${index}`}
group={'custom'}
action={commandKey}
index={index}
inputType={'keyboard'}
keys={keys}
gamepad={gamepad}
/>
)}
<div style={{ marginRight: 'auto' }} ></div>
{
userConfig?.['custom']?.[commandKey]?.gamepad ? <Button
onClick={() => {
updateCurrBind(group, commandKey)
resetBinding('custom', commandKey, 'gamepad')
}}
className={styles['undo-keyboard']}
icon={'pixelarticons:undo'}
/>
: null}
<ButtonWithMatchesAlert
group={'custom'}
action={commandKey}
index={0}
inputType={'gamepad'}
keys={keys}
gamepad={gamepad}
/>
<Button
onClick={() => {
setCustomConfig(prev => {
const { [commandKey]: commandToRemove, ...newConfig } = prev
return newConfig
})
}}
style={{ color: 'red' }}
icon={'pixelarticons:delete'}
/>
</div>
</div>
}

View file

@ -0,0 +1,88 @@
.container {
overflow-x: auto;
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}
@media (max-width: 590px) {
.container {
width: 90vw;
}
}
.group {
display: flex;
flex-direction: column;
gap: 5px;
}
.group-category {
font-size: 1rem;
text-align: center;
margin-top: 15px;
margin-bottom: 7px;
text-transform: capitalize;
}
.actionBinds {
position: relative;
display: flex;
}
.warning-container {
flex-basis: 25%;
display: flex;
flex-direction: column;
height: inherit;
}
.actionName {
flex-basis: 30%;
margin-right: 5px;
flex-wrap: wrap;
align-self: center;
font-size: 10px;
}
.undo-keyboard,
.undo-gamepad {
aspect-ratio: 1;
}
.button {
width: 100%;
font-size: 7px;
}
.margin-left {
margin-left: 25px;
}
.matched-bind {
border: 1px solid red;
border-bottom: 1px solid red;
}
.matched-bind-warning {
display: flex;
color: yellow;
font-family: inherit;
font-size: 5px;
width: fit-content;
}
.matched-bind-warning a {
color: inherit;
}
/* ~~~~ custom bindings styles */
.chat-command {
font: inherit;
color: white;
display: block;
height: 100%;
flex-grow: 1;
}

View file

@ -1,12 +1,382 @@
import { useState, useEffect, useRef, createContext, useContext } from 'react'
import { UserOverridesConfig } from 'contro-max/build/types/store'
import { contro as controEx } from '../controls'
import { hideModal } from '../globalState'
import triangle from './ps_icons/playstation_triangle_console_controller_gamepad_icon.svg'
import square from './ps_icons/playstation_square_console_controller_gamepad_icon.svg'
import circle from './ps_icons/circle_playstation_console_controller_gamepad_icon.svg'
import cross from './ps_icons/cross_playstation_console_controller_gamepad_icon.svg'
import PixelartIcon from './PixelartIcon'
import KeybindingsCustom, { CustomCommandsMap } from './KeybindingsCustom'
import { BindingActionsContext } from './KeybindingsScreenProvider'
import Button from './Button'
import Screen from './Screen'
import styles from './KeybindingsScreen.module.css'
export default ({
onBack,
onReset,
onSet,
keybindings
}) => {
return <Screen title="Keybindings" backdrop>
<p>Here you can change the keybindings for the game.</p>
</Screen>
type HandleClick = (group: string, action: string, index: number, type: string | null) => void
type setBinding = (data: any, group: string, command: string, buttonIndex: number) => void
export const Context = createContext(
{
isPS: false as boolean | undefined,
userConfig: controEx?.userConfig ?? {} as UserOverridesConfig | undefined,
setUserConfig (config) { },
handleClick: (() => { }) as HandleClick,
parseBindingName (binding) { return '' as string },
bindsMap: { keyboard: {} as any, gamepad: {} as any }
}
)
export default (
{
contro,
isPS,
}: {
contro: typeof controEx,
isPS?: boolean
}
) => {
const containerRef = useRef<HTMLDivElement | null>(null)
const bindsMap = useRef({ keyboard: {} as any, gamepad: {} as any })
const { commands } = contro.inputSchema
const [userConfig, setUserConfig] = useState(contro.userConfig ?? {})
const [awaitingInputType, setAwaitingInputType] = useState(null as null | 'keyboard' | 'gamepad')
const [groupName, setGroupName] = useState('')
const [actionName, setActionName] = useState('')
const [buttonNum, setButtonNum] = useState(0)
const { updateBinds } = useContext(BindingActionsContext)
const [customCommands, setCustomCommands] = useState<CustomCommandsMap>(userConfig.custom as CustomCommandsMap ?? {})
const updateCurrBind = (group: string, action: string) => {
setGroupName(prev => group)
setActionName(prev => action)
}
const handleClick: HandleClick = (group, action, index, type) => {
//@ts-expect-error
setAwaitingInputType(type)
updateCurrBind(group, action)
setButtonNum(prev => index)
}
const setBinding: setBinding = (data, group, command, buttonIndex) => {
setUserConfig(prev => {
const newConfig = { ...prev }
newConfig[group] ??= {}
newConfig[group][command] ??= {}
// keys and buttons should always exist in commands
const type = 'code' in data ? 'keys' : 'button' in data ? 'gamepad' : null
if (type) {
newConfig[group][command][type] ??= group === 'custom' ? [] : [...contro.inputSchema.commands[group][command][type]]
newConfig[group][command][type]![buttonIndex] = data.code ?? data.button
}
return newConfig
})
}
const resetBinding = (group: string, command: string, inputType: string) => {
if (!userConfig?.[group]?.[command]) return
setUserConfig(prev => {
const newConfig = { ...prev }
const prop = inputType === 'keyboard' ? 'keys' : 'gamepad'
newConfig[group][command][prop] = undefined
return newConfig
})
}
useEffect(() => {
updateBinds(userConfig)
setCustomCommands({ ...userConfig.custom as CustomCommandsMap })
updateBindMap()
}, [userConfig])
// const updateKeyboardBinding = (e: import('react').KeyboardEvent<HTMLDivElement>) => {
// if (!e.code || e.key === 'Escape' || !awaitingInputType) return
// setBinding({ code: e.code, state: true }, groupName, actionName, buttonNum)
// }
const updateBinding = (data: any) => {
if ((!data.state && awaitingInputType) || !awaitingInputType) {
setAwaitingInputType(null)
return
}
if ('code' in data) {
if (data.code === 'Escape' || ['Mouse0', 'Mouse1', 'Mouse2'].includes(data.code)) {
setAwaitingInputType(null)
return
}
setBinding({ code: data.code, state: true }, groupName, actionName, buttonNum)
}
if ('button' in data) {
contro.enabled = false
void Promise.resolve().then(() => { contro.enabled = true })
setBinding(data, groupName, actionName, buttonNum)
}
setAwaitingInputType(null)
}
const updateBindMap = () => {
bindsMap.current = { keyboard: {} as any, gamepad: {} as any }
if (commands) {
for (const [group, actions] of Object.entries(commands)) {
for (const [action, { keys, gamepad }] of Object.entries(actions)) {
if (keys) {
let currKeys
if (userConfig?.[group]?.[action]?.keys) {
currKeys = userConfig[group][action].keys
} else {
currKeys = keys
}
for (const [index, key] of currKeys.entries()) {
bindsMap.current.keyboard[key] ??= []
if (!bindsMap.current.keyboard[key].some(obj => obj.group === group && obj.action === action && obj.index === index)) {
bindsMap.current.keyboard[key].push({ group, action, index })
}
}
}
if (gamepad) {
let currButtons
if (userConfig?.[group]?.[action]?.gamepad) {
currButtons = userConfig[group][action].gamepad
} else {
currButtons = gamepad
}
if (currButtons.length > 0) {
bindsMap.current.gamepad[currButtons[0]] ??= []
bindsMap.current.gamepad[currButtons[0]].push({ group, action, index: 0 })
}
}
}
}
}
}
// fill binds map
useEffect(() => {
updateBindMap()
}, [])
useEffect(() => {
contro.on('pressedKeyOrButtonChanged', updateBinding)
return () => {
contro.off('pressedKeyOrButtonChanged', updateBinding)
}
}, [groupName, actionName, awaitingInputType])
return <Context.Provider value={{
isPS,
userConfig,
setUserConfig,
handleClick,
parseBindingName,
bindsMap: bindsMap.current
}}>
<Screen title="Keybindings" backdrop>
{awaitingInputType && <AwaitingInputOverlay isGamepad={awaitingInputType === 'gamepad'} />}
<div className={styles.container}
ref={containerRef}
>
<Button
onClick={() => { hideModal() }}
style={{ alignSelf: 'center' }}
>Back</Button>
{Object.entries(commands).map(([group, actions], index) => {
if (group === 'custom') return null
return <div key={`group-container-${group}-${index}`} className={styles.group}>
<div className={styles['group-category']}>{group}</div>
{group === 'general' ? (
<div style={{
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}
{Object.entries(actions).map(([action, { keys, gamepad }]) => {
return <div key={`action-container-${action}`} className={styles.actionBinds}>
<div className={styles.actionName}>{parseActionName(action)}</div>
<Button
onClick={() => {
updateCurrBind(group, action)
resetBinding(group, action, 'keyboard')
}}
style={{ opacity: userConfig?.[group]?.[action]?.keys?.length ? 1 : 0 }}
className={styles['undo-keyboard']}
icon={'pixelarticons:undo'}
/>
{[0, 1].map((key, index) => <ButtonWithMatchesAlert
key={`keyboard-${group}-${action}-${index}`}
group={group}
action={action}
index={index}
inputType={'keyboard'}
keys={keys}
gamepad={gamepad}
/>)}
<Button
key={`keyboard-${group}-${action}`}
onClick={() => {
updateCurrBind(group, action)
resetBinding(group, action, 'gamepad')
}}
style={{
opacity: userConfig?.[group]?.[action]?.gamepad?.length ? 1 : 0,
width: '0px'
}}
className={`${styles['undo-gamepad']} ${styles['margin-left']}`}
icon={'pixelarticons:undo'}
/>
<ButtonWithMatchesAlert
key={`gamepad-${group}-${action}`}
group={group}
action={action}
index={0}
inputType={'gamepad'}
keys={keys}
gamepad={gamepad}
/>
</div>
})}
</div>
})}
<KeybindingsCustom
customCommands={customCommands}
updateCurrBind={updateCurrBind}
resetBinding={resetBinding}
/>
</div>
</Screen>
</Context.Provider>
}
export const ButtonWithMatchesAlert = ({
group,
action,
index,
inputType,
keys,
gamepad,
}) => {
const { isPS, userConfig, handleClick, parseBindingName, bindsMap } = useContext(Context)
const [buttonSign, setButtonSign] = useState('')
useEffect(() => {
const type = inputType === 'keyboard' ? 'keys' : 'gamepad'
const customValue = userConfig?.[group]?.[action]?.[type]?.[index]
if (customValue) {
if (type === 'keys') {
setButtonSign(parseBindingName(customValue))
} else {
setButtonSign(isPS && buttonsMap[customValue] ? buttonsMap[customValue] : customValue)
}
} else if (type === 'keys') {
setButtonSign(keys?.length ? parseBindingName(keys[index]) : '')
} else {
setButtonSign(gamepad?.[0] ?
isPS ?
buttonsMap[gamepad[0]] ?? gamepad[0]
: gamepad[0]
: '')
}
}, [userConfig, isPS])
return <div
key={`warning-container-${inputType}-${action}`}
className={`${styles['warning-container']}`}
>
<Button
key={`${inputType}-${group}-${action}-${index}`}
onClick={() => handleClick(group, action, index, inputType)}
className={`${styles.button}`}>
{buttonSign}
</Button>
{userConfig?.[group]?.[action]?.[inputType === 'keyboard' ? 'keys' : 'gamepad']?.some(
key => Object.keys(bindsMap[inputType]).includes(key)
&& bindsMap[inputType][key].length > 1
&& bindsMap[inputType][key].some(
prop => prop.index === index
&& prop.group === group
&& prop.action === action
)
) ? (
<div id={`bind-warning-${group}-${action}-${inputType}-${index}`} className={styles['matched-bind-warning']}>
<PixelartIcon
iconName={'alert'}
width={5}
styles={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginRight: '2px'
}} />
<div>
This bind is already in use. <span></span>
</div>
</div>
) : null}
</div>
}
export const AwaitingInputOverlay = ({ isGamepad }) => {
return <div style={{
position: 'fixed',
inset: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
color: 'white',
fontSize: 24,
zIndex: 10
}}
>
<div >
{isGamepad ? 'Press the button on the gamepad ' : 'Press the key, side mouse button '}
or ESC to cancel.
</div>
<Button
onClick={() => {
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
}}
>
Cancel
</Button>
</div>
}
const parseActionName = (action: string) => {
const parts = action.split(/(?=[A-Z])/)
parts[0] = parts[0].charAt(0).toUpperCase() + parts[0].slice(1)
return parts.join(' ')
}
const parseBindingName = (binding: string | undefined) => {
if (!binding) return ''
const cut = binding.replaceAll(/(Numpad|Digit|Key)/g, '')
const parts = cut.split(/(?=[A-Z\d])/)
return parts.reverse().join(' ')
}
const buttonsMap = {
'A': cross,
'B': circle,
'X': square,
'Y': triangle
}

View file

@ -1,51 +0,0 @@
import { useState } from 'react'
import { contro } from '../controls'
import Button from './Button'
import Screen from './Screen'
export default () => {
const { commands } = contro.inputSchema
const [awaitingInputType, setAwaitingInputType] = useState(null as null | 'keyboard' | 'gamepad')
// const
return <Screen title="Keybindings" backdrop>
{awaitingInputType && <AwaitingInputOverlay isGamepad={awaitingInputType === 'gamepad'} />}
<p>Here you can change the keybindings for the game.</p>
<div style={{
display: 'flex',
justifyContent: 'center',
}}>
{Object.entries(commands).map(([group, actions]) => {
return <div>
<h2>{group}</h2>
{Object.entries(actions).map(([action, { keys, gamepadButtons }]) => {
return <div style={{
display: 'flex',
gap: 5
}}>
<Button>{keys.join(', ')}</Button>
<Button>{gamepadButtons.join(', ')}</Button>
</div>
})}
</div>
})}
</div>
</Screen>
}
const AwaitingInputOverlay = ({ isGamepad }) => {
return <div style={{
position: 'fixed',
inset: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
color: 'white',
fontSize: 24,
}}>
{isGamepad ? 'Press the button on the gamepad' : 'Press the key'}.
Press ESC to cancel.
</div>
}

View file

@ -0,0 +1,50 @@
import { createContext, useState } from 'react'
import { contro } from '../controls'
import KeybindingsScreen from './KeybindingsScreen'
import { useIsModalActive } from './utilsApp'
export const updateBinds = (commands: any) => {
contro.inputSchema.commands.custom = Object.fromEntries(Object.entries(commands?.custom ?? {}).map(([key, value]) => {
return [key, {
keys: [],
gamepad: [],
type: '',
inputs: []
}]
}))
for (const [group, actions] of Object.entries(commands)) {
contro.userConfig![group] = Object.fromEntries(Object.entries(actions).map(([key, value]) => {
const newValue = {
keys: value?.keys ?? undefined,
gamepad: value?.gamepad ?? undefined,
}
if (group === 'custom') {
newValue['type'] = (value).type
newValue['inputs'] = (value).inputs
}
return [key, newValue]
}))
}
}
const bindingActions = {
updateBinds
}
export const BindingActionsContext = createContext(bindingActions)
export default () => {
const [bindActions, setBindActions] = useState(bindingActions)
const isModalActive = useIsModalActive('keybindings')
if (!isModalActive) return null
const hasPsGamepad = [...(navigator.getGamepads?.() ?? [])].some(gp => gp?.id.match(/playstation|dualsense|dualshock/i)) // todo: use last used gamepad detection
return <BindingActionsContext.Provider value={bindActions}>
<KeybindingsScreen isPS={hasPsGamepad} contro={contro} />
</BindingActionsContext.Provider>
}

View file

@ -3,7 +3,7 @@ import { options } from '../optionsStorage'
import { OptionsGroupType, guiOptionsScheme } from '../optionsGuiScheme'
import OptionsItems, { OptionMeta } from './OptionsItems'
const optionValueToType = (optionValue: any, item: OptionMeta) => {
export const optionValueToType = (optionValue: any, item: OptionMeta) => {
if (typeof optionValue === 'boolean' || item.values) return 'toggle'
if (typeof optionValue === 'number') return 'slider'
if (typeof optionValue === 'string') return 'element'

View file

@ -25,6 +25,10 @@ declare module '*.png' {
const png: string
export default png
}
declare module '*.svg' {
const svg: string
export default svg
}
interface PromiseConstructor {
withResolvers<T> (): {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,13 @@
import { CustomCommand } from './KeybindingsCustom'
type StorageData = {
customCommands: Record<string, CustomCommand>
// ...
}
export const getStoredValue = <T extends keyof StorageData> (name: T): StorageData[T] | undefined => {
return localStorage[name] ? JSON.parse(localStorage[name]) : undefined
}
export const setStoredValue = <T extends keyof StorageData> (name: T, value: StorageData[T]) => {
localStorage[name] = JSON.stringify(value)
}

View file

@ -36,6 +36,7 @@ import Crosshair from './react/Crosshair'
import ButtonAppProvider from './react/ButtonAppProvider'
import ServersListProvider from './react/ServersListProvider'
import GamepadUiCursor from './react/GamepadUiCursor'
import KeybindingsScreenProvider from './react/KeybindingsScreenProvider'
import HeldMapUi from './react/HeldMapUi'
const RobustPortal = ({ children, to }) => {
@ -157,6 +158,7 @@ const App = () => {
<SingleplayerProvider />
<CreateWorldProvider />
<AppStatusProvider />
<KeybindingsScreenProvider />
<SelectOption />
<ServersListProvider />
<OptionsRenderApp />

View file

@ -33,6 +33,7 @@
margin-top: 35px;
/* todo remove it but without it in chrome android the screen is not scrollable */
overflow: auto;
height: fit-content;
/* todo I'm not sure about it */
/* margin-top: calc(100% / 6 - 16px); */
align-items: center;