From ce6e02ed1f19584005cd36ff3d547bcf0887796b Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 4 Sep 2023 07:39:16 +0300 Subject: [PATCH] refactor controls to use contro-max! it allows to use between mobile keyboard & gamepad consistently! fixed double jump! --- .gitignore | 3 +- .vscode/launch.json | 1 + package.json | 1 + src/botControls.js | 193 ------------------------ src/chat.js | 16 -- src/controls.ts | 266 +++++++++++++++++++++++++++++++++ src/index.js | 6 +- src/menus/components/hotbar.js | 4 +- src/menus/options_screen.js | 2 +- src/reactUi.jsx | 83 ++-------- tsconfig.json | 3 +- 11 files changed, 291 insertions(+), 287 deletions(-) delete mode 100644 src/botControls.js create mode 100644 src/controls.ts diff --git a/.gitignore b/.gitignore index 23b666f8..950306c9 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.vscode/launch.json b/.vscode/launch.json index abdc541b..87e66901 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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 diff --git a/package.json b/package.json index 2ccd2a25..ef3e2bbb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/botControls.js b/src/botControls.js deleted file mode 100644 index 829a0db7..00000000 --- a/src/botControls.js +++ /dev/null @@ -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|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} */ -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) diff --git a/src/chat.js b/src/chat.js index 2497d09d..bd36b837 100644 --- a/src/chat.js +++ b/src/chat.js @@ -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 } diff --git a/src/controls.ts b/src/controls.ts new file mode 100644 index 00000000..c6023333 --- /dev/null +++ b/src/controls.ts @@ -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>, + movementKeymap: 'WASD', + movementVector: '2d', + groupedCommands: { + general: { + switchSlot: ['Digits', []] + } + }, +}, { + target: document, + captureEvents() { + return bot && isGameActive(false) + }, + storeProvider: { + load: () => customKeymaps, + save() { }, + } +}) +export type Command = CommandEventArgument['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 = {} + 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 | 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 diff --git a/src/index.js b/src/index.js index c59ea2da..a1d66b9f 100644 --- a/src/index.js +++ b/src/index.js @@ -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') diff --git a/src/menus/components/hotbar.js b/src/menus/components/hotbar.js index 216454c3..7f65681e 100644 --- a/src/menus/components/hotbar.js +++ b/src/menus/components/hotbar.js @@ -224,9 +224,9 @@ class Hotbar extends LitElement {
- ${miscUiState.currentTouch && html`
{ + ${miscUiState.currentTouch ? html`
{ showModal({ reactType: 'inventory', }) - }}>`} + }}>` : undefined}
diff --git a/src/menus/options_screen.js b/src/menus/options_screen.js index ba0f19e8..fc2cfce1 100644 --- a/src/menus/options_screen.js +++ b/src/menus/options_screen.js @@ -113,7 +113,7 @@ class OptionsScreen extends CommonOptionsScreen { }}>
- showModal(document.getElementById('keybinds-screen'))}> + showModal(document.getElementById('keybinds-screen'))}> { this.changeOption('guiScale', e.target.value) document.documentElement.style.setProperty('--guiScale', `${this.guiScale}`) diff --git a/src/reactUi.jsx b/src/reactUi.jsx index 5a254adb..72751aaf 100644 --- a/src/reactUi.jsx +++ b/src/reactUi.jsx @@ -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 { - // scale: 0.6; - // background: transparent !important; - // } - // `}> - // { - // bot.moveSlotItem(oldSlot, newSlotIndex) - // } } /> - //
-} - const App = () => { const isBotAvailable = useIsBotAvailable() - if (!isBotAvailable) return null + // if (!isBotAvailable) return return
-
} diff --git a/tsconfig.json b/tsconfig.json index 883dcf2f..d6ed70d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "allowSyntheticDefaultImports": true, "noEmit": true, "strictFunctionTypes": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "noFallthroughCasesInSwitch": true // "strictNullChecks": true }, "include": [