feat: 加了 4 个可调项:Forward、Strafe、Turn、Turbo
This commit is contained in:
@@ -17,6 +17,13 @@ type ButtonFeedback = {
|
||||
pressed: boolean
|
||||
}
|
||||
|
||||
type ControlTuning = {
|
||||
forward: number
|
||||
strafe: number
|
||||
turn: number
|
||||
turbo: number
|
||||
}
|
||||
|
||||
const TRACKED_KEYS = ['KeyW', 'KeyS', 'KeyA', 'KeyD', 'KeyQ', 'KeyE', 'ShiftLeft', 'ShiftRight', 'Space']
|
||||
const KEY_LABELS: Record<string, string> = {
|
||||
KeyW: 'W',
|
||||
@@ -33,11 +40,18 @@ const GAMEPAD_BUTTON_LABELS = ['A', 'B', 'X', 'Y', 'LB', 'RB', 'LT', 'RT', 'Back
|
||||
|
||||
const ZERO_COMMAND: CommandTuple = [0, 0, 0, 0, 0, 0]
|
||||
const GAMEPAD_DEADZONE = 0.14
|
||||
const TURBO_MULTIPLIER = 1.5
|
||||
const COMMAND_SEND_INTERVAL_MS = 50
|
||||
const KEYBOARD_FORWARD_SPEED = 0.8
|
||||
const KEYBOARD_STRAFE_SPEED = 0.15
|
||||
const KEYBOARD_TURN_SPEED = 0.4
|
||||
const DEFAULT_CONTROL_TUNING: ControlTuning = {
|
||||
forward: 0.8,
|
||||
strafe: 0.15,
|
||||
turn: 0.4,
|
||||
turbo: 1.5,
|
||||
}
|
||||
const CONTROL_TUNING_STORAGE_KEY = 'robot-command-center.control-tuning'
|
||||
const MIN_AXIS_SPEED = 0.05
|
||||
const MAX_AXIS_SPEED = 3
|
||||
const MIN_TURBO_MULTIPLIER = 1
|
||||
const MAX_TURBO_MULTIPLIER = 3
|
||||
|
||||
const pressedKeys = ref<Set<string>>(new Set())
|
||||
const socketState = ref<SocketState>('connecting')
|
||||
@@ -51,6 +65,65 @@ const gamepadAxes = ref<number[]>([0, 0, 0, 0])
|
||||
const gamepadButtonPressed = ref<boolean[]>(Array.from({ length: GAMEPAD_BUTTON_LABELS.length }, () => false))
|
||||
const activeSource = ref<ControlSource>('idle')
|
||||
|
||||
function clampValue(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
|
||||
function sanitizeAxisSpeed(value: unknown, fallback: number) {
|
||||
const numericValue = typeof value === 'number' ? value : Number(value)
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
return fallback
|
||||
}
|
||||
return roundValue(clampValue(numericValue, MIN_AXIS_SPEED, MAX_AXIS_SPEED))
|
||||
}
|
||||
|
||||
function sanitizeTurboMultiplier(value: unknown, fallback: number) {
|
||||
const numericValue = typeof value === 'number' ? value : Number(value)
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
return fallback
|
||||
}
|
||||
return roundValue(clampValue(numericValue, MIN_TURBO_MULTIPLIER, MAX_TURBO_MULTIPLIER))
|
||||
}
|
||||
|
||||
function normalizeControlTuning(raw?: Partial<ControlTuning>): ControlTuning {
|
||||
return {
|
||||
forward: sanitizeAxisSpeed(raw?.forward, DEFAULT_CONTROL_TUNING.forward),
|
||||
strafe: sanitizeAxisSpeed(raw?.strafe, DEFAULT_CONTROL_TUNING.strafe),
|
||||
turn: sanitizeAxisSpeed(raw?.turn, DEFAULT_CONTROL_TUNING.turn),
|
||||
turbo: sanitizeTurboMultiplier(raw?.turbo, DEFAULT_CONTROL_TUNING.turbo),
|
||||
}
|
||||
}
|
||||
|
||||
function loadPersistedControlTuning() {
|
||||
if (typeof window === 'undefined') {
|
||||
return DEFAULT_CONTROL_TUNING
|
||||
}
|
||||
|
||||
let raw: string | null = null
|
||||
|
||||
try {
|
||||
raw = window.localStorage.getItem(CONTROL_TUNING_STORAGE_KEY)
|
||||
} catch {
|
||||
return DEFAULT_CONTROL_TUNING
|
||||
}
|
||||
|
||||
if (raw == null) {
|
||||
return DEFAULT_CONTROL_TUNING
|
||||
}
|
||||
|
||||
try {
|
||||
return normalizeControlTuning(JSON.parse(raw) as Partial<ControlTuning>)
|
||||
} catch {
|
||||
return DEFAULT_CONTROL_TUNING
|
||||
}
|
||||
}
|
||||
|
||||
const initialControlTuning = loadPersistedControlTuning()
|
||||
const forwardSpeed = ref(initialControlTuning.forward)
|
||||
const strafeSpeed = ref(initialControlTuning.strafe)
|
||||
const turnSpeed = ref(initialControlTuning.turn)
|
||||
const turboMultiplier = ref(initialControlTuning.turbo)
|
||||
|
||||
let socket: WebSocket | null = null
|
||||
let sendTimer: number | null = null
|
||||
let reconnectTimer: number | null = null
|
||||
@@ -72,6 +145,54 @@ function roundValue(value: number) {
|
||||
return Math.round(value * 1000) / 1000
|
||||
}
|
||||
|
||||
function persistControlTuning() {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
CONTROL_TUNING_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
forward: forwardSpeed.value,
|
||||
strafe: strafeSpeed.value,
|
||||
turn: turnSpeed.value,
|
||||
turbo: turboMultiplier.value,
|
||||
}),
|
||||
)
|
||||
} catch {
|
||||
// Ignore storage failures so tuning still works for the current session.
|
||||
}
|
||||
}
|
||||
|
||||
function setControlTuning(next: Partial<ControlTuning>) {
|
||||
const resolved = normalizeControlTuning({
|
||||
forward: next.forward ?? forwardSpeed.value,
|
||||
strafe: next.strafe ?? strafeSpeed.value,
|
||||
turn: next.turn ?? turnSpeed.value,
|
||||
turbo: next.turbo ?? turboMultiplier.value,
|
||||
})
|
||||
const changed =
|
||||
resolved.forward !== forwardSpeed.value ||
|
||||
resolved.strafe !== strafeSpeed.value ||
|
||||
resolved.turn !== turnSpeed.value ||
|
||||
resolved.turbo !== turboMultiplier.value
|
||||
|
||||
forwardSpeed.value = resolved.forward
|
||||
strafeSpeed.value = resolved.strafe
|
||||
turnSpeed.value = resolved.turn
|
||||
turboMultiplier.value = resolved.turbo
|
||||
persistControlTuning()
|
||||
|
||||
if (changed) {
|
||||
refreshSendLoop(true)
|
||||
}
|
||||
}
|
||||
|
||||
function resetControlTuning() {
|
||||
setControlTuning(DEFAULT_CONTROL_TUNING)
|
||||
}
|
||||
|
||||
function packCommand(values: CommandTuple) {
|
||||
const buffer = new ArrayBuffer(24)
|
||||
const view = new DataView(buffer)
|
||||
@@ -95,18 +216,18 @@ function activeTurnAxis() {
|
||||
|
||||
function keyboardCommandValues(): CommandTuple {
|
||||
const keys = pressedKeys.value
|
||||
const turbo = keys.has('ShiftLeft') || keys.has('ShiftRight') ? TURBO_MULTIPLIER : 1
|
||||
const turbo = keys.has('ShiftLeft') || keys.has('ShiftRight') ? turboMultiplier.value : 1
|
||||
|
||||
let lx = 0
|
||||
let ly = 0
|
||||
let az = 0
|
||||
|
||||
if (keys.has('KeyW')) lx += KEYBOARD_FORWARD_SPEED
|
||||
if (keys.has('KeyS')) lx -= KEYBOARD_FORWARD_SPEED
|
||||
if (keys.has('KeyA')) ly += KEYBOARD_STRAFE_SPEED
|
||||
if (keys.has('KeyD')) ly -= KEYBOARD_STRAFE_SPEED
|
||||
if (keys.has('KeyQ')) az += KEYBOARD_TURN_SPEED
|
||||
if (keys.has('KeyE')) az -= KEYBOARD_TURN_SPEED
|
||||
if (keys.has('KeyW')) lx += forwardSpeed.value
|
||||
if (keys.has('KeyS')) lx -= forwardSpeed.value
|
||||
if (keys.has('KeyA')) ly += strafeSpeed.value
|
||||
if (keys.has('KeyD')) ly -= strafeSpeed.value
|
||||
if (keys.has('KeyQ')) az += turnSpeed.value
|
||||
if (keys.has('KeyE')) az -= turnSpeed.value
|
||||
|
||||
if (keys.has('Space')) {
|
||||
return ZERO_COMMAND
|
||||
@@ -128,15 +249,15 @@ function gamepadCommandValues(): CommandTuple {
|
||||
}
|
||||
|
||||
const buttons = gamepadButtonPressed.value
|
||||
const turbo = buttons[5] ? TURBO_MULTIPLIER : 1
|
||||
const turbo = buttons[5] ? turboMultiplier.value : 1
|
||||
|
||||
if (buttons[0]) {
|
||||
return ZERO_COMMAND
|
||||
}
|
||||
|
||||
const lx = roundValue(-normalizeAxis(gamepadAxes.value[1] ?? 0) * KEYBOARD_FORWARD_SPEED * turbo)
|
||||
const ly = roundValue(-normalizeAxis(gamepadAxes.value[0] ?? 0) * KEYBOARD_STRAFE_SPEED * turbo)
|
||||
const az = roundValue(-activeTurnAxis() * KEYBOARD_TURN_SPEED * turbo)
|
||||
const lx = roundValue(-normalizeAxis(gamepadAxes.value[1] ?? 0) * forwardSpeed.value * turbo)
|
||||
const ly = roundValue(-normalizeAxis(gamepadAxes.value[0] ?? 0) * strafeSpeed.value * turbo)
|
||||
const az = roundValue(-activeTurnAxis() * turnSpeed.value * turbo)
|
||||
|
||||
return [lx, ly, 0, 0, 0, az]
|
||||
}
|
||||
@@ -419,12 +540,13 @@ const commandLabel = computed(() => {
|
||||
|
||||
const commandMagnitude = computed(() => {
|
||||
const { lx, ly, az } = commandValues.value
|
||||
const limits = controlLimits.value
|
||||
return Math.min(
|
||||
1,
|
||||
Math.max(
|
||||
Math.abs(lx) / KEYBOARD_FORWARD_SPEED,
|
||||
Math.abs(ly) / KEYBOARD_STRAFE_SPEED,
|
||||
Math.abs(az) / KEYBOARD_TURN_SPEED,
|
||||
Math.abs(lx) / Math.max(limits.forward, MIN_AXIS_SPEED),
|
||||
Math.abs(ly) / Math.max(limits.strafe, MIN_AXIS_SPEED),
|
||||
Math.abs(az) / Math.max(limits.turn, MIN_AXIS_SPEED),
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -440,6 +562,17 @@ const keyboardKeys = computed<KeyFeedback[]>(() =>
|
||||
)
|
||||
|
||||
const keyboardTurbo = computed(() => pressedKeys.value.has('ShiftLeft') || pressedKeys.value.has('ShiftRight'))
|
||||
const controlTuning = computed<ControlTuning>(() => ({
|
||||
forward: forwardSpeed.value,
|
||||
strafe: strafeSpeed.value,
|
||||
turn: turnSpeed.value,
|
||||
turbo: turboMultiplier.value,
|
||||
}))
|
||||
const controlLimits = computed(() => ({
|
||||
forward: roundValue(forwardSpeed.value * turboMultiplier.value),
|
||||
strafe: roundValue(strafeSpeed.value * turboMultiplier.value),
|
||||
turn: roundValue(turnSpeed.value * turboMultiplier.value),
|
||||
}))
|
||||
|
||||
const gamepadButtons = computed<ButtonFeedback[]>(() =>
|
||||
GAMEPAD_BUTTON_LABELS.map((label, index) => ({
|
||||
@@ -476,6 +609,10 @@ export function useControlInterface() {
|
||||
commandValues,
|
||||
commandLabel,
|
||||
commandMagnitude,
|
||||
controlTuning,
|
||||
controlLimits,
|
||||
setControlTuning,
|
||||
resetControlTuning,
|
||||
pressedKeysLabel,
|
||||
keyboardKeys,
|
||||
keyboardTurbo,
|
||||
|
||||
Reference in New Issue
Block a user