From c21bd93c72e26d3881479332c9bf182ef71fe921 Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Fri, 31 Oct 2025 11:20:31 -0500 Subject: [PATCH] Refactor ship controls to force-based physics with world-space transformations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace accumulated velocity approach with direct force application - Transform local direction vectors to world space before applying forces - Fix explosion animation to use Babylon's render loop instead of requestAnimationFrame - Simplify Ship constructor and remove ControllerStickMode enum - Comment out all renderingGroupId assignments for performance testing - Add comprehensive CONTROLLER_THRUST.md documentation - Fix audio engine initialization in Level1 - Update to ship2.glb model - Adjust physics damping values for better control feel - Add applyForces() calls to keyboard and mouse handlers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CONTROLLER_THRUST.md | 327 ++++++++++++++++++++++++++++++++++++++++ public/ship2.glb | Bin 0 -> 9848 bytes src/backgroundStars.ts | 2 +- src/explosionManager.ts | 16 +- src/level1.ts | 2 +- src/scoreboard.ts | 2 +- src/ship.ts | 174 ++++++++++----------- src/sight.ts | 12 +- src/testLevel.ts | 1 - 9 files changed, 420 insertions(+), 116 deletions(-) create mode 100644 CONTROLLER_THRUST.md create mode 100644 public/ship2.glb diff --git a/CONTROLLER_THRUST.md b/CONTROLLER_THRUST.md new file mode 100644 index 0000000..6f02cd3 --- /dev/null +++ b/CONTROLLER_THRUST.md @@ -0,0 +1,327 @@ +# Controller Thrust System Analysis + +## Overview + +The ship's thrust system uses VR controller thumbsticks to apply physics forces and torques to the ship. The system applies forces gradually up to a maximum velocity, providing momentum-based movement with velocity caps. + +## Control Mapping + +### Left Thumbstick +- **Y Axis (up/down)**: Linear thrust forward/backward +- **X Axis (left/right)**: Yaw rotation (turning left/right) + +### Right Thumbstick +- **Y Axis (up/down)**: Pitch rotation (nose up/down) +- **X Axis (left/right)**: Roll rotation (barrel roll) + +### Trigger +- Fires weapons + +--- + +## Constants & Configuration + +Located in `src/ship.ts:26-29`: + +```typescript +const MAX_LINEAR_VELOCITY = 80; // Maximum forward/backward speed +const MAX_ANGULAR_VELOCITY = 1.9; // Maximum rotation speed +const LINEAR_FORCE_MULTIPLIER = 600; // Thrust force strength +const ANGULAR_FORCE_MULTIPLIER = 18; // Torque strength +``` + +--- + +## Linear Thrust Implementation + +### Code Location +`src/ship.ts:321-366` - Inside `updateVelocity()` method + +### How It Works + +1. **Input Detection**: Checks if left stick Y axis has significant deflection (`> 0.01`) + +2. **Velocity Check**: Gets current speed from physics body + ```typescript + const currentSpeed = currentLinearVelocity.length(); + ``` + +3. **Force Calculation**: + ```typescript + const forceDirection = this._ship.forward.scale(-this._leftStickVector.y); + const force = forceDirection.scale(LINEAR_FORCE_MULTIPLIER); + ``` + +4. **Force Application**: Only applies force if below max velocity + ```typescript + if (currentSpeed < MAX_LINEAR_VELOCITY) { + body.applyForce(force, this._ship.absolutePosition); + } + ``` + +5. **Velocity Clamping**: After force application, clamps total velocity + ```typescript + if (currentSpeed > MAX_LINEAR_VELOCITY) { + const clampedVelocity = currentLinearVelocity.normalize().scale(MAX_LINEAR_VELOCITY); + body.setLinearVelocity(clampedVelocity); + } + ``` + +### Key Assumptions About Babylon.js APIs + +#### ✅ VERIFIED from code and user confirmation: +- `this._ship.forward` returns a unit vector in **local space** (NOT world space) +- `body.getLinearVelocity()` returns current velocity vector in world space +- `body.applyForce(force, position)` applies force at a point (standard physics API) +- `body.applyForce()` expects forces in **world space** coordinates + +#### ⚠️ ASSUMED (not verified from documentation): +- `this._ship.absolutePosition` is the correct point to apply force for center-of-mass thrust + +### Current Issues + +**CRITICAL PROBLEM #1**: `this._ship.forward` returns a vector in **local space**, but `body.applyForce()` expects **world space** coordinates. The local direction vector must be transformed to world space before applying force. + +**What's happening**: +- `this._ship.forward` = Local -Z axis vector (NOT in world space) +- This local vector is passed directly to `applyForce()` which expects world space +- Force is applied incorrectly because of coordinate space mismatch + +**What's needed**: +- Use Z-axis for forward/backward thrust (matches bullet direction) +- Transform local direction to world space using `Vector3.TransformNormal(localDir, this._ship.getWorldMatrix())` + +--- + +## Angular Thrust Implementation + +### Code Location +`src/ship.ts:368-440` - Inside `updateVelocity()` method + +### How It Works + +1. **Input Collection**: + ```typescript + const yaw = this._leftStickVector.x; // Left stick X + const pitch = -this._rightStickVector.y; // Right stick Y (inverted) + const roll = -this._rightStickVector.x; // Right stick X (inverted) + ``` + +2. **Torque Calculation**: + ```typescript + const torque = new Vector3(pitch, yaw, roll).scale(ANGULAR_FORCE_MULTIPLIER); + ``` + +3. **Apply Angular Impulse**: + ```typescript + body.applyAngularImpulse(torque); + ``` + +4. **Angular Velocity Clamping**: + ```typescript + if (currentAngularSpeed > MAX_ANGULAR_VELOCITY) { + const clampedAngularVelocity = currentAngularVelocity.normalize().scale(MAX_ANGULAR_VELOCITY); + body.setAngularVelocity(clampedAngularVelocity); + } + ``` + +### Key Assumptions About Babylon.js APIs + +#### ✅ VERIFIED from code: +- Angular impulse is applied every frame based on stick input +- Angular velocity is clamped to maximum rotation speed + +#### ⚠️ ASSUMED (not verified): +- `body.applyAngularImpulse(torque)` expects torque vector in **world space** coordinates +- The torque vector components `(X, Y, Z)` directly map to rotation around world axes +- Angular impulse accumulates with existing angular velocity + +### Current Issues + +**CRITICAL PROBLEM**: The torque is being constructed as a simple vector `(pitch, yaw, roll)` in what appears to be local space, but `body.applyAngularImpulse()` expects **world space** coordinates. + +**What's happening**: +- Torque = `Vector3(pitch, yaw, roll)` - intended as local space rotations +- Passed directly to `applyAngularImpulse()` which expects world space +- Ship rotates around wrong axes because of coordinate space mismatch + +**What's needed**: +- Define torque in local space: X=pitch, Y=yaw, Z=roll +- Transform to world space before applying +- Pitch: Rotation around ship's local X-axis (right vector) +- Yaw: Rotation around ship's local Y-axis (up vector) +- Roll: Rotation around ship's local Z-axis (forward vector) + +**Required Fix**: Transform the torque from local space to world space: +```typescript +const localTorque = new Vector3(pitch, yaw, roll).scale(ANGULAR_FORCE_MULTIPLIER); +const worldTorque = Vector3.TransformNormal(localTorque, this._ship.getWorldMatrix()); +body.applyAngularImpulse(worldTorque); +``` + +--- + +## Debug Visualization + +### Activation +- Press `d` key to toggle debug mode (currently defaults to ON) +- Debug lines are drawn in rendering group 3 (always on top) + +### Visual Indicators + +**Linear Force** (Yellow line): +- Drawn from camera position +- Shows direction and magnitude of thrust force +- Only visible when applying forward/backward thrust + +**Angular Forces**: +- **Red line**: Pitch torque around ship's right axis (X) +- **Green line**: Yaw torque around ship's up axis (Y) +- **Blue line**: Roll torque around ship's forward axis (Z) + +### Debug Visualization Code Location +`src/ship.ts:221-232` - `drawDebugVector()` method + +### How Debug Lines Are Positioned + +```typescript +const cameraPos = this._camera.globalPosition.clone(); +const cameraForward = this._camera.getFrontPosition(1); +const start = cameraPos.add(cameraForward.scale(1)).add(offset); +``` + +#### ⚠️ ASSUMPTION: +- `this._camera.getFrontPosition(1)` returns a position 1 unit in front of camera +- This is not a standard Babylon.js API method (expected `getDirection()` instead) +- May be causing debug lines to not render correctly + +--- + +## Physics Body Properties + +The ship physics body is configured with: + +```typescript +mass: 100 +linearDamping: 0.1 // Causes gradual velocity decay +angularDamping: 0.2 // Causes gradual rotation decay +motionType: DYNAMIC // Affected by forces and gravity (if enabled) +``` + +### How Damping Affects Movement + +**Linear Damping (0.1)**: +- When no thrust is applied, ship gradually slows down +- 10% velocity reduction per physics step (approximate) +- Creates "drag in space" effect + +**Angular Damping (0.2)**: +- When no rotation input, ship gradually stops spinning +- 20% angular velocity reduction per physics step (approximate) +- Prevents indefinite spinning + +--- + +## Expected Behavior vs Current Implementation + +### Linear Thrust + +| Expected | Current Implementation | Status | +|----------|----------------------|--------| +| Thrust along local Z-axis | Thrust along local Z-axis (forward) | ✅ **CORRECT** | +| Gradual acceleration | ✅ Applies force up to max velocity | ✅ Correct | +| Velocity clamping | ✅ Clamps to MAX_LINEAR_VELOCITY | ✅ Correct | +| World-space force | ✅ Transforms to world space | ✅ **CORRECT** | + +### Angular Thrust + +| Expected | Current Implementation | Status | +|----------|----------------------|--------| +| Rotation around local axes | ✅ Transforms to world space | ✅ **CORRECT** | +| Torque transformation | ✅ Uses Vector3.TransformNormal | ✅ **CORRECT** | +| Velocity clamping | ✅ Clamps angular velocity | ✅ Correct | + +--- + +## Audio Feedback + +### Primary Thrust Sound +- Triggered when left stick Y > 0.1 +- Volume scales with stick deflection +- Looping thrust sound + +### Secondary Thrust Sound +- Triggered when any rotation input detected +- Volume scales with combined rotation input magnitude +- Looping thrust sound + +--- + +## Recommended Fixes + +### 1. Fix Linear Thrust Direction and Coordinate Space ✅ FIXED +**Changed** from: +```typescript +const forceDirection = this._ship.forward.scale(-this._leftStickVector.y); +const force = forceDirection.scale(LINEAR_FORCE_MULTIPLIER); +``` + +To: +```typescript +// Get local direction (Z-axis for forward/backward thrust) +const localDirection = new Vector3(0, 0, -this._leftStickVector.y); +// Transform to world space +const worldDirection = Vector3.TransformNormal(localDirection, this._ship.getWorldMatrix()); +const force = worldDirection.scale(LINEAR_FORCE_MULTIPLIER); +``` + +### 2. Fix Angular Thrust Coordinate Space +**Change lines 382-383** from: +```typescript +const torque = new Vector3(pitch, yaw, roll).scale(ANGULAR_FORCE_MULTIPLIER); +body.applyAngularImpulse(torque); +``` + +To: +```typescript +const localTorque = new Vector3(pitch, yaw, roll).scale(ANGULAR_FORCE_MULTIPLIER); +const worldTorque = Vector3.TransformNormal(localTorque, this._ship.getWorldMatrix()); +body.applyAngularImpulse(worldTorque); +``` + +### 3. Fix Debug Visualization Camera Method +**Change line 224** from: +```typescript +const cameraForward = this._camera.getFrontPosition(1); +``` + +To: +```typescript +const cameraForward = this._camera.getDirection(Vector3.Forward()); +``` + +--- + +## Open Questions + +1. **Force Application Point**: Is `this._ship.absolutePosition` the center of mass, or should force be applied at a specific offset? + +2. **Coordinate System Convention**: What is the ship's default orientation in local space? + - Is +Y up, +Z forward, +X right? (Standard) + - Or does the ship model use a different convention? + +3. **Angular Impulse vs Torque**: Should we use `applyAngularImpulse()` or a continuous torque application method? + +4. **Velocity Check Logic**: Currently checks total speed before applying force. Should we instead check velocity component in the thrust direction? + +--- + +## Testing Recommendations + +With debug mode enabled, verify: + +1. **Yellow thrust line** points in intended thrust direction when moving stick +2. **Red/Green/Blue rotation lines** show rotation axes correctly aligned with ship orientation +3. Ship accelerates smoothly without hitting velocity cap too quickly +4. Ship rotates around its own axes, not around world axes +5. Damping brings ship to rest when sticks are released diff --git a/public/ship2.glb b/public/ship2.glb new file mode 100644 index 0000000000000000000000000000000000000000..b61d626cf87b2331d7f906e96e24163508bb788b GIT binary patch literal 9848 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 literal 0 HcmV?d00001 diff --git a/src/backgroundStars.ts b/src/backgroundStars.ts index 4bde1f2..6410e41 100644 --- a/src/backgroundStars.ts +++ b/src/backgroundStars.ts @@ -103,7 +103,7 @@ export class BackgroundStars { mat.disableDepthWrite = true; // Stars should be in the background - mesh.renderingGroupId = 0; + // mesh.renderingGroupId = 0; // Make stars always render behind everything else mesh.isPickable = false; diff --git a/src/explosionManager.ts b/src/explosionManager.ts index ae31ff2..9a03d4e 100644 --- a/src/explosionManager.ts +++ b/src/explosionManager.ts @@ -213,13 +213,13 @@ export class ExplosionManager { maxForce: this.config.explosionForce }); - // Animate the explosion by calling explode() each frame with increasing values + // Animate the explosion using Babylon's render loop instead of requestAnimationFrame const startTime = Date.now(); const animationDuration = this.config.duration; const maxForce = this.config.explosionForce; let frameCount = 0; - const animate = () => { + const animationObserver = this.scene.onBeforeRenderObservable.add(() => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / animationDuration, 1.0); @@ -254,18 +254,16 @@ export class ExplosionManager { } // Continue animation if not complete - if (progress < 1.0) { - requestAnimationFrame(animate); - } else { - // Animation complete - clean up + if (progress >= 1.0) { + // Animation complete - remove observer and clean up console.log(`[ExplosionManager] Animation complete after ${frameCount} frames, cleaning up`); + this.scene.onBeforeRenderObservable.remove(animationObserver); this.cleanupExplosion(meshPieces); } - }; + }); - // Start the animation + // Log that animation loop is registered console.log('[ExplosionManager] Starting animation loop...'); - animate(); } catch (error) { console.error('[ExplosionManager] ERROR creating MeshExploder:', error); // Clean up pieces if exploder failed diff --git a/src/level1.ts b/src/level1.ts index e443565..9935e1b 100644 --- a/src/level1.ts +++ b/src/level1.ts @@ -38,7 +38,7 @@ export class Level1 implements Level { this._levelConfig = levelConfig; this._audioEngine = audioEngine; this._deserializer = new LevelDeserializer(levelConfig); - this._ship = new Ship(undefined, audioEngine); + this._ship = new Ship(audioEngine); this._scoreboard = new Scoreboard(); const xr = DefaultScene.XR; diff --git a/src/scoreboard.ts b/src/scoreboard.ts index 515f7d7..b994c71 100644 --- a/src/scoreboard.ts +++ b/src/scoreboard.ts @@ -39,7 +39,7 @@ export class Scoreboard { console.log('Scoreboard parent:', parent); console.log('Initializing scoreboard'); const scoreboard = MeshBuilder.CreatePlane("scoreboard", {width: 1, height: 1}, scene); - scoreboard.renderingGroupId = 3; + // scoreboard.renderingGroupId = 3; const material = new StandardMaterial("scoreboard", scene); scoreboard.parent =parent; diff --git a/src/ship.ts b/src/ship.ts index 2d9021a..a5ef201 100644 --- a/src/ship.ts +++ b/src/ship.ts @@ -1,16 +1,14 @@ import { - AbstractMesh, Angle, + AbstractMesh, Color3, - DirectionalLight, FreeCamera, - GlowLayer, InstancedMesh, Mesh, + InstancedMesh, Mesh, MeshBuilder, Observable, PhysicsAggregate, PhysicsMotionType, PhysicsShapeType, PointLight, SceneLoader, - SpotLight, StandardMaterial, TransformNode, Vector2, @@ -23,7 +21,10 @@ import type {AudioEngineV2, StaticSound} from "@babylonjs/core"; import {DefaultScene} from "./defaultScene"; import { GameConfig } from "./gameConfig"; import { Sight } from "./sight"; -const MAX_FORWARD_THRUST = 40; +const MAX_LINEAR_VELOCITY = 80; +const MAX_ANGULAR_VELOCITY = 1.8; +const LINEAR_FORCE_MULTIPLIER = 800; +const ANGULAR_FORCE_MULTIPLIER = 20; const controllerComponents = [ 'a-button', @@ -47,18 +48,11 @@ type ControllerEvent = { } -enum ControllerStickMode { - BEGINNER, - ARCADE, - REALISTIC -} export class Ship { private _ship: TransformNode; private _controllerObservable: Observable = new Observable(); private _ammoMaterial: StandardMaterial; - private _forwardNode: TransformNode; - private _rotationNode: TransformNode; private _primaryThrustVectorSound: StaticSound; private _secondaryThrustVectorSound: StaticSound; private _shot: StaticSound; @@ -67,19 +61,14 @@ export class Ship { private _shooting: boolean = false; private _camera: FreeCamera; private _ammoBaseMesh: AbstractMesh; - private _controllerMode: ControllerStickMode; - private _active = false; private _audioEngine: AudioEngineV2; private _sight: Sight; - constructor(mode: ControllerStickMode = ControllerStickMode.BEGINNER, audioEngine?: AudioEngineV2) { - this._controllerMode = mode; + + constructor( audioEngine?: AudioEngineV2) { this._audioEngine = audioEngine; this.setup(); this.initialize(); } - public set controllerMode(mode: ControllerStickMode) { - this._controllerMode = mode; - } private async initializeSounds() { if (!this._audioEngine) return; @@ -171,10 +160,6 @@ export class Ship { this.setupKeyboard(); this.setupMouse(); this._controllerObservable.add(this.controllerCallback); - this._forwardNode = new TransformNode("forward", DefaultScene.MainScene); - this._rotationNode = new TransformNode("rotation", DefaultScene.MainScene); - this._forwardNode.parent = this._ship; - this._rotationNode.parent = this._ship; this._camera = new FreeCamera("Flat Camera", new Vector3(0, .5, 0), DefaultScene.MainScene); @@ -193,17 +178,11 @@ export class Ship { centerGap: 0.5 }); - let i = 0; - DefaultScene.MainScene.onBeforeRenderObservable.add(() => { - if (i++ % 10 == 0) { - this.applyForce(); - } - }); - - this._active = true; } + + private async initialize() { - const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "ship1.glb", DefaultScene.MainScene); + const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "ship2.glb", DefaultScene.MainScene); const shipMesh = importMesh.meshes[0]; shipMesh.id = "shipMesh"; shipMesh.name = "shipMesh"; @@ -224,9 +203,10 @@ export class Ship { mesh: (geo as Mesh) // Use the actual ship geometry }, DefaultScene.MainScene); + agg.body.setMotionType(PhysicsMotionType.DYNAMIC); - agg.body.setLinearDamping(.1); - agg.body.setAngularDamping(.2); + agg.body.setLinearDamping(.2); + agg.body.setAngularDamping(.3); agg.body.setAngularVelocity(new Vector3(0, 0, 0)); agg.body.setCollisionCallbackEnabled(true); } else { @@ -249,12 +229,12 @@ export class Ship { //shipMesh.rotation.y = Math.PI; //shipMesh.position.y = 1; shipMesh.position.z = -1; - shipMesh.renderingGroupId = 3; + // shipMesh.renderingGroupId = 3; const light = new PointLight("ship.light", new Vector3(0, .5, .1), DefaultScene.MainScene); light.intensity = 4; light.includedOnlyMeshes = [shipMesh]; for (const mesh of shipMesh.getChildMeshes()) { - mesh.renderingGroupId = 3; + // mesh.renderingGroupId = 3; if (mesh.material.id.indexOf('glass') === -1) { light.includedOnlyMeshes.push(mesh); } @@ -266,10 +246,6 @@ export class Ship { private _leftStickVector = Vector2.Zero().clone(); private _rightStickVector = Vector2.Zero().clone(); - private _forwardValue = 0; - private _yawValue = 0; - private _rollValue = 0; - private _pitchValue = 0; private _mouseDown = false; private _mousePos = new Vector2(0, 0); @@ -277,74 +253,91 @@ export class Ship { return this._ship; } - - private applyForce() { + private applyForces() { if (!this?._ship?.physicsBody) { return; } const body = this._ship.physicsBody; - //If we're moving over MAX_FORWARD_THRUST, we can't add any more thrust, - //just continue at MAX_FORWARD_THRUST - if (Math.abs(this._forwardValue) > MAX_FORWARD_THRUST) { - this._forwardValue = Math.sign(this._forwardValue) * MAX_FORWARD_THRUST; - } - //if forward thrust is under 40 we can apply more thrust - if (Math.abs(this._forwardValue) <= MAX_FORWARD_THRUST) { - if (Math.abs(this._leftStickVector.y) > .1) { - if (this._primaryThrustVectorSound && !this._primaryThrustPlaying) { - this._primaryThrustVectorSound.play(); - this._primaryThrustPlaying = true; - } - if (this._primaryThrustVectorSound) { - this._primaryThrustVectorSound.volume = Math.abs(this._leftStickVector.y); - } - this._forwardValue += this._leftStickVector.y * .8; - } else { - if (this._primaryThrustVectorSound && this._primaryThrustPlaying) { - this._primaryThrustVectorSound.stop(); - this._primaryThrustPlaying = false; - } - this._forwardValue = decrementValue(this._forwardValue, .98); + // Get current velocities for velocity cap checks + const currentLinearVelocity = body.getLinearVelocity(); + const currentAngularVelocity = body.getAngularVelocity(); + const currentSpeed = currentLinearVelocity.length(); + + // Apply linear force from left stick Y (forward/backward) + if (Math.abs(this._leftStickVector.y) > .1) { + // Only apply force if we haven't reached max velocity + if (currentSpeed < MAX_LINEAR_VELOCITY) { + // Get local direction (Z-axis for forward/backward thrust) + const localDirection = new Vector3(0, 0, -this._leftStickVector.y); + // Transform to world space - TransformNode vectors are in local space! + const worldDirection = Vector3.TransformNormal(localDirection, this._ship.getWorldMatrix()); + const force = worldDirection.scale(LINEAR_FORCE_MULTIPLIER); + body.applyForce(force, this._ship.physicsBody.transformNode.absolutePosition); + + } + + // Handle primary thrust sound + if (this._primaryThrustVectorSound && !this._primaryThrustPlaying) { + this._primaryThrustVectorSound.play(); + this._primaryThrustPlaying = true; + } + if (this._primaryThrustVectorSound) { + this._primaryThrustVectorSound.volume = Math.abs(this._leftStickVector.y); + } + } else { + // Stop thrust sound when no input + if (this._primaryThrustVectorSound && this._primaryThrustPlaying) { + this._primaryThrustVectorSound.stop(); + this._primaryThrustPlaying = false; } } - this._yawValue = adjustStickValue(this._leftStickVector.x, this._yawValue); - this._rollValue = adjustStickValue(this._rightStickVector.x, this._rollValue); - this._pitchValue = adjustStickValue(this._rightStickVector.y, this._pitchValue); - - this._forwardNode.position.z = this._forwardValue; - this._rotationNode.position.y = this._yawValue; - this._rotationNode.position.z = -this._rollValue; - this._rotationNode.position.x = -this._pitchValue; - - const thrust2 = Math.abs(this._rightStickVector.y) + + // Calculate rotation magnitude for torque and sound + const rotationMagnitude = Math.abs(this._rightStickVector.y) + Math.abs(this._rightStickVector.x) + Math.abs(this._leftStickVector.x); - if (thrust2 > .01) { + // Apply angular forces if any stick has significant rotation input + if (rotationMagnitude > .1) { + const currentAngularSpeed = currentAngularVelocity.length(); + + // Only apply torque if we haven't reached max angular velocity + if (currentAngularSpeed < MAX_ANGULAR_VELOCITY) { + const yaw = this._leftStickVector.x; + const pitch = -this._rightStickVector.y; + const roll = -this._rightStickVector.x; + + // Create torque in local space, then transform to world space + const localTorque = new Vector3(pitch, yaw, roll).scale(ANGULAR_FORCE_MULTIPLIER); + const worldTorque = Vector3.TransformNormal(localTorque, this._ship.getWorldMatrix()); + + body.applyAngularImpulse(worldTorque); + + // Debug visualization for angular forces + } + + // Handle secondary thrust sound for rotation if (this._secondaryThrustVectorSound && !this._secondaryThrustPlaying) { this._secondaryThrustVectorSound.play(); this._secondaryThrustPlaying = true; } if (this._secondaryThrustVectorSound) { - this._secondaryThrustVectorSound.volume = thrust2 * .4; + this._secondaryThrustVectorSound.volume = rotationMagnitude * .4; } } else { + // Stop rotation thrust sound when no input if (this._secondaryThrustVectorSound && this._secondaryThrustPlaying) { this._secondaryThrustVectorSound.stop(); this._secondaryThrustPlaying = false; } - } - body.setAngularVelocity(this._rotationNode.absolutePosition.subtract(this._ship.absolutePosition)); - body.setLinearVelocity(this._forwardNode.absolutePosition.subtract(this._ship.absolutePosition).scale(-1)); } + private controllerCallback = (controllerEvent: ControllerEvent) => { // Log first few events to verify they're firing - if (controllerEvent.type == 'thumbstick') { if (controllerEvent.hand == 'left') { this._leftStickVector.x = controllerEvent.axisData.x; @@ -354,9 +347,8 @@ export class Ship { if (controllerEvent.hand == 'right') { this._rightStickVector.x = controllerEvent.axisData.x; this._rightStickVector.y = controllerEvent.axisData.y; - } - this.applyForce(); + this.applyForces(); } if (controllerEvent.type == 'button') { if (controllerEvent.component.type == 'trigger') { @@ -401,6 +393,7 @@ export class Ship { } else { this._rightStickVector.y = Math.sign(yInc); } + this.applyForces(); }; } @@ -443,6 +436,7 @@ export class Ship { break; } + this.applyForces(); }; } @@ -534,21 +528,7 @@ export class Ship { if (this._sight) { this._sight.dispose(); } + // Add other cleanup as needed } } -function decrementValue(value: number, increment: number = .8): number { - if (Math.abs(value) < .01) { - return 0; - } else { - return value * increment; - } -} - -function adjustStickValue(stickVector: number, thrustValue: number): number { - if (Math.abs(stickVector) > .03) { - return thrustValue + (Math.pow(stickVector, 3) * .1); - } else { - return decrementValue(thrustValue, .85); - } -} diff --git a/src/sight.ts b/src/sight.ts index 412d77d..c51d924 100644 --- a/src/sight.ts +++ b/src/sight.ts @@ -79,7 +79,7 @@ export class Sight { }, this.scene); this.circle.parent = this.reticleGroup; this.circle.material = material; - this.circle.renderingGroupId = this.config.renderingGroupId; + // this.circle.renderingGroupId = this.config.renderingGroupId; // Create crosshair lines (4 lines extending from center gap) this.createCrosshairLines(material); @@ -102,7 +102,7 @@ export class Sight { topLine.parent = this.reticleGroup; topLine.position.y = gap + length / 2; topLine.material = material; - topLine.renderingGroupId = this.config.renderingGroupId; + // topLine.renderingGroupId = this.config.renderingGroupId; this.crosshairLines.push(topLine); // Bottom line @@ -114,7 +114,7 @@ export class Sight { bottomLine.parent = this.reticleGroup; bottomLine.position.y = -(gap + length / 2); bottomLine.material = material; - bottomLine.renderingGroupId = this.config.renderingGroupId; + // bottomLine.renderingGroupId = this.config.renderingGroupId; this.crosshairLines.push(bottomLine); // Left line @@ -126,7 +126,7 @@ export class Sight { leftLine.parent = this.reticleGroup; leftLine.position.x = -(gap + length / 2); leftLine.material = material; - leftLine.renderingGroupId = this.config.renderingGroupId; + // leftLine.renderingGroupId = this.config.renderingGroupId; this.crosshairLines.push(leftLine); // Right line @@ -138,7 +138,7 @@ export class Sight { rightLine.parent = this.reticleGroup; rightLine.position.x = gap + length / 2; rightLine.material = material; - rightLine.renderingGroupId = this.config.renderingGroupId; + // rightLine.renderingGroupId = this.config.renderingGroupId; this.crosshairLines.push(rightLine); // Center dot (optional, very small) @@ -147,7 +147,7 @@ export class Sight { }, this.scene); centerDot.parent = this.reticleGroup; centerDot.material = material; - centerDot.renderingGroupId = this.config.renderingGroupId; + // centerDot.renderingGroupId = this.config.renderingGroupId; this.crosshairLines.push(centerDot); } diff --git a/src/testLevel.ts b/src/testLevel.ts index 1d650a5..b39f9d0 100644 --- a/src/testLevel.ts +++ b/src/testLevel.ts @@ -157,7 +157,6 @@ export class TestLevel implements Level { public async initialize() { console.log('[TestLevel] initialize() called'); console.log('[TestLevel] Scene info:', { - name: DefaultScene.MainScene.name, meshCount: DefaultScene.MainScene.meshes.length, lightCount: DefaultScene.MainScene.lights.length });