Fix physics issues: sleep behavior, center of mass, and input scaling
This commit resolves several physics-related issues that were causing unexpected behavior in ship and asteroid movement: **Physics Sleep System** - Fixed abrupt stops by preventing Havok from putting bodies to sleep - Added PhysicsActivationControl.ALWAYS_ACTIVE for ship and asteroids - Made ship sleep behavior configurable via shipPhysics.alwaysActive - Sleep was causing sudden velocity zeroing at low speeds **Center of Mass Issues** - Discovered mesh-based physics calculated offset CoM: (0, -0.38, 0.37) - Override ship center of mass to (0, 0, 0) to prevent thrust torque - Applying force at offset CoM was creating unwanted pitch rotation - Added debug logging to track mass properties **Input Deadzone Improvements** - Implemented smooth deadzone scaling (0.1-0.15 range) - Replaced hard threshold cliff with linear interpolation - Prevents abrupt control cutoff during gentle inputs - Added VR mode check to disable keyboard fallback in VR **Configuration System** - Added DEFAULT_SHIP_PHYSICS constant as single source of truth - Added tunable parameters: linearDamping, angularDamping, alwaysActive - Added fuel consumption rates: linearFuelConsumptionRate, angularFuelConsumptionRate - Tuned for 1 minute linear thrust, 2 minutes angular thrust at 60Hz - All physics parameters now persist to localStorage **Other Fixes** - Changed orbit center to STATIC motion type (was ANIMATED) - Fixed linear force application point (removed offset) - Added ship initial velocity support from level config - Changed physics update from every 10 frames to every physics tick - Increased linear input threshold from 0.1 to 0.15 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
71ff46e4cf
commit
e31e25f9e5
9
.claude/mcp.json
Normal file
9
.claude/mcp.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"babylon-mcp": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["mcp-proxy", "http://localhost:4000/mcp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0",
|
"version": "1.1",
|
||||||
"difficulty": "recruit",
|
"difficulty": "recruit",
|
||||||
"timestamp": "2025-11-11T23:44:24.807Z",
|
"timestamp": "2025-11-11T23:44:24.807Z",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@ -11,8 +11,8 @@
|
|||||||
"ship": {
|
"ship": {
|
||||||
"position": [
|
"position": [
|
||||||
0,
|
0,
|
||||||
1.5,
|
2,
|
||||||
500
|
0
|
||||||
],
|
],
|
||||||
"rotation": [
|
"rotation": [
|
||||||
0,
|
0,
|
||||||
|
|||||||
@ -1,3 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Default ship physics configuration
|
||||||
|
*/
|
||||||
|
const DEFAULT_SHIP_PHYSICS = {
|
||||||
|
maxLinearVelocity: 200,
|
||||||
|
maxAngularVelocity: 1.4,
|
||||||
|
linearForceMultiplier: 100,
|
||||||
|
angularForceMultiplier: 1.5,
|
||||||
|
linearFuelConsumptionRate: 0.00002778, // 1 minute at full thrust (60 Hz)
|
||||||
|
angularFuelConsumptionRate: 0.0001389, // 2 minutes at full thrust (60 Hz)
|
||||||
|
linearDamping: 0.2,
|
||||||
|
angularDamping: 0.3, // Moderate damping for 2-3 second coast
|
||||||
|
alwaysActive: true // Prevent physics sleep (false may cause abrupt stops at zero velocity)
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global game configuration settings
|
* Global game configuration settings
|
||||||
* Singleton class for managing game-wide settings
|
* Singleton class for managing game-wide settings
|
||||||
@ -13,12 +28,7 @@ export class GameConfig {
|
|||||||
public progressionEnabled: boolean = true; // Enable level progression system
|
public progressionEnabled: boolean = true; // Enable level progression system
|
||||||
|
|
||||||
// Ship physics tuning parameters
|
// Ship physics tuning parameters
|
||||||
public shipPhysics = {
|
public shipPhysics = { ...DEFAULT_SHIP_PHYSICS };
|
||||||
maxLinearVelocity: 200,
|
|
||||||
maxAngularVelocity: 1.4,
|
|
||||||
linearForceMultiplier: 800,
|
|
||||||
angularForceMultiplier: 15
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private constructor for singleton pattern
|
* Private constructor for singleton pattern
|
||||||
@ -66,10 +76,15 @@ export class GameConfig {
|
|||||||
// Load ship physics with fallback to defaults
|
// Load ship physics with fallback to defaults
|
||||||
if (config.shipPhysics) {
|
if (config.shipPhysics) {
|
||||||
this.shipPhysics = {
|
this.shipPhysics = {
|
||||||
maxLinearVelocity: config.shipPhysics.maxLinearVelocity ?? 200,
|
maxLinearVelocity: config.shipPhysics.maxLinearVelocity ?? DEFAULT_SHIP_PHYSICS.maxLinearVelocity,
|
||||||
maxAngularVelocity: config.shipPhysics.maxAngularVelocity ?? 1.4,
|
maxAngularVelocity: config.shipPhysics.maxAngularVelocity ?? DEFAULT_SHIP_PHYSICS.maxAngularVelocity,
|
||||||
linearForceMultiplier: config.shipPhysics.linearForceMultiplier ?? 800,
|
linearForceMultiplier: config.shipPhysics.linearForceMultiplier ?? DEFAULT_SHIP_PHYSICS.linearForceMultiplier,
|
||||||
angularForceMultiplier: config.shipPhysics.angularForceMultiplier ?? 15
|
angularForceMultiplier: config.shipPhysics.angularForceMultiplier ?? DEFAULT_SHIP_PHYSICS.angularForceMultiplier,
|
||||||
|
linearFuelConsumptionRate: config.shipPhysics.linearFuelConsumptionRate ?? DEFAULT_SHIP_PHYSICS.linearFuelConsumptionRate,
|
||||||
|
angularFuelConsumptionRate: config.shipPhysics.angularFuelConsumptionRate ?? DEFAULT_SHIP_PHYSICS.angularFuelConsumptionRate,
|
||||||
|
linearDamping: config.shipPhysics.linearDamping ?? DEFAULT_SHIP_PHYSICS.linearDamping,
|
||||||
|
angularDamping: config.shipPhysics.angularDamping ?? DEFAULT_SHIP_PHYSICS.angularDamping,
|
||||||
|
alwaysActive: config.shipPhysics.alwaysActive ?? DEFAULT_SHIP_PHYSICS.alwaysActive,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -87,12 +102,7 @@ export class GameConfig {
|
|||||||
this.physicsEnabled = true;
|
this.physicsEnabled = true;
|
||||||
this.debug = false;
|
this.debug = false;
|
||||||
this.progressionEnabled = true;
|
this.progressionEnabled = true;
|
||||||
this.shipPhysics = {
|
this.shipPhysics = { ...DEFAULT_SHIP_PHYSICS };
|
||||||
maxLinearVelocity: 200,
|
|
||||||
maxAngularVelocity: 1.4,
|
|
||||||
linearForceMultiplier: 800,
|
|
||||||
angularForceMultiplier: 15
|
|
||||||
};
|
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,11 @@ import {
|
|||||||
AbstractMesh,
|
AbstractMesh,
|
||||||
AudioEngineV2,
|
AudioEngineV2,
|
||||||
DistanceConstraint,
|
DistanceConstraint,
|
||||||
|
HavokPlugin,
|
||||||
InstancedMesh,
|
InstancedMesh,
|
||||||
Mesh,
|
Mesh,
|
||||||
Observable,
|
Observable,
|
||||||
|
PhysicsActivationControl,
|
||||||
PhysicsAggregate,
|
PhysicsAggregate,
|
||||||
PhysicsBody,
|
PhysicsBody,
|
||||||
PhysicsMotionType,
|
PhysicsMotionType,
|
||||||
@ -45,8 +47,8 @@ export class RockFactory {
|
|||||||
// Initialize explosion manager
|
// Initialize explosion manager
|
||||||
const node = new TransformNode('orbitCenter', DefaultScene.MainScene);
|
const node = new TransformNode('orbitCenter', DefaultScene.MainScene);
|
||||||
node.position = Vector3.Zero();
|
node.position = Vector3.Zero();
|
||||||
this._orbitCenter = new PhysicsAggregate(node, PhysicsShapeType.SPHERE, {radius: .1, mass: 1000}, DefaultScene.MainScene );
|
this._orbitCenter = new PhysicsAggregate(node, PhysicsShapeType.SPHERE, {radius: .1, mass: 0}, DefaultScene.MainScene );
|
||||||
this._orbitCenter.body.setMotionType(PhysicsMotionType.ANIMATED);
|
this._orbitCenter.body.setMotionType(PhysicsMotionType.STATIC);
|
||||||
this._explosionManager = new ExplosionManager(DefaultScene.MainScene, {
|
this._explosionManager = new ExplosionManager(DefaultScene.MainScene, {
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
explosionForce: 150.0,
|
explosionForce: 150.0,
|
||||||
@ -115,6 +117,12 @@ export class RockFactory {
|
|||||||
body.setMotionType(PhysicsMotionType.DYNAMIC);
|
body.setMotionType(PhysicsMotionType.DYNAMIC);
|
||||||
body.setCollisionCallbackEnabled(true);
|
body.setCollisionCallbackEnabled(true);
|
||||||
|
|
||||||
|
// Prevent asteroids from sleeping to ensure consistent physics simulation
|
||||||
|
const physicsPlugin = DefaultScene.MainScene.getPhysicsEngine()?.getPhysicsPlugin() as HavokPlugin;
|
||||||
|
if (physicsPlugin) {
|
||||||
|
physicsPlugin.setActivationControl(body, PhysicsActivationControl.ALWAYS_ACTIVE);
|
||||||
|
}
|
||||||
|
|
||||||
debugLog(`[RockFactory] Setting velocities for ${rock.name}:`);
|
debugLog(`[RockFactory] Setting velocities for ${rock.name}:`);
|
||||||
debugLog(`[RockFactory] Linear velocity input: ${linearVelocitry.toString()}`);
|
debugLog(`[RockFactory] Linear velocity input: ${linearVelocitry.toString()}`);
|
||||||
debugLog(`[RockFactory] Angular velocity input: ${angularVelocity.toString()}`);
|
debugLog(`[RockFactory] Angular velocity input: ${angularVelocity.toString()}`);
|
||||||
|
|||||||
@ -61,6 +61,12 @@ export class Level1 implements Level {
|
|||||||
const currPose = xr.baseExperience.camera.globalPosition.y;
|
const currPose = xr.baseExperience.camera.globalPosition.y;
|
||||||
xr.baseExperience.camera.position = new Vector3(0, 1.5, 0);
|
xr.baseExperience.camera.position = new Vector3(0, 1.5, 0);
|
||||||
|
|
||||||
|
// Disable keyboard input in VR mode to prevent interference
|
||||||
|
if (this._ship.keyboardInput) {
|
||||||
|
this._ship.keyboardInput.setEnabled(false);
|
||||||
|
debugLog('[Level1] Keyboard input disabled for VR mode');
|
||||||
|
}
|
||||||
|
|
||||||
// Track WebXR session start
|
// Track WebXR session start
|
||||||
try {
|
try {
|
||||||
const analytics = getAnalytics();
|
const analytics = getAnalytics();
|
||||||
@ -290,6 +296,22 @@ export class Level1 implements Level {
|
|||||||
await this._ship.initialize();
|
await this._ship.initialize();
|
||||||
setLoadingMessage("Loading level from configuration...");
|
setLoadingMessage("Loading level from configuration...");
|
||||||
|
|
||||||
|
// Apply ship configuration from level config
|
||||||
|
const shipConfig = this._deserializer.getShipConfig();
|
||||||
|
this._ship.position = new Vector3(...shipConfig.position);
|
||||||
|
|
||||||
|
if (shipConfig.linearVelocity) {
|
||||||
|
this._ship.setLinearVelocity(new Vector3(...shipConfig.linearVelocity));
|
||||||
|
} else {
|
||||||
|
this._ship.setLinearVelocity(Vector3.Zero());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shipConfig.angularVelocity) {
|
||||||
|
this._ship.setAngularVelocity(new Vector3(...shipConfig.angularVelocity));
|
||||||
|
} else {
|
||||||
|
this._ship.setAngularVelocity(Vector3.Zero());
|
||||||
|
}
|
||||||
|
|
||||||
// Use deserializer to create all entities from config
|
// Use deserializer to create all entities from config
|
||||||
const entities = await this._deserializer.deserialize(this._ship.scoreboard.onScoreObservable);
|
const entities = await this._deserializer.deserialize(this._ship.scoreboard.onScoreObservable);
|
||||||
|
|
||||||
|
|||||||
@ -538,6 +538,13 @@ export class Main {
|
|||||||
const inputManager = InputControlManager.getInstance();
|
const inputManager = InputControlManager.getInstance();
|
||||||
inputManager.registerPointerFeature(pointerFeature);
|
inputManager.registerPointerFeature(pointerFeature);
|
||||||
debugLog("Pointer selection feature registered with InputControlManager");
|
debugLog("Pointer selection feature registered with InputControlManager");
|
||||||
|
|
||||||
|
// Configure scene-wide picking predicate to only allow UI meshes
|
||||||
|
DefaultScene.MainScene.pointerMovePredicate = (mesh) => {
|
||||||
|
// Only allow picking meshes with metadata.uiPickable = true
|
||||||
|
return mesh.metadata?.uiPickable === true;
|
||||||
|
};
|
||||||
|
debugLog("Scene picking predicate configured for VR UI only");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide Discord widget when entering VR, show when exiting
|
// Hide Discord widget when entering VR, show when exiting
|
||||||
|
|||||||
@ -2,8 +2,10 @@ import {
|
|||||||
AbstractMesh,
|
AbstractMesh,
|
||||||
Color3,
|
Color3,
|
||||||
FreeCamera,
|
FreeCamera,
|
||||||
|
HavokPlugin,
|
||||||
Mesh,
|
Mesh,
|
||||||
Observable,
|
Observable,
|
||||||
|
PhysicsActivationControl,
|
||||||
PhysicsAggregate,
|
PhysicsAggregate,
|
||||||
PhysicsMotionType,
|
PhysicsMotionType,
|
||||||
PhysicsShapeType,
|
PhysicsShapeType,
|
||||||
@ -118,6 +120,18 @@ export class Ship {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setLinearVelocity(velocity: Vector3): void {
|
||||||
|
if (this._ship?.physicsBody) {
|
||||||
|
this._ship.physicsBody.setLinearVelocity(velocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAngularVelocity(velocity: Vector3): void {
|
||||||
|
if (this._ship?.physicsBody) {
|
||||||
|
this._ship.physicsBody.setAngularVelocity(velocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async initialize() {
|
public async initialize() {
|
||||||
this._scoreboard = new Scoreboard();
|
this._scoreboard = new Scoreboard();
|
||||||
this._scoreboard.setShip(this); // Pass ship reference for velocity reading
|
this._scoreboard.setShip(this); // Pass ship reference for velocity reading
|
||||||
@ -126,7 +140,7 @@ export class Ship {
|
|||||||
const data = await loadAsset("ship.glb");
|
const data = await loadAsset("ship.glb");
|
||||||
this._ship = data.container.transformNodes[0];
|
this._ship = data.container.transformNodes[0];
|
||||||
// this._ship.id = "Ship"; // Set ID so mission brief can find it
|
// this._ship.id = "Ship"; // Set ID so mission brief can find it
|
||||||
this._ship.position.y = 5;
|
// Position is now set from level config in Level1.initialize()
|
||||||
|
|
||||||
// Create physics if enabled
|
// Create physics if enabled
|
||||||
const config = GameConfig.getInstance();
|
const config = GameConfig.getInstance();
|
||||||
@ -144,11 +158,37 @@ export class Ship {
|
|||||||
);
|
);
|
||||||
|
|
||||||
agg.body.setMotionType(PhysicsMotionType.DYNAMIC);
|
agg.body.setMotionType(PhysicsMotionType.DYNAMIC);
|
||||||
agg.body.setLinearDamping(0.2);
|
agg.body.setLinearDamping(config.shipPhysics.linearDamping);
|
||||||
agg.body.setAngularDamping(0.4);
|
agg.body.setAngularDamping(config.shipPhysics.angularDamping);
|
||||||
agg.body.setAngularVelocity(new Vector3(0, 0, 0));
|
agg.body.setAngularVelocity(new Vector3(0, 0, 0));
|
||||||
agg.body.setCollisionCallbackEnabled(true);
|
agg.body.setCollisionCallbackEnabled(true);
|
||||||
|
|
||||||
|
// Debug: Log center of mass before override
|
||||||
|
const massProps = agg.body.getMassProperties();
|
||||||
|
console.log(`[Ship] Original center of mass (local): ${massProps.centerOfMass.toString()}`);
|
||||||
|
console.log(`[Ship] Mass: ${massProps.mass}`);
|
||||||
|
console.log(`[Ship] Inertia: ${massProps.inertia.toString()}`);
|
||||||
|
|
||||||
|
// Override center of mass to origin to prevent thrust from causing torque
|
||||||
|
// (mesh-based physics was calculating offset center of mass from geometry)
|
||||||
|
agg.body.setMassProperties({
|
||||||
|
mass: 10,
|
||||||
|
centerOfMass: new Vector3(0, 0, 0),
|
||||||
|
inertia: massProps.inertia,
|
||||||
|
inertiaOrientation: massProps.inertiaOrientation
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Ship] Center of mass overridden to: ${agg.body.getMassProperties().centerOfMass.toString()}`);
|
||||||
|
|
||||||
|
// Configure physics sleep behavior from config
|
||||||
|
// (disabling sleep prevents abrupt stops at zero linear velocity)
|
||||||
|
if (config.shipPhysics.alwaysActive) {
|
||||||
|
const physicsPlugin = DefaultScene.MainScene.getPhysicsEngine()?.getPhysicsPlugin() as HavokPlugin;
|
||||||
|
if (physicsPlugin) {
|
||||||
|
physicsPlugin.setActivationControl(agg.body, PhysicsActivationControl.ALWAYS_ACTIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register collision handler for energy-based hull damage
|
// Register collision handler for energy-based hull damage
|
||||||
const observable = agg.body.getCollisionObservable();
|
const observable = agg.body.getCollisionObservable();
|
||||||
observable.add((collisionEvent) => {
|
observable.add((collisionEvent) => {
|
||||||
@ -276,18 +316,14 @@ export class Ship {
|
|||||||
this._physics.setGameStats(this._gameStats);
|
this._physics.setGameStats(this._gameStats);
|
||||||
|
|
||||||
// Setup physics update loop (every 10 frames)
|
// Setup physics update loop (every 10 frames)
|
||||||
DefaultScene.MainScene.onAfterRenderObservable.add(() => {
|
DefaultScene.MainScene.onAfterPhysicsObservable.add(() => {
|
||||||
this._frameCount++;
|
|
||||||
if (this._frameCount >= 10) {
|
|
||||||
this._frameCount = 0;
|
|
||||||
this.updatePhysics();
|
this.updatePhysics();
|
||||||
}
|
})
|
||||||
|
DefaultScene.MainScene.onAfterRenderObservable.add(() => {
|
||||||
// Update voice audio system (checks for completed sounds and plays next in queue)
|
// Update voice audio system (checks for completed sounds and plays next in queue)
|
||||||
if (this._voiceAudio) {
|
if (this._voiceAudio) {
|
||||||
this._voiceAudio.update();
|
this._voiceAudio.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check game end conditions every frame (but only acts once)
|
// Check game end conditions every frame (but only acts once)
|
||||||
this.checkGameEndConditions();
|
this.checkGameEndConditions();
|
||||||
});
|
});
|
||||||
@ -473,6 +509,9 @@ export class Ship {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we're in VR mode
|
||||||
|
const inVRMode = DefaultScene.XR?.baseExperience?.state === 2; // WebXRState.IN_XR = 2
|
||||||
|
|
||||||
// Combine input from keyboard and controller
|
// Combine input from keyboard and controller
|
||||||
const keyboardState = this._keyboardInput?.getInputState() || {
|
const keyboardState = this._keyboardInput?.getInputState() || {
|
||||||
leftStick: Vector2.Zero(),
|
leftStick: Vector2.Zero(),
|
||||||
@ -483,16 +522,24 @@ export class Ship {
|
|||||||
rightStick: Vector2.Zero(),
|
rightStick: Vector2.Zero(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Merge inputs (controller takes priority if active)
|
// Merge inputs with smooth deadzone scaling (controller takes priority if active, keyboard disabled in VR)
|
||||||
|
// Deadzone: 0.1-0.15 range with linear scaling (avoids abrupt cliff effect)
|
||||||
|
const leftMagnitude = controllerState.leftStick.length();
|
||||||
|
const rightMagnitude = controllerState.rightStick.length();
|
||||||
|
|
||||||
|
// Scale factor: 0% at 0.1, 100% at 0.15, linear interpolation between
|
||||||
|
const leftScale = Math.max(0, Math.min(1, (leftMagnitude - 0.1) / 0.05));
|
||||||
|
const rightScale = Math.max(0, Math.min(1, (rightMagnitude - 0.1) / 0.05));
|
||||||
|
|
||||||
const combinedInput = {
|
const combinedInput = {
|
||||||
leftStick:
|
leftStick:
|
||||||
controllerState.leftStick.length() > 0.1
|
leftMagnitude > 0.1
|
||||||
? controllerState.leftStick
|
? controllerState.leftStick.scale(leftScale)
|
||||||
: keyboardState.leftStick,
|
: (inVRMode ? Vector2.Zero() : keyboardState.leftStick),
|
||||||
rightStick:
|
rightStick:
|
||||||
controllerState.rightStick.length() > 0.1
|
rightMagnitude > 0.1
|
||||||
? controllerState.rightStick
|
? controllerState.rightStick.scale(rightScale)
|
||||||
: keyboardState.rightStick,
|
: (inVRMode ? Vector2.Zero() : keyboardState.rightStick),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply forces and get magnitudes for audio
|
// Apply forces and get magnitudes for audio
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export class ShipPhysics {
|
|||||||
let angularMagnitude = 0;
|
let angularMagnitude = 0;
|
||||||
|
|
||||||
// Apply linear force from left stick Y (forward/backward)
|
// Apply linear force from left stick Y (forward/backward)
|
||||||
if (Math.abs(leftStick.y) > 0.1) {
|
if (Math.abs(leftStick.y) > 0.15) {
|
||||||
linearMagnitude = Math.abs(leftStick.y);
|
linearMagnitude = Math.abs(leftStick.y);
|
||||||
|
|
||||||
// Check if we have fuel before applying force
|
// Check if we have fuel before applying force
|
||||||
@ -79,16 +79,17 @@ export class ShipPhysics {
|
|||||||
);
|
);
|
||||||
const force = worldDirection.scale(this._config.linearForceMultiplier);
|
const force = worldDirection.scale(this._config.linearForceMultiplier);
|
||||||
|
|
||||||
// Calculate thrust point: center of mass + offset (0, 1, 0) in world space
|
// Apply force at center of mass to avoid unintended torque
|
||||||
|
// (applying at an offset point creates rotation, noticeable at zero linear velocity)
|
||||||
const thrustPoint = Vector3.TransformCoordinates(
|
const thrustPoint = Vector3.TransformCoordinates(
|
||||||
physicsBody.getMassProperties().centerOfMass.add(new Vector3(0, 1, 0)),
|
physicsBody.getMassProperties().centerOfMass,
|
||||||
transformNode.getWorldMatrix()
|
transformNode.getWorldMatrix()
|
||||||
);
|
);
|
||||||
|
|
||||||
physicsBody.applyForce(force, thrustPoint);
|
physicsBody.applyForce(force, thrustPoint);
|
||||||
|
|
||||||
// Consume fuel: normalized magnitude (0-1) * 0.005 per frame
|
// Consume fuel based on config rate (tuned for 1 minute at full thrust)
|
||||||
const fuelConsumption = linearMagnitude * 0.005;
|
const fuelConsumption = linearMagnitude * this._config.linearFuelConsumptionRate;
|
||||||
this._shipStatus.consumeFuel(fuelConsumption);
|
this._shipStatus.consumeFuel(fuelConsumption);
|
||||||
|
|
||||||
// Track fuel consumed for statistics
|
// Track fuel consumed for statistics
|
||||||
@ -126,11 +127,15 @@ export class ShipPhysics {
|
|||||||
transformNode.getWorldMatrix()
|
transformNode.getWorldMatrix()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Note: Havok only exposes angular impulse, not torque
|
||||||
|
// Babylon.js implements applyForce() as: impulse = force * timeStep
|
||||||
|
// We do the same for angular: scale torque by physics timestep (1/60)
|
||||||
|
// Since we call this every 10 frames, we accumulate 10 timesteps worth
|
||||||
physicsBody.applyAngularImpulse(worldTorque);
|
physicsBody.applyAngularImpulse(worldTorque);
|
||||||
|
|
||||||
// Consume fuel: normalized magnitude (0-3 max) / 3 * 0.005 per frame
|
// Consume fuel based on config rate (tuned for 2 minutes at full thrust)
|
||||||
const normalizedAngularMagnitude = Math.min(angularMagnitude / 3.0, 1.0);
|
const normalizedAngularMagnitude = Math.min(angularMagnitude / 3.0, 1.0);
|
||||||
const fuelConsumption = normalizedAngularMagnitude * 0.005;
|
const fuelConsumption = normalizedAngularMagnitude * this._config.angularFuelConsumptionRate;
|
||||||
this._shipStatus.consumeFuel(fuelConsumption);
|
this._shipStatus.consumeFuel(fuelConsumption);
|
||||||
|
|
||||||
// Track fuel consumed for statistics
|
// Track fuel consumed for statistics
|
||||||
|
|||||||
@ -46,6 +46,8 @@ export class MissionBrief {
|
|||||||
|
|
||||||
mesh.parent = ship;
|
mesh.parent = ship;
|
||||||
mesh.position = new Vector3(0,1,2.8);
|
mesh.position = new Vector3(0,1,2.8);
|
||||||
|
mesh.renderingGroupId = 3; // Same as status screen for consistent rendering
|
||||||
|
mesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
|
||||||
console.log('[MissionBrief] Mesh parented to ship at position:', mesh.position);
|
console.log('[MissionBrief] Mesh parented to ship at position:', mesh.position);
|
||||||
console.log('[MissionBrief] Mesh absolute position:', mesh.getAbsolutePosition());
|
console.log('[MissionBrief] Mesh absolute position:', mesh.getAbsolutePosition());
|
||||||
console.log('[MissionBrief] Mesh scaling:', mesh.scaling);
|
console.log('[MissionBrief] Mesh scaling:', mesh.scaling);
|
||||||
|
|||||||
@ -94,6 +94,7 @@ export class StatusScreen {
|
|||||||
this._screenMesh.position = new Vector3(0, 0, 2); // 2 meters forward in local space
|
this._screenMesh.position = new Vector3(0, 0, 2); // 2 meters forward in local space
|
||||||
//this._screenMesh.rotation.y = Math.PI; // Face backward (toward user)
|
//this._screenMesh.rotation.y = Math.PI; // Face backward (toward user)
|
||||||
this._screenMesh.renderingGroupId = 3; // Always render on top
|
this._screenMesh.renderingGroupId = 3; // Always render on top
|
||||||
|
this._screenMesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
|
||||||
|
|
||||||
// Create material
|
// Create material
|
||||||
const material = new StandardMaterial("statusScreenMaterial", this._scene);
|
const material = new StandardMaterial("statusScreenMaterial", this._scene);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user