|
|
@@ -25,6 +25,9 @@ export class GamepadMapper {
|
|
|
this.eventListeners = new Map();
|
|
|
this.isPolling = false;
|
|
|
|
|
|
+ // Dynamic calibration tracking
|
|
|
+ this.calibrationData = new Map(); // gamepadIndex -> { axisName -> { min, max } }
|
|
|
+
|
|
|
// Standard controller mappings
|
|
|
this.controllerMappings = this.initializeControllerMappings();
|
|
|
|
|
|
@@ -170,6 +173,9 @@ export class GamepadMapper {
|
|
|
axes: {}
|
|
|
});
|
|
|
|
|
|
+ // Initialize calibration data for this gamepad
|
|
|
+ this.calibrationData.set(gamepad.index, {});
|
|
|
+
|
|
|
this.emit('connected', {
|
|
|
index: gamepad.index,
|
|
|
id: gamepad.id,
|
|
|
@@ -183,6 +189,7 @@ export class GamepadMapper {
|
|
|
|
|
|
this.connectedGamepads.delete(index);
|
|
|
this.previousStates.delete(index);
|
|
|
+ this.calibrationData.delete(index);
|
|
|
|
|
|
// Clear any pending debounce timers
|
|
|
if (this.buttonDebounceTimers.has(index)) {
|
|
|
@@ -298,13 +305,18 @@ export class GamepadMapper {
|
|
|
const axisName = mapping[axisIndex];
|
|
|
if (!axisName) return;
|
|
|
|
|
|
+ // Update calibration data for non-DPAD axes
|
|
|
+ if (axisName !== 'DPAD_AXIS') {
|
|
|
+ this.updateCalibration(gamepad.index, axisName, axisValue);
|
|
|
+ }
|
|
|
+
|
|
|
// DPAD_AXIS is immune to deadzone filtering and gets special processing
|
|
|
let processedValue;
|
|
|
if (axisName === 'DPAD_AXIS') {
|
|
|
processedValue = this.processDpadAxis(axisValue);
|
|
|
} else {
|
|
|
- // Apply deadzone to all other axes
|
|
|
- processedValue = this.applyDeadzone(axisValue);
|
|
|
+ // Apply dynamic calibration and deadzone to all other axes
|
|
|
+ processedValue = this.applyDynamicCalibration(gamepad.index, axisName, axisValue);
|
|
|
}
|
|
|
|
|
|
const previousValue = previousState.axes[axisName] || 0;
|
|
|
@@ -366,6 +378,94 @@ export class GamepadMapper {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
+ updateCalibration(gamepadIndex, axisName, rawValue) {
|
|
|
+ const gamepadCalibration = this.calibrationData.get(gamepadIndex);
|
|
|
+
|
|
|
+ if (!gamepadCalibration[axisName]) {
|
|
|
+ gamepadCalibration[axisName] = {
|
|
|
+ min: rawValue, // Start with actual observed value
|
|
|
+ max: rawValue, // Start with actual observed value
|
|
|
+ neutral: 0.0, // Assume neutral is zero instead of first raw value
|
|
|
+ neutralSamples: [0.0],
|
|
|
+ positiveMax: 0.3, // Start with conservative positive range
|
|
|
+ negativeMin: -0.3, // Start with conservative negative range
|
|
|
+ dynamicDeadzone: 0.01 // Start with small default deadzone
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ const cal = gamepadCalibration[axisName];
|
|
|
+
|
|
|
+ // Update overall min/max
|
|
|
+ cal.min = Math.min(cal.min, rawValue);
|
|
|
+ cal.max = Math.max(cal.max, rawValue);
|
|
|
+
|
|
|
+ // Track neutral position (values close to initial position)
|
|
|
+ const neutralThreshold = 0.05;
|
|
|
+ if (Math.abs(rawValue - cal.neutral) < neutralThreshold) {
|
|
|
+ cal.neutralSamples.push(rawValue);
|
|
|
+ // Keep only recent samples for rolling average
|
|
|
+ if (cal.neutralSamples.length > 100) {
|
|
|
+ cal.neutralSamples = cal.neutralSamples.slice(-50);
|
|
|
+ }
|
|
|
+ // Update neutral as average of samples
|
|
|
+ cal.neutral = cal.neutralSamples.reduce((a, b) => a + b, 0) / cal.neutralSamples.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Track separate positive and negative extremes
|
|
|
+ if (rawValue > cal.neutral) {
|
|
|
+ cal.positiveMax = Math.max(cal.positiveMax, rawValue);
|
|
|
+ } else if (rawValue < cal.neutral) {
|
|
|
+ cal.negativeMin = Math.min(cal.negativeMin, rawValue);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Calculate dynamic deadzone based on neutral drift
|
|
|
+ const neutralRange = Math.max(...cal.neutralSamples) - Math.min(...cal.neutralSamples);
|
|
|
+ cal.dynamicDeadzone = Math.max(neutralRange * 1.5, 0.01); // 1.5x the neutral drift range
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ applyDynamicCalibration(gamepadIndex, axisName, rawValue) {
|
|
|
+ const gamepadCalibration = this.calibrationData.get(gamepadIndex);
|
|
|
+ const axisCalibration = gamepadCalibration[axisName];
|
|
|
+
|
|
|
+ // If no calibration data yet, use standard deadzone
|
|
|
+ if (!axisCalibration) {
|
|
|
+ return this.applyDeadzone(rawValue);
|
|
|
+ }
|
|
|
+
|
|
|
+ const { neutral, positiveMax, negativeMin, dynamicDeadzone } = axisCalibration;
|
|
|
+
|
|
|
+ // Use dynamic deadzone if available, otherwise fall back to config deadzone
|
|
|
+ const effectiveDeadzone = Math.max(dynamicDeadzone, this.config.deadzone);
|
|
|
+
|
|
|
+ // Apply deadzone around neutral position
|
|
|
+ const distanceFromNeutral = Math.abs(rawValue - neutral);
|
|
|
+ if (distanceFromNeutral < effectiveDeadzone) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle positive and negative sides separately
|
|
|
+ let normalizedValue;
|
|
|
+ if (rawValue > neutral) {
|
|
|
+ // Positive side: map from neutral to positiveMax -> 0 to 1
|
|
|
+ const positiveRange = positiveMax - neutral;
|
|
|
+ if (positiveRange < 0.01) return 0; // Range too small
|
|
|
+
|
|
|
+ const adjustedValue = rawValue - neutral - effectiveDeadzone;
|
|
|
+ const adjustedRange = positiveRange - effectiveDeadzone;
|
|
|
+ normalizedValue = Math.min(1, Math.max(0, adjustedValue / adjustedRange));
|
|
|
+ } else {
|
|
|
+ // Negative side: map from negativeMin to neutral -> -1 to 0
|
|
|
+ const negativeRange = neutral - negativeMin;
|
|
|
+ if (negativeRange < 0.01) return 0; // Range too small
|
|
|
+
|
|
|
+ const adjustedValue = neutral - rawValue - effectiveDeadzone;
|
|
|
+ const adjustedRange = negativeRange - effectiveDeadzone;
|
|
|
+ normalizedValue = -Math.min(1, Math.max(0, adjustedValue / adjustedRange));
|
|
|
+ }
|
|
|
+
|
|
|
+ return normalizedValue;
|
|
|
+ }
|
|
|
+
|
|
|
applyDeadzone(value) {
|
|
|
if (Math.abs(value) < this.config.deadzone) {
|
|
|
return 0;
|
|
|
@@ -456,6 +556,11 @@ export class GamepadMapper {
|
|
|
this.config.debounceTime = Math.max(0, time);
|
|
|
}
|
|
|
|
|
|
+ // Get calibration data for debugging
|
|
|
+ getCalibrationData(gamepadIndex) {
|
|
|
+ return this.calibrationData.get(gamepadIndex) || {};
|
|
|
+ }
|
|
|
+
|
|
|
// Vibration support (if available)
|
|
|
vibrate(gamepadIndex, duration = 200, strongMagnitude = 1.0, weakMagnitude = 1.0) {
|
|
|
if (!this.config.enableVibration) return false;
|