Compare commits

..

No commits in common. "622e0a52596bb368f3e2f89489fb018b8e74537c" and "71ff46e4cf8d6771206ac36286848f45cbb1a389" have entirely different histories.

11 changed files with 49 additions and 172 deletions

View File

@ -1,9 +0,0 @@
{
"mcpServers": {
"babylon-mcp": {
"command": "npx",
"args": ["mcp-proxy", "http://localhost:4000/mcp"]
}
}
}

View File

@ -1,5 +1,5 @@
{ {
"version": "1.1", "version": "1.0",
"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,
2, 1.5,
0 500
], ],
"rotation": [ "rotation": [
0, 0,

View File

@ -1,19 +1,3 @@
/**
* Default ship physics configuration
*/
const DEFAULT_SHIP_PHYSICS = {
maxLinearVelocity: 200,
maxAngularVelocity: 1.4,
linearForceMultiplier: 500,
angularForceMultiplier: .5,
linearFuelConsumptionRate: 0.0002778, // 1 minute at full thrust (60 Hz)
angularFuelConsumptionRate: 0.0001389, // 2 minutes at full thrust (60 Hz)
linearDamping: 0.2,
angularDamping: 0.6, // Moderate damping for 2-3 second coast
alwaysActive: true, // Prevent physics sleep (false may cause abrupt stops at zero velocity)
reverseThrustFactor: 0.3 // Reverse thrust at 50% of forward thrust power
};
/** /**
* Global game configuration settings * Global game configuration settings
* Singleton class for managing game-wide settings * Singleton class for managing game-wide settings
@ -29,7 +13,12 @@ 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 = { ...DEFAULT_SHIP_PHYSICS }; public shipPhysics = {
maxLinearVelocity: 200,
maxAngularVelocity: 1.4,
linearForceMultiplier: 800,
angularForceMultiplier: 15
};
/** /**
* Private constructor for singleton pattern * Private constructor for singleton pattern
@ -77,16 +66,10 @@ 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 ?? DEFAULT_SHIP_PHYSICS.maxLinearVelocity, maxLinearVelocity: config.shipPhysics.maxLinearVelocity ?? 200,
maxAngularVelocity: config.shipPhysics.maxAngularVelocity ?? DEFAULT_SHIP_PHYSICS.maxAngularVelocity, maxAngularVelocity: config.shipPhysics.maxAngularVelocity ?? 1.4,
linearForceMultiplier: config.shipPhysics.linearForceMultiplier ?? DEFAULT_SHIP_PHYSICS.linearForceMultiplier, linearForceMultiplier: config.shipPhysics.linearForceMultiplier ?? 800,
angularForceMultiplier: config.shipPhysics.angularForceMultiplier ?? DEFAULT_SHIP_PHYSICS.angularForceMultiplier, angularForceMultiplier: config.shipPhysics.angularForceMultiplier ?? 15
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,
reverseThrustFactor: config.shipPhysics.reverseThrustFactor ?? DEFAULT_SHIP_PHYSICS.reverseThrustFactor,
}; };
} }
} else { } else {
@ -104,7 +87,12 @@ export class GameConfig {
this.physicsEnabled = true; this.physicsEnabled = true;
this.debug = false; this.debug = false;
this.progressionEnabled = true; this.progressionEnabled = true;
this.shipPhysics = { ...DEFAULT_SHIP_PHYSICS }; this.shipPhysics = {
maxLinearVelocity: 200,
maxAngularVelocity: 1.4,
linearForceMultiplier: 800,
angularForceMultiplier: 15
};
this.save(); this.save();
} }
} }

View File

@ -2,11 +2,9 @@ import {
AbstractMesh, AbstractMesh,
AudioEngineV2, AudioEngineV2,
DistanceConstraint, DistanceConstraint,
HavokPlugin,
InstancedMesh, InstancedMesh,
Mesh, Mesh,
Observable, Observable,
PhysicsActivationControl,
PhysicsAggregate, PhysicsAggregate,
PhysicsBody, PhysicsBody,
PhysicsMotionType, PhysicsMotionType,
@ -47,8 +45,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: 0}, DefaultScene.MainScene ); this._orbitCenter = new PhysicsAggregate(node, PhysicsShapeType.SPHERE, {radius: .1, mass: 1000}, DefaultScene.MainScene );
this._orbitCenter.body.setMotionType(PhysicsMotionType.STATIC); this._orbitCenter.body.setMotionType(PhysicsMotionType.ANIMATED);
this._explosionManager = new ExplosionManager(DefaultScene.MainScene, { this._explosionManager = new ExplosionManager(DefaultScene.MainScene, {
duration: 2000, duration: 2000,
explosionForce: 150.0, explosionForce: 150.0,
@ -117,12 +115,6 @@ 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()}`);

View File

@ -61,12 +61,6 @@ 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();
@ -296,22 +290,6 @@ 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);

View File

@ -538,13 +538,6 @@ 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

View File

@ -119,12 +119,10 @@ export class InputControlManager {
} }
// Enable pointer selection // Enable pointer selection
console.log(`[InputControlManager] About to update pointer feature...`);
this.updatePointerFeature(); this.updatePointerFeature();
// Emit state change event // Emit state change event
this.emitStateChange(requester); this.emitStateChange(requester);
console.log(`[InputControlManager] ===== Ship controls disabled =====`);
} }
/** /**

View File

@ -2,10 +2,8 @@ import {
AbstractMesh, AbstractMesh,
Color3, Color3,
FreeCamera, FreeCamera,
HavokPlugin,
Mesh, Mesh,
Observable, Observable,
PhysicsActivationControl,
PhysicsAggregate, PhysicsAggregate,
PhysicsMotionType, PhysicsMotionType,
PhysicsShapeType, PhysicsShapeType,
@ -120,18 +118,6 @@ 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
@ -140,7 +126,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
// Position is now set from level config in Level1.initialize() this._ship.position.y = 5;
// Create physics if enabled // Create physics if enabled
const config = GameConfig.getInstance(); const config = GameConfig.getInstance();
@ -158,37 +144,11 @@ export class Ship {
); );
agg.body.setMotionType(PhysicsMotionType.DYNAMIC); agg.body.setMotionType(PhysicsMotionType.DYNAMIC);
agg.body.setLinearDamping(config.shipPhysics.linearDamping); agg.body.setLinearDamping(0.2);
agg.body.setAngularDamping(config.shipPhysics.angularDamping); agg.body.setAngularDamping(0.4);
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) => {
@ -316,14 +276,18 @@ 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.onAfterPhysicsObservable.add(() => {
this.updatePhysics();
})
DefaultScene.MainScene.onAfterRenderObservable.add(() => { DefaultScene.MainScene.onAfterRenderObservable.add(() => {
this._frameCount++;
if (this._frameCount >= 10) {
this._frameCount = 0;
this.updatePhysics();
}
// 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();
}); });
@ -509,9 +473,6 @@ 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(),
@ -522,24 +483,16 @@ export class Ship {
rightStick: Vector2.Zero(), rightStick: Vector2.Zero(),
}; };
// Merge inputs with smooth deadzone scaling (controller takes priority if active, keyboard disabled in VR) // Merge inputs (controller takes priority if active)
// 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:
leftMagnitude > 0.1 controllerState.leftStick.length() > 0.1
? controllerState.leftStick.scale(leftScale) ? controllerState.leftStick
: (inVRMode ? Vector2.Zero() : keyboardState.leftStick), : keyboardState.leftStick,
rightStick: rightStick:
rightMagnitude > 0.1 controllerState.rightStick.length() > 0.1
? controllerState.rightStick.scale(rightScale) ? controllerState.rightStick
: (inVRMode ? Vector2.Zero() : keyboardState.rightStick), : keyboardState.rightStick,
}; };
// Apply forces and get magnitudes for audio // Apply forces and get magnitudes for audio

View File

@ -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.15) { if (Math.abs(leftStick.y) > 0.1) {
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
@ -71,33 +71,24 @@ export class ShipPhysics {
// Only apply force if we haven't reached max velocity // Only apply force if we haven't reached max velocity
if (currentSpeed < this._config.maxLinearVelocity) { if (currentSpeed < this._config.maxLinearVelocity) {
// Get local direction (Z-axis for forward/backward thrust) // Get local direction (Z-axis for forward/backward thrust)
const thrustDirection = -leftStick.y; // negative = forward, positive = reverse const localDirection = new Vector3(0, 0, -leftStick.y);
const localDirection = new Vector3(0, 0, thrustDirection);
// Transform to world space // Transform to world space
const worldDirection = Vector3.TransformNormal( const worldDirection = Vector3.TransformNormal(
localDirection, localDirection,
transformNode.getWorldMatrix() transformNode.getWorldMatrix()
); );
const force = worldDirection.scale(this._config.linearForceMultiplier);
// Apply reverse thrust factor: forward at full power, reverse at reduced power // Calculate thrust point: center of mass + offset (0, 1, 0) in world space
const thrustMultiplier = thrustDirection < 0 const thrustPoint = Vector3.TransformCoordinates(
? 1.0 // Forward thrust at full power physicsBody.getMassProperties().centerOfMass.add(new Vector3(0, 1, 0)),
: this._config.reverseThrustFactor; // Reverse thrust scaled down transformNode.getWorldMatrix()
const force = worldDirection.scale(
this._config.linearForceMultiplier * thrustMultiplier
); );
// Apply force at ship's world position (center of mass)
// Since we overrode center of mass to (0,0,0) in local space, the transform origin is the CoM
// Using getAbsolutePosition() instead of transforming CoM avoids gyroscopic coupling during rotation
const thrustPoint = transformNode.getAbsolutePosition();
physicsBody.applyForce(force, thrustPoint); physicsBody.applyForce(force, thrustPoint);
// Consume fuel based on config rate (tuned for 1 minute at full thrust) // Consume fuel: normalized magnitude (0-1) * 0.005 per frame
const fuelConsumption = linearMagnitude * this._config.linearFuelConsumptionRate; const fuelConsumption = linearMagnitude * 0.005;
this._shipStatus.consumeFuel(fuelConsumption); this._shipStatus.consumeFuel(fuelConsumption);
// Track fuel consumed for statistics // Track fuel consumed for statistics
@ -135,15 +126,11 @@ 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 based on config rate (tuned for 2 minutes at full thrust) // Consume fuel: normalized magnitude (0-3 max) / 3 * 0.005 per frame
const normalizedAngularMagnitude = Math.min(angularMagnitude / 3.0, 1.0); const normalizedAngularMagnitude = Math.min(angularMagnitude / 3.0, 1.0);
const fuelConsumption = normalizedAngularMagnitude * this._config.angularFuelConsumptionRate; const fuelConsumption = normalizedAngularMagnitude * 0.005;
this._shipStatus.consumeFuel(fuelConsumption); this._shipStatus.consumeFuel(fuelConsumption);
// Track fuel consumed for statistics // Track fuel consumed for statistics

View File

@ -46,8 +46,6 @@ 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);

View File

@ -94,7 +94,6 @@ 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);