refactor controls to use contro-max!

it allows to use between mobile keyboard & gamepad consistently!
fixed double jump!
This commit is contained in:
Vitaly 2023-09-04 07:39:16 +03:00
commit ce6e02ed1f
11 changed files with 291 additions and 287 deletions

3
.gitignore vendored
View file

@ -1,7 +1,7 @@
node_modules/
package-lock.json
.vscode
public
**/public
*.log
.env.local
Thumbs.db
@ -13,3 +13,4 @@ dist
world
out
*.iml
.vercel

1
.vscode/launch.json vendored
View file

@ -1,5 +1,6 @@
{
"configurations": [
// UPDATED: all configs below are misconfigured and will crash vscode, open dist/index.html and use live preview debug instead
// recommended as much faster
{
// to launch "C:\Program Files\Google\Chrome Beta\Application\chrome.exe" --remote-debugging-port=9222

View file

@ -62,6 +62,7 @@
"buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0",
"constants-browserify": "^1.0.0",
"contro-max": "^0.1.0",
"copy-webpack-plugin": "^11.0.0",
"crypto-browserify": "^3.12.0",
"css-loader": "^6.8.1",

View file

@ -1,193 +0,0 @@
//@ts-check
const { Vec3 } = require('vec3')
const { isGameActive, showModal, gameAdditionalState, activeModalStack } = require('./globalState')
const { subscribe } = require('valtio')
const keyBindScrn = document.getElementById('keybinds-screen')
// these controls are for gamemode 3 actually
const makeInterval = (fn, interval) => {
const intervalId = setInterval(fn, interval)
const cleanup = () => {
clearInterval(intervalId)
cleanup.active = false
}
cleanup.active = true
return cleanup
}
const flySpeedMult = 0.5
const isFlying = () => bot.physics.gravity === 0
/** @type {ReturnType<typeof makeInterval>|undefined} */
let endFlyLoop
const currentFlyVector = new Vec3(0, 0, 0)
window.currentFlyVector = currentFlyVector
const startFlyLoop = () => {
if (!isFlying()) return
endFlyLoop?.()
endFlyLoop = makeInterval(() => {
if (!window.bot) endFlyLoop()
bot.entity.position.add(currentFlyVector.clone().multiply(new Vec3(flySpeedMult, flySpeedMult, flySpeedMult)))
}, 50)
}
// todo we will get rid of patching it when refactor controls
let originalSetControlState
const patchedSetControlState = (action, state) => {
if (!isFlying()) {
return originalSetControlState(action, state)
}
const actionPerFlyVector = {
jump: new Vec3(0, 1, 0),
sneak: new Vec3(0, -1, 0),
}
const changeVec = actionPerFlyVector[action]
if (!changeVec) {
return originalSetControlState(action, state)
}
const toAddVec = changeVec.scaled(state ? 1 : -1)
for (const coord of ['x', 'y', 'z']) {
if (toAddVec[coord] === 0) continue
if (currentFlyVector[coord] === toAddVec[coord]) return
}
currentFlyVector.add(toAddVec)
}
const standardAirborneAcceleration = 0.02
const toggleFly = () => {
if (bot.game.gameMode !== 'creative' && bot.game.gameMode !== 'spectator') return
if (bot.setControlState !== patchedSetControlState) {
originalSetControlState = bot.setControlState
bot.setControlState = patchedSetControlState
}
if (isFlying()) {
bot.physics['airborneAcceleration'] = standardAirborneAcceleration
bot.creative.stopFlying()
endFlyLoop?.()
} else {
// window.flyingSpeed will be removed
bot.physics['airborneAcceleration'] = window.flyingSpeed ?? 0.1
bot.entity.velocity = new Vec3(0, 0, 0)
bot.creative.startFlying()
startFlyLoop()
}
gameAdditionalState.isFlying = isFlying()
}
/** @type {Set<string>} */
const pressedKeys = new Set()
// window.pressedKeys = pressedKeys
// detect pause open, as ANY keyup event is not fired when you exit pointer lock (esc)
subscribe(activeModalStack, () => {
if (activeModalStack.length) {
// iterate over pressedKeys
for (const key of pressedKeys) {
const e = new KeyboardEvent('keyup', { code: key })
document.dispatchEvent(e)
}
}
})
let lastJumpUsage = 0
document.addEventListener('keydown', (e) => {
if (!isGameActive(true)) return
keyBindScrn.keymaps.forEach(km => {
if (e.code === km.key) {
switch (km.defaultKey) {
case 'KeyE':
// todo reenable
showModal({ reactType: 'inventory', })
// todo seems to be workaround
// avoid calling inner keybinding listener, but should be handled there
e.stopImmediatePropagation()
break
case 'KeyQ':
if (bot.heldItem) bot.tossStack(bot.heldItem)
break
case 'ControlLeft':
bot.setControlState('sprint', true)
gameAdditionalState.isSprinting = true
break
case 'ShiftLeft':
bot.setControlState('sneak', true)
break
case 'Space':
bot.setControlState('jump', true)
break
case 'KeyD':
bot.setControlState('right', true)
e.preventDefault()
break
case 'KeyA':
bot.setControlState('left', true)
e.preventDefault()
break
case 'KeyS':
bot.setControlState('back', true)
e.preventDefault()
break
case 'KeyW':
bot.setControlState('forward', true)
break
}
}
})
pressedKeys.add(e.code)
}, {
capture: true,
})
document.addEventListener('keyup', (e) => {
// workaround for pause pressed keys, multiple keyboard
if (!isGameActive(false) || !pressedKeys.has(e.code)) {
return
}
keyBindScrn.keymaps.forEach(km => {
if (e.code === km.key) {
switch (km.defaultKey) {
case 'ControlLeft':
bot.setControlState('sprint', false)
gameAdditionalState.isSprinting = false
break
case 'ShiftLeft':
bot.setControlState('sneak', false)
break
case 'Space':
const toggleFlyAction = Date.now() - lastJumpUsage < 500
if (toggleFlyAction) {
toggleFly()
}
lastJumpUsage = Date.now()
bot.setControlState('jump', false)
break
case 'KeyD':
bot.setControlState('right', false)
break
case 'KeyA':
bot.setControlState('left', false)
break
case 'KeyS':
bot.setControlState('back', false)
break
case 'KeyW':
bot.setControlState('forward', false)
break
}
}
})
pressedKeys.delete(e.code)
}, false)

View file

@ -228,24 +228,8 @@ class ChatBox extends LitElement {
}
})
const keyBindScrn = document.getElementById('keybinds-screen')
document.addEventListener('keypress', e => {
if (!this.inChat && activeModalStack.length === 0) {
keyBindScrn.keymaps.forEach(km => {
if (e.code === km.key) {
switch (km.defaultKey) {
case 'KeyT':
setTimeout(() => this.enableChat(), 0)
break
case 'Slash':
setTimeout(() => this.enableChat('/'), 0)
e.preventDefault()
break
}
}
})
return false
}

266
src/controls.ts Normal file
View file

@ -0,0 +1,266 @@
//@ts-check
import { Vec3 } from 'vec3'
import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal } from './globalState'
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'
// doesnt seem to work for now
const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}'))
subscribe(customKeymaps, () => {
localStorage.keymap = JSON.parse(customKeymaps)
})
export const contro = new ControMax({
commands: {
general: {
jump: ['Space', 'A'],
inventory: ['KeyE', 'X'],
drop: ['KeyQ', 'B'],
sneak: ['ShiftLeft', 'Right Stick'],
sprint: ['ControlLeft', 'Left Stick'],
nextHotbarSlot: [null, 'Left Bumper'],
prevHotbarSlot: [null, 'Right Bumper'],
attackDestroy: [null, 'Right Trigger'],
interactPlace: [null, 'Left Trigger'],
chat: [['KeyT', 'Enter'], null],
command: ['Slash', null],
},
// waila: {
// showLookingBlockRecipe: ['Numpad3'],
// showLookingBlockUsages: ['Numpad4']
// }
} satisfies Record<string, Record<string, SchemaCommandInput>>,
movementKeymap: 'WASD',
movementVector: '2d',
groupedCommands: {
general: {
switchSlot: ['Digits', []]
}
},
}, {
target: document,
captureEvents() {
return bot && isGameActive(false)
},
storeProvider: {
load: () => customKeymaps,
save() { },
}
})
export type Command = CommandEventArgument<typeof contro['_commandsRaw']>['command']
const setSprinting = (state: boolean) => {
bot.setControlState('sprint', state)
gameAdditionalState.isSprinting = state
}
contro.on('movementUpdate', ({ vector, gamepadIndex }) => {
// gamepadIndex will be used for splitscreen in future
const coordToAction = [
['z', -1, 'forward'],
['z', 1, 'back'],
['x', -1, 'left'],
['x', 1, 'right'],
] as const
const newState: Partial<typeof bot.controlState> = {}
for (const [coord, v] of Object.entries(vector)) {
if (v === undefined || Math.abs(v) < 0.3) continue
// todo use raw values eg for slow movement
const mappedValue = v < 0 ? -1 : 1
const foundAction = coordToAction.find(([c, mapV]) => c === coord && mapV === mappedValue)?.[2]
newState[foundAction] = true
}
for (const key of ['forward', 'back', 'left', 'right'] as const) {
if (newState[key] === bot.controlState[key]) continue
const action = !!newState[key]
if (action && !isGameActive(true)) continue
bot.setControlState(key, action)
if (key === 'forward') {
// todo workaround: need to refactor
if (action) {
contro.emit('trigger', { command: 'general.forward' } as any)
} else {
setSprinting(false)
}
}
}
})
let lastCommandTrigger = null as { command: string, time: number } | null
const secondActionActivationTimeout = 600
const secondActionCommands = {
'general.jump'() {
toggleFly()
},
'general.forward'() {
setSprinting(true)
}
}
// detect pause open, as ANY keyup event is not fired when you exit pointer lock (esc)
subscribe(activeModalStack, () => {
if (activeModalStack.length) {
// iterate over pressedKeys
for (const key of contro.pressedKeys) {
contro.pressedKeyOrButtonChanged({ code: key }, false)
}
}
})
const onTriggerOrReleased = (command: Command, pressed: boolean) => {
// always allow release!
if (pressed && !isGameActive(true)) return
if (stringStartsWith(command, 'general')) {
// handle general commands
switch (command) {
case 'general.jump':
bot.setControlState('jump', pressed)
break
case 'general.sneak':
bot.setControlState('sneak', pressed)
break
case 'general.sprint':
setSprinting(pressed)
break
}
}
}
// im still not sure, maybe need to refactor to handle in inventory instead
const alwaysHandledCommand = (command: Command) => {
if (command === 'general.inventory') {
if (activeModalStack.at(-1)?.reactType === 'inventory') {
hideCurrentModal()
}
}
}
contro.on('trigger', ({ command }) => {
const willContinue = !isGameActive(true)
alwaysHandledCommand(command)
if (willContinue) return
const secondActionCommand = secondActionCommands[command]
if (secondActionCommand) {
const commandToTrigger = secondActionCommands[lastCommandTrigger?.command]
if (commandToTrigger && Date.now() - lastCommandTrigger.time < secondActionActivationTimeout) {
commandToTrigger()
lastCommandTrigger = null
} else {
lastCommandTrigger = {
command,
time: Date.now(),
}
}
}
onTriggerOrReleased(command, true)
if (stringStartsWith(command, 'general')) {
switch (command) {
case 'general.inventory':
document.exitPointerLock?.()
showModal({ reactType: 'inventory' })
break
case 'general.drop':
if (bot.heldItem) bot.tossStack(bot.heldItem)
break
case 'general.chat':
document.getElementById('hud').shadowRoot.getElementById('chat').enableChat()
break
case 'general.command':
document.getElementById('hud').shadowRoot.getElementById('chat').enableChat('/')
break
// todo place / destroy
}
}
})
contro.on('release', ({ command }) => {
onTriggerOrReleased(command, false)
})
// #region creative fly
// these controls are more like for gamemode 3
const makeInterval = (fn, interval) => {
const intervalId = setInterval(fn, interval)
const cleanup = () => {
clearInterval(intervalId)
cleanup.active = false
}
cleanup.active = true
return cleanup
}
const isFlying = () => bot.physics.gravity === 0
let endFlyLoop: ReturnType<typeof makeInterval> | undefined
const currentFlyVector = new Vec3(0, 0, 0)
window.currentFlyVector = currentFlyVector
const startFlyLoop = () => {
if (!isFlying()) return
endFlyLoop?.()
endFlyLoop = makeInterval(() => {
if (!window.bot) endFlyLoop()
bot.entity.position.add(currentFlyVector.clone().multiply(new Vec3(0, 0.5, 0)))
}, 50)
}
// todo we will get rid of patching it when refactor controls
let originalSetControlState
const patchedSetControlState = (action, state) => {
if (!isFlying()) {
return originalSetControlState(action, state)
}
const actionPerFlyVector = {
jump: new Vec3(0, 1, 0),
sneak: new Vec3(0, -1, 0),
}
const changeVec = actionPerFlyVector[action]
if (!changeVec) {
return originalSetControlState(action, state)
}
const toAddVec = changeVec.scaled(state ? 1 : -1)
for (const coord of ['x', 'y', 'z']) {
if (toAddVec[coord] === 0) continue
if (currentFlyVector[coord] === toAddVec[coord]) return
}
currentFlyVector.add(toAddVec)
}
const standardAirborneAcceleration = 0.02
const toggleFly = () => {
if (bot.game.gameMode !== 'creative' && bot.game.gameMode !== 'spectator') return
if (bot.setControlState !== patchedSetControlState) {
originalSetControlState = bot.setControlState
bot.setControlState = patchedSetControlState
}
if (isFlying()) {
bot.physics['airborneAcceleration'] = standardAirborneAcceleration
bot.creative.stopFlying()
endFlyLoop?.()
} else {
// window.flyingSpeed will be removed
bot.physics['airborneAcceleration'] = window.flyingSpeed ?? 0.1
bot.entity.velocity = new Vec3(0, 0, 0)
bot.creative.startFlying()
startFlyLoop()
}
gameAdditionalState.isFlying = isFlying()
}
// #endregion

View file

@ -37,7 +37,7 @@ require('./menus/title_screen')
require('./optionsStorage')
require('./reactUi.jsx')
require('./botControls')
require('./controls')
require('./dragndrop')
require('./browserfs')
require('./eruda')
@ -47,9 +47,7 @@ const net = require('net')
const Stats = require('stats.js')
const mineflayer = require('mineflayer')
const { WorldView, Viewer, MapControls } = require('prismarine-viewer/viewer')
const PrismarineWorld = require('prismarine-world')
const nbt = require('prismarine-nbt')
const { WorldView, Viewer } = require('prismarine-viewer/viewer')
const pathfinder = require('mineflayer-pathfinder')
const { Vec3 } = require('vec3')

View file

@ -224,9 +224,9 @@ class Hotbar extends LitElement {
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
${miscUiState.currentTouch && html`<div class="hotbar-item hotbar-more" @click=${() => {
${miscUiState.currentTouch ? html`<div class="hotbar-item hotbar-more" @click=${() => {
showModal({ reactType: 'inventory', })
}}>`}
}}>` : undefined}
</div>
</div>
</div>

View file

@ -113,7 +113,7 @@ class OptionsScreen extends CommonOptionsScreen {
}}></pmui-slider>
</div>
<div class="wrapper">
<pmui-button pmui-width="150px" pmui-label="Key Binds" @pmui-click=${() => showModal(document.getElementById('keybinds-screen'))}></pmui-button>
<pmui-button pmui-disabled="true" pmui-width="150px" pmui-label="Key Binds" @pmui-click=${() => showModal(document.getElementById('keybinds-screen'))}></pmui-button>
<pmui-slider pmui-label="Gui Scale" pmui-value="${this.guiScale}" pmui-min="1" pmui-max="4" pmui-type="" @change=${(e) => {
this.changeOption('guiScale', e.target.value)
document.documentElement.style.setProperty('--guiScale', `${this.guiScale}`)

View file

@ -1,13 +1,13 @@
//@ts-check
import { renderToDom } from '@zardoy/react-util'
import { LeftTouchArea, InventoryNew, RightTouchArea, useUsingTouch, useInterfaceState } from '@dimaka/interface'
import { LeftTouchArea, RightTouchArea, useUsingTouch, useInterfaceState } from '@dimaka/interface'
import { css } from '@emotion/css'
import { activeModalStack, hideCurrentModal, isGameActive } from './globalState'
import { useEffect, useState } from 'react'
import { useProxy } from 'valtio/utils'
import useTypedEventListener from 'use-typed-event-listener'
import { activeModalStack, isGameActive } from './globalState'
import { isProbablyIphone } from './menus/components/common'
// import DeathScreen from './react/DeathScreen'
import { useSnapshot } from 'valtio'
import { contro } from './controls'
// todo
useInterfaceState.setState({
@ -17,23 +17,22 @@ useInterfaceState.setState({
},
updateCoord: ([coord, state]) => {
const coordToAction = [
['z', -1, 'forward'],
['z', 1, 'back'],
['x', -1, 'left'],
['x', 1, 'right'],
['y', 1, 'jump'],
['z', -1, 'KeyW'],
['z', 1, 'KeyS'],
['x', -1, 'KeyA'],
['x', 1, 'KeyD'],
['y', 1, 'Space'], // todo jump
]
// todo refactor
const actionAndState = state !== 0 ? coordToAction.find(([axis, value]) => axis === coord && value === state) : coordToAction.filter(([axis]) => axis === coord)
if (!bot) return
if (state === 0) {
for (const action of actionAndState) {
//@ts-ignore
bot.setControlState(action[2], false)
contro.pressedKeyOrButtonChanged({code: action[2],}, false)
}
} else {
//@ts-ignore
bot.setControlState(actionAndState[2], true)
contro.pressedKeyOrButtonChanged({code: actionAndState[2],}, true)
}
}
})
@ -67,71 +66,17 @@ const TouchControls = () => {
)
}
const useActivateModal = (/** @type {string} */search, onlyLast = true) => {
const stack = useProxy(activeModalStack)
return onlyLast ? stack.at(-1)?.reactType === search : stack.some((modal) => modal.reactType === search)
}
function useIsBotAvailable() {
const stack = useProxy(activeModalStack)
const stack = useSnapshot(activeModalStack)
return isGameActive(false)
}
function InventoryWrapper() {
const isInventoryOpen = useActivateModal('inventory', false)
const [slots, setSlots] = useState(bot.inventory.slots)
useEffect(() => {
if (isInventoryOpen) {
document.exitPointerLock?.()
}
}, [isInventoryOpen])
useTypedEventListener(document, 'keydown', (e) => {
// todo use refactored keymap
if (e.code === 'KeyE' && activeModalStack.at(-1)?.reactType === 'inventory') {
hideCurrentModal()
}
})
useEffect(() => {
bot.inventory.on('updateSlot', () => {
setSlots([...bot.inventory.slots])
})
// todo need to think of better solution
window['mcData'] = require('minecraft-data')(bot.version)
window['mcAssets'] = require('minecraft-assets')(bot.version)
}, [])
if (!isInventoryOpen) return null
return null
// return <div className={css`
// position: fixed;
// width: 100%;
// height: 100%;
// background: rgba(0, 0, 0, 0.5);
// & > div {
// scale: 0.6;
// background: transparent !important;
// }
// `}>
// <InventoryNew slots={slots} action={(oldSlot, newSlotIndex) => {
// bot.moveSlotItem(oldSlot, newSlotIndex)
// } } />
// </div>
}
const App = () => {
const isBotAvailable = useIsBotAvailable()
if (!isBotAvailable) return null
// if (!isBotAvailable) return <DeathScreen />
return <div>
<InventoryWrapper />
<TouchControls />
</div>
}

View file

@ -8,7 +8,8 @@
"allowSyntheticDefaultImports": true,
"noEmit": true,
"strictFunctionTypes": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"noFallthroughCasesInSwitch": true
// "strictNullChecks": true
},
"include": [