feat: Key binds screen (#108)
--------- Co-authored-by: gguio <nikvish150@gmail.com> Co-authored-by: Vitaly <vital2580@icloud.com>
This commit is contained in:
parent
6375df1576
commit
6bf1085fbe
21 changed files with 857 additions and 71 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
10
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
91
src/customCommands.ts
Normal 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: {}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
171
src/react/KeybindingsCustom.tsx
Normal file
171
src/react/KeybindingsCustom.tsx
Normal 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>
|
||||
}
|
||||
88
src/react/KeybindingsScreen.module.css
Normal file
88
src/react/KeybindingsScreen.module.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
50
src/react/KeybindingsScreenProvider.tsx
Normal file
50
src/react/KeybindingsScreenProvider.tsx
Normal 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>
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
4
src/react/globals.d.ts
vendored
4
src/react/globals.d.ts
vendored
|
|
@ -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 |
13
src/react/storageProvider.ts
Normal file
13
src/react/storageProvider.ts
Normal 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)
|
||||
}
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue