Compare commits

...

2 Commits

Author SHA1 Message Date
052d972e69 Optimize WebXR performance and add procedural planet generation
Some checks failed
Build / build (push) Failing after 4m43s
Enable multiview rendering in WebXR for improved framerate, remove expensive PhotoDome per-frame updates, and add planet generation system with 76 unique textures across 12 planet types.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 11:50:18 -05:00
57ffe8f689 Migrate to AudioEngineV2 and fix Meta Quest 2 controller detection
- Upgrade audio system from deprecated Sound API to AudioEngineV2
  - Use CreateAudioEngineAsync() for audio engine initialization
  - Replace new Sound() with createSoundAsync() throughout codebase
  - Track sound playing state manually (StaticSound lacks isPlaying)
  - Use volume property instead of setVolume() method
  - Use stop() instead of pause() for proper StaticSound lifecycle

- Fix controller detection for Meta Quest 2
  - Check for already-connected controllers after entering XR mode
  - Fixes issue where Quest 2 controllers only become available after enterXRAsync()
  - Maintains backward compatibility with WebXR emulator

- Improve initialization performance
  - Move RockFactory.init() to main initialization (before level select)
  - Pre-load asteroid meshes and explosion particle systems on startup
  - Level initialization now only creates asteroids, not resources

- Refactor level initialization flow
  - Level creation now happens before entering XR mode
  - Add level ready observable to track initialization completion
  - Show loading messages during asteroid creation
  - Extract loading message logic to separate module

- Add audio unlock on user interaction (button click)
- Make play() methods async to support AudioEngineV2
- Pass AudioEngineV2 instance to Ship and Level1 constructors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 13:59:48 -05:00
86 changed files with 843 additions and 133 deletions

58
package-lock.json generated
View File

@ -24,9 +24,9 @@
}
},
"node_modules/@babylonjs/addons": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/addons/-/addons-8.32.0.tgz",
"integrity": "sha512-mv3rF6slOYcArpQbWJzVlN7Qb71/um4HpZRYIyA37wmmoX1behxk+1Of9J6PqdrwLQjng9e8gxfrIA1OUxIEpw==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@babylonjs/addons/-/addons-8.32.1.tgz",
"integrity": "sha512-FAPhpiE7tZvmVX6DiKbRmM/koOUrvBItYGKDWyXikaV9ftkOfQNi82Oz9s08WdmIsItE6jUmNT3Kc7+T5JDeSg==",
"peer": true,
"peerDependencies": {
"@babylonjs/core": "^8.0.0"
@ -38,17 +38,17 @@
"integrity": "sha512-Z83WIe2eZEAOo5bb9Tjd+lY4ru6N8qgtZJGjWcoXOiP3BrtbatPUXdVKqm7m60ItQABFaVdMGygvIXY+wNXU/Q=="
},
"node_modules/@babylonjs/gui": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/gui/-/gui-8.32.0.tgz",
"integrity": "sha512-nR+E3u3hgGky+/6k1h8F5B4tS4OW6x3y63hf88kS3GgANd8as2iL54NC1Z74a25/8/nlaSRhodEYwx5O7lZKlQ==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@babylonjs/gui/-/gui-8.32.1.tgz",
"integrity": "sha512-KTAtDYK4JUsqfgnLxQ1tVu5PU8nQr2VJupT5wlojQkLmsmP1pyiTMQ7r00BcJUiZQu+0MJBS4yQ+6WDBS8NWiA==",
"peerDependencies": {
"@babylonjs/core": "^8.0.0"
}
},
"node_modules/@babylonjs/gui-editor": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/gui-editor/-/gui-editor-8.32.0.tgz",
"integrity": "sha512-sRql2tCW+dUulQxWbzSOskGsgDUGYHJ5mM0eLbsKPFTCggURse3drNtynOw/24dO/OCaES0zO0Ob/nDH0qrFxA==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@babylonjs/gui-editor/-/gui-editor-8.32.1.tgz",
"integrity": "sha512-cPm9bfBEksZXtZ3NgDYkvIcimEAaAOqDdhTZC2EWLUks6fVAZgheGa4IxV5zHQxpl9djhxqKDJPS4I1zFUw2IQ==",
"peer": true,
"peerDependencies": {
"@babylonjs/core": "^8.0.0",
@ -489,45 +489,41 @@
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz",
"integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==",
"hasInstallScript": true,
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz",
"integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==",
"hasInstallScript": true,
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.2"
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz",
"integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==",
"hasInstallScript": true,
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.2"
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz",
"integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==",
"hasInstallScript": true,
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.2"
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
@ -815,9 +811,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/babylonjs-gltf2interface": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-8.32.0.tgz",
"integrity": "sha512-OECfOlxbIXHp4kYzqZNj42e0I5MfVAmgAgBkQaFGJdojK+hc/iIg9LQfAhYbKjr+Rpb1v1HnQ8tWBjXLwgtyXg==",
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-8.32.1.tgz",
"integrity": "sha512-GfpzooetdbFU22X75SvWzAMjzfkdypzB4WtG5Y+F2UGFf0CUa9PCftwTiH2wJFaE+OQDXF6+l4rgwZCIjOSUCw==",
"peer": true
},
"node_modules/combined-stream": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

BIN
public/song1.mp3 Normal file

Binary file not shown.

159
src/controllerDebug.ts Normal file
View File

@ -0,0 +1,159 @@
import {
Engine,
Scene,
HemisphericLight,
Vector3,
MeshBuilder,
WebXRDefaultExperience,
Color3
} from "@babylonjs/core";
/**
* Minimal standalone class to debug WebXR controller detection
* Usage: import and instantiate in main.ts instead of normal flow
*/
export class ControllerDebug {
private engine: Engine;
private scene: Scene;
constructor() {
console.log('🔍 ControllerDebug: Starting minimal test...');
this.init();
}
private async init() {
// Get canvas
const canvas = document.querySelector('#gameCanvas') as HTMLCanvasElement;
// Create engine (no antialiasing for Quest compatibility)
console.log('🔍 Creating engine...');
this.engine = new Engine(canvas, false);
// Create scene
console.log('🔍 Creating scene...');
this.scene = new Scene(this.engine);
this.scene.clearColor = new Color3(0.1, 0.1, 0.2).toColor4();
// Add light
//const light = new HemisphericLight("light", new Vector3(0, 1, 0), this.scene);
// Add ground for reference
const ground = MeshBuilder.CreateGround("ground", { width: 10, height: 10 }, this.scene);
// Create WebXR
//consol e.log('🔍 Creating WebXR...');
//await navigator.xr.offerSession("immersive-vr");
const xr = await this.scene.createDefaultXRExperienceAsync( {
disablePointerSelection: true,
disableTeleportation: true,
disableDefaultUI: false, // Enable UI for this test
disableHandTracking: true
});
console.log('🔍 WebXR created successfully');
console.log('🔍 XR input exists:', !!xr.input);
console.log('🔍 XR input controllers:', xr.input.controllers.length);
// Set up controller observable
console.log('🔍 Setting up onControllerAddedObservable...');
xr.input.onControllerAddedObservable.add((controller) => {
console.log('✅ CONTROLLER ADDED! Handedness:', controller.inputSource.handedness);
console.log(' - Input source:', controller.inputSource);
console.log(' - Has motion controller:', !!controller.motionController);
// Wait for motion controller
controller.onMotionControllerInitObservable.add((motionController) => {
console.log('✅ MOTION CONTROLLER INITIALIZED:', motionController.handness);
console.log(' - Profile:', motionController.profileId);
console.log(' - Components:', Object.keys(motionController.components));
// Log when any component changes
Object.keys(motionController.components).forEach(componentId => {
const component = motionController.components[componentId];
if (component.onAxisValueChangedObservable) {
component.onAxisValueChangedObservable.add((axes) => {
console.log(`📍 ${motionController.handness} ${componentId} axes:`, axes);
});
}
if (component.onButtonStateChangedObservable) {
component.onButtonStateChangedObservable.add((state) => {
console.log(`🔘 ${motionController.handness} ${componentId} button:`, {
pressed: state.pressed,
touched: state.touched,
value: state.value
});
});
}
});
});
});
console.log('🔍 Observable registered. Waiting for controllers...');
// Render loop
this.engine.runRenderLoop(() => {
this.scene.render();
});
// Create button to enter VR (requires user gesture)
this.createEnterVRButton(xr);
}
private createEnterVRButton(xr: WebXRDefaultExperience) {
const button = document.createElement('button');
button.textContent = 'Enter VR (Controller Debug)';
button.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 20px 40px;
font-size: 24px;
background: #4CAF50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
z-index: 9999;
`;
button.onclick = async () => {
console.log('🔍 Button clicked - Entering VR mode...');
button.remove();
try {
await xr.baseExperience.enterXRAsync('immersive-vr', 'local-floor', undefined, {
requiredFeatures: ['local-floor'],
});
console.log(xr.baseExperience.featuresManager.getEnabledFeatures());
//await xr.baseExperience.exitXRAsync();
//await xr.baseExperience.enterXRAsync('immersive-vr', 'local-floor');
console.log('🔍 ✅ Entered VR mode successfully');
console.log('🔍 Controllers after entering VR:', xr.input.controllers.length);
// Check again after delays
setTimeout(() => {
console.log('🔍 [+1s after VR] Controller count:', xr.input.controllers.length);
}, 1000);
setTimeout(() => {
console.log('🔍 [+3s after VR] Controller count:', xr.input.controllers.length);
}, 3000);
setTimeout(() => {
console.log('🔍 [+5s after VR] Controller count:', xr.input.controllers.length);
}, 5000);
} catch (error) {
console.error('🔍 ❌ Failed to enter VR:', error);
}
};
document.body.appendChild(button);
console.log('🔍 Click the button to enter VR mode');
}
}

133
src/createPlanets.ts Normal file
View File

@ -0,0 +1,133 @@
import {
AbstractMesh, Color3,
MeshBuilder,
StandardMaterial,
Texture,
Vector3
} from "@babylonjs/core";
import { DefaultScene } from "./defaultScene";
import { getRandomPlanetTexture } from "./planetTextures";
/**
* Creates multiple planets with random textures, sizes, and positions
* @param count - Number of planets to create
* @param sunPosition - Position of the sun (center point for planet orbit distances)
* @param minDiameter - Minimum planet diameter (default: 50)
* @param maxDiameter - Maximum planet diameter (default: 100)
* @param minDistance - Minimum distance from sun (default: 400)
* @param maxDistance - Maximum distance from sun (default: 1000)
* @returns Array of created planet meshes
*/
export function createPlanets(
count: number,
sunPosition: Vector3 = Vector3.Zero(),
minDiameter: number = 100,
maxDiameter: number = 200,
minDistance: number = 500,
maxDistance: number = 1000
): AbstractMesh[] {
const planets: AbstractMesh[] = [];
for (let i = 0; i < count; i++) {
// Random diameter between min and max
const diameter = minDiameter + Math.random() * (maxDiameter - minDiameter);
// Create sphere
const planet = MeshBuilder.CreateSphere(
`planet-${i}`,
{ diameter: diameter, segments: 32 },
DefaultScene.MainScene
);
// Random distance from sun
const distance = minDistance + Math.random() * (maxDistance - minDistance);
// Random position on a sphere around the sun
const theta = Math.random() * Math.PI * 2; // Random angle around Y axis
const phi = Math.random() * Math.PI; // Random angle from Y axis
// Convert spherical coordinates to Cartesian
const x = distance * Math.sin(phi) * Math.cos(theta);
const y = distance * Math.sin(phi) * Math.sin(theta);
const z = distance * Math.cos(phi);
planet.position = new Vector3(
sunPosition.x + x,
sunPosition.y + y,
sunPosition.z + z
);
// Apply random planet texture
const material = new StandardMaterial(`planet-material-${i}`, DefaultScene.MainScene);
material.diffuseTexture = new Texture(getRandomPlanetTexture(), DefaultScene.MainScene);
planet.material = material;
material.specularColor = Color3.Black()
planets.push(planet);
}
console.log(`Created ${count} planets with diameters ${minDiameter}-${maxDiameter} at distances ${minDistance}-${maxDistance}`);
return planets;
}
/**
* Creates planets in a more organized orbital pattern (flat solar system style)
* @param count - Number of planets to create
* @param sunPosition - Position of the sun
* @param minDiameter - Minimum planet diameter (default: 50)
* @param maxDiameter - Maximum planet diameter (default: 100)
* @param minDistance - Minimum distance from sun (default: 400)
* @param maxDistance - Maximum distance from sun (default: 1000)
* @returns Array of created planet meshes
*/
export function createPlanetsOrbital(
count: number,
sunPosition: Vector3 = Vector3.Zero(),
minDiameter: number = 50,
maxDiameter: number = 100,
minDistance: number = 400,
maxDistance: number = 1000
): AbstractMesh[] {
const planets: AbstractMesh[] = [];
for (let i = 0; i < count; i++) {
// Random diameter between min and max
const diameter = minDiameter + Math.random() * (maxDiameter - minDiameter);
// Create sphere
const planet = MeshBuilder.CreateSphere(
`planet-${i}`,
{ diameter: diameter, segments: 32 },
DefaultScene.MainScene
);
// Random distance from sun
const distance = minDistance + Math.random() * (maxDistance - minDistance);
// Random angle around Y axis (orbital plane)
const angle = Math.random() * Math.PI * 2;
// Keep planets mostly in a plane (like a solar system)
const y = (Math.random() - 0.5) * 100; // Small vertical variation
planet.position = new Vector3(
sunPosition.x + distance * Math.cos(angle),
sunPosition.y + y,
sunPosition.z + distance * Math.sin(angle)
);
// Apply random planet texture
const material = new StandardMaterial(`planet-material-${i}`, DefaultScene.MainScene);
const texture = new Texture(getRandomPlanetTexture(), DefaultScene.MainScene);
material.diffuseTexture = texture;
material.ambientTexture = texture;
planet.material = material;
material.specularColor = Color3.Black()
planets.push(planet);
}
console.log(`Created ${count} planets in orbital pattern with diameters ${minDiameter}-${maxDiameter} at distances ${minDistance}-${maxDistance}`);
return planets;
}

View File

@ -6,28 +6,42 @@ import {
PhysicsMotionType,
PhysicsShapeType,
PointLight,
StandardMaterial,
StandardMaterial, Texture,
Vector3
} from "@babylonjs/core";
import {DefaultScene} from "./defaultScene";
import {FireProceduralTexture} from "@babylonjs/procedural-textures";
export function createSun() : AbstractMesh {
const light = new PointLight("light", new Vector3(0, 0, 0), DefaultScene.MainScene);
const light = new PointLight("light", new Vector3(0, 0, 400), DefaultScene.MainScene);
light.intensity = 100;
const sun = MeshBuilder.CreateSphere("sun", {diameter: 50, segments: 32}, DefaultScene.MainScene);
const sunAggregate = new PhysicsAggregate(sun, PhysicsShapeType.SPHERE, {mass: 0}, DefaultScene.MainScene);
sunAggregate.body.setMotionType(PhysicsMotionType.STATIC);
//const sunAggregate = new PhysicsAggregate(sun, PhysicsShapeType.SPHERE, {mass: 0}, DefaultScene.MainScene);
//sunAggregate.body.setMotionType(PhysicsMotionType.STATIC);
const material = new StandardMaterial("material", DefaultScene.MainScene);
material.emissiveTexture =new FireProceduralTexture("fire", 256, DefaultScene.MainScene);
material.emissiveTexture =new FireProceduralTexture("fire", 512, DefaultScene.MainScene);
material.emissiveColor = new Color3(.5, .5, .1);
material.disableLighting = true;
sun.material = material;
const gl = new GlowLayer("glow", DefaultScene.MainScene);
//gl.addIncludedOnlyMesh(sun);
gl.intensity = 5;
gl.intensity = 1;
sun.position = new Vector3(0, 0, 0);
sun.position = new Vector3(0, 0, 400);
return sun;
}
export function createPlanet(position: Vector3, diameter: number, name: string) : AbstractMesh {
const planet = MeshBuilder.CreateSphere(name, {diameter: diameter, segments: 32}, DefaultScene.MainScene);
const material = new StandardMaterial(name + "-material", DefaultScene.MainScene);
const texture = new Texture("/planetTextures/Arid/Arid_01-512x512.png", DefaultScene.MainScene);
material.diffuseTexture = texture;
material.ambientTexture = texture;
material.roughness = 1;
material.specularColor = Color3.Black();
//material.diffuseColor = new Color3(Math.random(), Math.random(), Math.random());
planet.material = material;
planet.position = position;
return planet;
}

View File

@ -7,15 +7,19 @@ import {
ParticleHelper,
PhysicsAggregate,
PhysicsMotionType,
PhysicsShapeType, PointsCloudSystem, Sound,
PhysicsShapeType, PointsCloudSystem,
StandardMaterial, TransformNode,
Vector3
} from "@babylonjs/core";
import type {AudioEngineV2} from "@babylonjs/core";
import {Ship} from "./ship";
import {RockFactory} from "./starfield";
import Level from "./level";
import {Scoreboard} from "./scoreboard";
import setLoadingMessage from "./setLoadingMessage";
import {createPlanet, createSun} from "./createSun";
import {createPlanetsOrbital} from "./createPlanets";
export class Level1 implements Level {
private _ship: Ship;
@ -25,6 +29,7 @@ export class Level1 implements Level {
private _endBase: AbstractMesh;
private _scoreboard: Scoreboard;
private _difficulty: string;
private _audioEngine: AudioEngineV2;
private _difficultyConfig: {
rockCount: number;
forceMultiplier: number;
@ -34,19 +39,30 @@ export class Level1 implements Level {
distanceMax: number;
};
constructor(difficulty: string = 'recruit') {
constructor(difficulty: string = 'recruit', audioEngine: AudioEngineV2) {
this._difficulty = difficulty;
this._audioEngine = audioEngine;
this._difficultyConfig = this.getDifficultyConfig(difficulty);
this._ship = new Ship();
this._ship = new Ship(undefined, audioEngine);
this._scoreboard = new Scoreboard();
const xr = DefaultScene.XR;
console.log('Level1 constructor - Setting up XR observables');
console.log('XR input exists:', !!xr.input);
console.log('onControllerAddedObservable exists:', !!xr.input?.onControllerAddedObservable);
xr.baseExperience.onInitialXRPoseSetObservable.add(() => {
xr.baseExperience.camera.parent = this._ship.transformNode;
xr.baseExperience.camera.position = new Vector3(0, 0, 0);
const observer = xr.input.onControllerAddedObservable.add((controller) => {
console.log('🎮 onControllerAddedObservable FIRED for:', controller.inputSource.handedness);
this._ship.addController(controller);
});
});
xr.input.onControllerAddedObservable.add((controller) => {
this._ship.addController(controller);
});
//console.log('Controller observable registered, observer:', !!observer);
this.createStartBase();
this.initialize();
@ -58,10 +74,10 @@ export class Level1 implements Level {
return {
rockCount: 5,
forceMultiplier: 1,
rockSizeMin: 4,
rockSizeMin: 5,
rockSizeMax: 10,
distanceMin: 150,
distanceMax: 180
distanceMax: 200
};
case 'pilot':
return {
@ -116,10 +132,31 @@ export class Level1 implements Level {
}
private scored: Set<string> = new Set<string>();
public play() {
const background = new Sound("background", "/background.mp3", DefaultScene.MainScene, () => {
}, {loop: true, autoplay: true, volume: .2});
DefaultScene.XR.baseExperience.enterXRAsync('immersive-vr', 'local-floor');
public async play() {
// Create background music using AudioEngineV2
const background = await this._audioEngine.createSoundAsync("background", "/song1.mp3", {
loop: true,
volume: 0.5
});
background.play();
// Enter XR mode
const xr = await DefaultScene.XR.baseExperience.enterXRAsync('immersive-vr', 'local-floor');
// Check for controllers that are already connected after entering XR
console.log('Checking for controllers after entering XR. Count:', DefaultScene.XR.input.controllers.length);
DefaultScene.XR.input.controllers.forEach((controller, index) => {
console.log(`Controller ${index} - handedness: ${controller.inputSource.handedness}`);
this._ship.addController(controller);
});
// Wait and check again after a delay (controllers might connect later)
console.log('Waiting 2 seconds to check for controllers again...');
setTimeout(() => {
console.log('After 2 second delay - controller count:', DefaultScene.XR.input.controllers.length);
DefaultScene.XR.input.controllers.forEach((controller, index) => {
console.log(` Late controller ${index} - handedness: ${controller.inputSource.handedness}`);
});
}, 2000);
}
public dispose() {
this._startBase.dispose();
@ -130,20 +167,19 @@ export class Level1 implements Level {
if (this._initialized) {
return;
}
this.createBackgroundElements();
this._initialized = true;
ParticleHelper.BaseAssetsUrl = window.location.href;
this._ship.position = new Vector3(0, 1, 0);
await RockFactory.init();
const config = this._difficultyConfig;
console.log(config);
setLoadingMessage("Creating Asteroids...");
for (let i = 0; i < config.rockCount; i++) {
const distRange = config.distanceMax - config.distanceMin;
const dist = (Math.random() * distRange) + config.distanceMin;
const sizeRange = config.rockSizeMax - config.rockSizeMin;
const size = Vector3.Random(1,1.3).scale(Math.random() * sizeRange + config.rockSizeMin)
const rock = await RockFactory.createRock(i, new Vector3(Math.random() * 200 +50 * Math.sign(Math.random() -.5),200,200),
const rock = await RockFactory.createRock(i, new Vector3(0,1,dist),
size,
this._scoreboard.onScoreObservable);
const constraint = new DistanceConstraint(dist, DefaultScene.MainScene);
@ -164,12 +200,16 @@ export class Level1 implements Level {
*/
this._scoreboard.onScoreObservable.notifyObservers({
score: 0,
remaining: 1,
remaining: i+1,
message: "Get Ready"
});
this._startBase.physicsBody.addConstraint(rock.physicsBody, constraint);
rock.physicsBody.applyForce(Vector3.Random(-1, 1).scale(5000000 * config.forceMultiplier), rock.position);
rock.physicsBody.applyForce(new Vector3(50000000 * config.forceMultiplier, 0, 0), rock.position);
//rock.physicsBody.applyForce(Vector3.Random(-1, 1).scale(5000000 * config.forceMultiplier), rock.position);
}
// Notify that initialization is complete
this._onReadyObservable.notifyObservers(this);
}
private createStartBase() {
@ -200,6 +240,27 @@ export class Level1 implements Level {
agg.body.setMotionType(PhysicsMotionType.ANIMATED);
this._endBase = mesh;
}
private createBackgroundElements() {
//const sun = MeshBuilder.CreateSphere("sun", {diameter: 200}, DefaultScene.MainScene);
//const sunMaterial = new StandardMaterial("sunMaterial", DefaultScene.MainScene);
//sunMaterial.emissiveColor = new Color3(1, 1, 0);
//sun.material = sunMaterial;
//sun.position = new Vector3(-200, 300, 500);
const sun = createSun();
// Create planets around the sun
const sunPosition = sun.position;
const planets = createPlanetsOrbital(
8, // 8 planets
sunPosition, // sun position
50, // min diameter
100, // max diameter
400, // min distance from sun
1000 // max distance from sun
);
console.log(`Created ${planets.length} planets around sun at position`, sunPosition);
}
private createTarget(i: number) {
const target = MeshBuilder.CreateTorus("target" + i, {diameter: 10, tessellation: 72}, DefaultScene.MainScene);

View File

@ -1,23 +1,29 @@
import type {AudioEngineV2} from "@babylonjs/core";
import {
Color3,
CreateAudioEngineAsync,
Engine,
HavokPlugin,
PhotoDome,
Scene, StandardMaterial,
ParticleHelper,
Scene,
Vector3,
WebGPUEngine,
WebXRDefaultExperience
WebXRDefaultExperience,
WebXRFeatureName
} from "@babylonjs/core";
import '@babylonjs/loaders';
import HavokPhysics from "@babylonjs/havok";
import {DefaultScene} from "./defaultScene";
import {Ship} from "./ship";
import {Level1} from "./level1";
import {Scoreboard} from "./scoreboard";
import Demo from "./demo";
import Level from "./level";
import setLoadingMessage from "./setLoadingMessage";
import {RockFactory} from "./starfield";
import {ControllerDebug} from "./controllerDebug";
// Set to true to run minimal controller debug test
const DEBUG_CONTROLLERS = false;
const webGpu = false;
const canvas = (document.querySelector('#gameCanvas') as HTMLCanvasElement);
enum GameState {
@ -25,44 +31,62 @@ enum GameState {
DEMO
}
export class Main {
private _loadingDiv: HTMLElement;
private _currentLevel: Level;
private _gameState: GameState = GameState.DEMO;
private _selectedDifficulty: string = 'recruit';
private _engine: Engine | WebGPUEngine;
private _audioEngine: AudioEngineV2;
constructor() {
this._loadingDiv = document.querySelector('#loadingDiv');
if (!navigator.xr) {
this._loadingDiv.innerText = "This browser does not support WebXR";
setLoadingMessage("This browser does not support WebXR");
return;
}
this.initialize();
document.querySelectorAll('.level-button').forEach(button => {
button.addEventListener('click', (e) => {
button.addEventListener('click', async (e) => {
const levelButton = e.target as HTMLButtonElement;
this._selectedDifficulty = levelButton.dataset.level;
this.setLoadingMessage("Initializing Level...");
this._currentLevel = new Level1(this._selectedDifficulty);
// Unlock audio engine if it exists
if (this._engine?.audioEngine) {
this._engine.audioEngine.unlock();
// Show loading UI again
const mainDiv = document.querySelector('#mainDiv');
const levelSelect = document.querySelector('#levelSelect') as HTMLElement;
if (levelSelect) {
levelSelect.style.display = 'none';
}
this.play();
document.querySelector('#mainDiv').remove();
setLoadingMessage("Initializing Level...");
// Unlock audio engine on user interaction
if (this._audioEngine) {
await this._audioEngine.unlockAsync();
}
// Create and initialize level BEFORE entering XR
this._currentLevel = new Level1(this._selectedDifficulty, this._audioEngine);
// Wait for level to be ready
this._currentLevel.getReadyObservable().add(() => {
setLoadingMessage("Level Ready! Entering VR...");
// Small delay to show message
setTimeout(() => {
mainDiv.remove();
this.play();
}, 500);
});
});
});
}
private _started = false;
public play() {
public async play() {
this._gameState = GameState.PLAY;
this._currentLevel.play();
await this._currentLevel.play();
}
public demo() {
this._gameState = GameState.DEMO;
}
private async initialize() {
this._loadingDiv.innerText = "Initializing.";
setLoadingMessage("Initializing.");
await this.setupScene();
DefaultScene.XR = await WebXRDefaultExperience.CreateAsync(DefaultScene.MainScene, {
@ -71,25 +95,28 @@ export class Main {
disableNearInteraction: true,
disableHandTracking: true,
disableDefaultUI: true,
});
DefaultScene.XR.baseExperience.featuresManager.enableFeature(WebXRFeatureName.LAYERS, "stable",
{preferMultiviewOnInit: true});
this.setLoadingMessage("Get Ready!");
const photoDome1 = new PhotoDome("testdome", '/8192.webp', {size: 1000}, DefaultScene.MainScene);
photoDome1.material.diffuseTexture.hasAlpha = true;
photoDome1.material.alpha = .3;
setLoadingMessage("Get Ready!");
const photoDome2 = new PhotoDome("testdome", '/8192.webp', {size: 2000}, DefaultScene.MainScene);
photoDome2.rotation.y = Math.PI;
photoDome2.rotation.x = Math.PI/2;
//const photoDome1 = new PhotoDome("testdome", '/8192.webp', {size: 1000}, DefaultScene.MainScene);
//photoDome1.material.diffuseTexture.hasAlpha = true;
//photoDome1.material.alpha = .3;
//const photoDome2 = new PhotoDome("testdome", '/8192.webp', {size: 2000}, DefaultScene.MainScene);
//photoDome2.rotation.y = Math.PI;
//photoDome2.rotation.x = Math.PI/2;
DefaultScene.MainScene.onAfterRenderObservable.add(() => {
photoDome1.position = DefaultScene.MainScene.activeCamera.globalPosition;
photoDome2.position = DefaultScene.MainScene.activeCamera.globalPosition;
// photoDome1.position = DefaultScene.MainScene.activeCamera.globalPosition;
// photoDome2.position = DefaultScene.MainScene.activeCamera.globalPosition;
});
setLoadingMessage("Select a difficulty to begin!");
}
private setLoadingMessage(message: string) {
this._loadingDiv.innerText = message;
}
private async setupScene() {
if (webGpu) {
@ -106,37 +133,50 @@ export class Main {
DefaultScene.MainScene = new Scene(this._engine);
DefaultScene.MainScene.ambientColor = new Color3(.2, .2, .2);
this.setLoadingMessage("Initializing Physics Engine..");
setLoadingMessage("Initializing Physics Engine..");
await this.setupPhysics();
this.setLoadingMessage("Physics Engine Ready!");
setLoadingMessage("Physics Engine Ready!");
setLoadingMessage("Loading Asteroids and Explosions...");
ParticleHelper.BaseAssetsUrl = window.location.href;
await RockFactory.init();
setLoadingMessage("Ready!");
// Initialize AudioEngineV2
setLoadingMessage("Initializing Audio Engine...");
this._audioEngine = await CreateAudioEngineAsync();
this.setupInspector();
this._engine.runRenderLoop(() => {
window.setTimeout(()=>{
if (!this._started) {
this._started = true;
this._loadingDiv.remove();
const levelSelect = document.querySelector('#levelSelect');
if (levelSelect) {
levelSelect.classList.add('ready');
setLoadingMessage("Ready!");
}
}
if (this._gameState == GameState.PLAY) {
})
this._engine.runRenderLoop(() => {
DefaultScene.MainScene.render();
} else {
DefaultScene.DemoScene.render();
}
});
}
private async setupPhysics() {
const havok = await HavokPhysics();
const havokPlugin = new HavokPlugin(true, havok);
DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin);
DefaultScene.MainScene.getPhysicsEngine().setSubTimeStep(5);
DefaultScene.MainScene.getPhysicsEngine().setTimeStep(1/90);
DefaultScene.MainScene.getPhysicsEngine().setSubTimeStep(9);
DefaultScene.MainScene.collisionsEnabled = true;
}
private setupInspector() {
this.setLoadingMessage("Initializing Inspector...");
setLoadingMessage("Initializing Inspector...");
window.addEventListener("keydown", (ev) => {
if (ev.key == 'i') {
import ("@babylonjs/inspector").then((inspector) => {
@ -150,8 +190,18 @@ export class Main {
}
}
const main = new Main();
const demo = new Demo(main);
if (DEBUG_CONTROLLERS) {
console.log('🔍 DEBUG MODE: Running minimal controller test');
// Hide the UI elements
const mainDiv = document.querySelector('#mainDiv');
if (mainDiv) {
(mainDiv as HTMLElement).style.display = 'none';
}
new ControllerDebug();
} else {
const main = new Main();
const demo = new Demo(main);
}

225
src/planetTextures.ts Normal file
View File

@ -0,0 +1,225 @@
/**
* Planet texture paths for randomly generating planets
* All textures are 512x512 PNG files
*/
export const PLANET_TEXTURES = [
// Arid planets (5 textures)
"/planetTextures/Arid/Arid_01-512x512.png",
"/planetTextures/Arid/Arid_02-512x512.png",
"/planetTextures/Arid/Arid_03-512x512.png",
"/planetTextures/Arid/Arid_04-512x512.png",
"/planetTextures/Arid/Arid_05-512x512.png",
// Barren planets (5 textures)
"/planetTextures/Barren/Barren_01-512x512.png",
"/planetTextures/Barren/Barren_02-512x512.png",
"/planetTextures/Barren/Barren_03-512x512.png",
"/planetTextures/Barren/Barren_04-512x512.png",
"/planetTextures/Barren/Barren_05-512x512.png",
// Dusty planets (5 textures)
"/planetTextures/Dusty/Dusty_01-512x512.png",
"/planetTextures/Dusty/Dusty_02-512x512.png",
"/planetTextures/Dusty/Dusty_03-512x512.png",
"/planetTextures/Dusty/Dusty_04-512x512.png",
"/planetTextures/Dusty/Dusty_05-512x512.png",
// Gaseous planets (20 textures)
"/planetTextures/Gaseous/Gaseous_01-512x512.png",
"/planetTextures/Gaseous/Gaseous_02-512x512.png",
"/planetTextures/Gaseous/Gaseous_03-512x512.png",
"/planetTextures/Gaseous/Gaseous_04-512x512.png",
"/planetTextures/Gaseous/Gaseous_05-512x512.png",
"/planetTextures/Gaseous/Gaseous_06-512x512.png",
"/planetTextures/Gaseous/Gaseous_07-512x512.png",
"/planetTextures/Gaseous/Gaseous_08-512x512.png",
"/planetTextures/Gaseous/Gaseous_09-512x512.png",
"/planetTextures/Gaseous/Gaseous_10-512x512.png",
"/planetTextures/Gaseous/Gaseous_11-512x512.png",
"/planetTextures/Gaseous/Gaseous_12-512x512.png",
"/planetTextures/Gaseous/Gaseous_13-512x512.png",
"/planetTextures/Gaseous/Gaseous_14-512x512.png",
"/planetTextures/Gaseous/Gaseous_15-512x512.png",
"/planetTextures/Gaseous/Gaseous_16-512x512.png",
"/planetTextures/Gaseous/Gaseous_17-512x512.png",
"/planetTextures/Gaseous/Gaseous_18-512x512.png",
"/planetTextures/Gaseous/Gaseous_19-512x512.png",
"/planetTextures/Gaseous/Gaseous_20-512x512.png",
// Grassland planets (5 textures)
"/planetTextures/Grassland/Grassland_01-512x512.png",
"/planetTextures/Grassland/Grassland_02-512x512.png",
"/planetTextures/Grassland/Grassland_03-512x512.png",
"/planetTextures/Grassland/Grassland_04-512x512.png",
"/planetTextures/Grassland/Grassland_05-512x512.png",
// Jungle planets (5 textures)
"/planetTextures/Jungle/Jungle_01-512x512.png",
"/planetTextures/Jungle/Jungle_02-512x512.png",
"/planetTextures/Jungle/Jungle_03-512x512.png",
"/planetTextures/Jungle/Jungle_04-512x512.png",
"/planetTextures/Jungle/Jungle_05-512x512.png",
// Marshy planets (5 textures)
"/planetTextures/Marshy/Marshy_01-512x512.png",
"/planetTextures/Marshy/Marshy_02-512x512.png",
"/planetTextures/Marshy/Marshy_03-512x512.png",
"/planetTextures/Marshy/Marshy_04-512x512.png",
"/planetTextures/Marshy/Marshy_05-512x512.png",
// Martian planets (5 textures)
"/planetTextures/Martian/Martian_01-512x512.png",
"/planetTextures/Martian/Martian_02-512x512.png",
"/planetTextures/Martian/Martian_03-512x512.png",
"/planetTextures/Martian/Martian_04-512x512.png",
"/planetTextures/Martian/Martian_05-512x512.png",
// Methane planets (5 textures)
"/planetTextures/Methane/Methane_01-512x512.png",
"/planetTextures/Methane/Methane_02-512x512.png",
"/planetTextures/Methane/Methane_03-512x512.png",
"/planetTextures/Methane/Methane_04-512x512.png",
"/planetTextures/Methane/Methane_05-512x512.png",
// Sandy planets (5 textures)
"/planetTextures/Sandy/Sandy_01-512x512.png",
"/planetTextures/Sandy/Sandy_02-512x512.png",
"/planetTextures/Sandy/Sandy_03-512x512.png",
"/planetTextures/Sandy/Sandy_04-512x512.png",
"/planetTextures/Sandy/Sandy_05-512x512.png",
// Snowy planets (5 textures)
"/planetTextures/Snowy/Snowy_01-512x512.png",
"/planetTextures/Snowy/Snowy_02-512x512.png",
"/planetTextures/Snowy/Snowy_03-512x512.png",
"/planetTextures/Snowy/Snowy_04-512x512.png",
"/planetTextures/Snowy/Snowy_05-512x512.png",
// Tundra planets (5 textures)
"/planetTextures/Tundra/Tundra_01-512x512.png",
"/planetTextures/Tundra/Tundra_02-512x512.png",
"/planetTextures/Tundra/Tundra_03-512x512.png",
"/planetTextures/Tundra/Tundra_04-512x512.png",
"/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
];
/**
* Get a random planet texture path
*/
export function getRandomPlanetTexture(): string {
return PLANET_TEXTURES[Math.floor(Math.random() * PLANET_TEXTURES.length)];
}
/**
* Planet texture categories organized by type
*/
export const PLANET_TEXTURES_BY_TYPE = {
arid: [
"/planetTextures/Arid/Arid_01-512x512.png",
"/planetTextures/Arid/Arid_02-512x512.png",
"/planetTextures/Arid/Arid_03-512x512.png",
"/planetTextures/Arid/Arid_04-512x512.png",
"/planetTextures/Arid/Arid_05-512x512.png",
],
barren: [
"/planetTextures/Barren/Barren_01-512x512.png",
"/planetTextures/Barren/Barren_02-512x512.png",
"/planetTextures/Barren/Barren_03-512x512.png",
"/planetTextures/Barren/Barren_04-512x512.png",
"/planetTextures/Barren/Barren_05-512x512.png",
],
dusty: [
"/planetTextures/Dusty/Dusty_01-512x512.png",
"/planetTextures/Dusty/Dusty_02-512x512.png",
"/planetTextures/Dusty/Dusty_03-512x512.png",
"/planetTextures/Dusty/Dusty_04-512x512.png",
"/planetTextures/Dusty/Dusty_05-512x512.png",
],
gaseous: [
"/planetTextures/Gaseous/Gaseous_01-512x512.png",
"/planetTextures/Gaseous/Gaseous_02-512x512.png",
"/planetTextures/Gaseous/Gaseous_03-512x512.png",
"/planetTextures/Gaseous/Gaseous_04-512x512.png",
"/planetTextures/Gaseous/Gaseous_05-512x512.png",
"/planetTextures/Gaseous/Gaseous_06-512x512.png",
"/planetTextures/Gaseous/Gaseous_07-512x512.png",
"/planetTextures/Gaseous/Gaseous_08-512x512.png",
"/planetTextures/Gaseous/Gaseous_09-512x512.png",
"/planetTextures/Gaseous/Gaseous_10-512x512.png",
"/planetTextures/Gaseous/Gaseous_11-512x512.png",
"/planetTextures/Gaseous/Gaseous_12-512x512.png",
"/planetTextures/Gaseous/Gaseous_13-512x512.png",
"/planetTextures/Gaseous/Gaseous_14-512x512.png",
"/planetTextures/Gaseous/Gaseous_15-512x512.png",
"/planetTextures/Gaseous/Gaseous_16-512x512.png",
"/planetTextures/Gaseous/Gaseous_17-512x512.png",
"/planetTextures/Gaseous/Gaseous_18-512x512.png",
"/planetTextures/Gaseous/Gaseous_19-512x512.png",
"/planetTextures/Gaseous/Gaseous_20-512x512.png",
],
grassland: [
"/planetTextures/Grassland/Grassland_01-512x512.png",
"/planetTextures/Grassland/Grassland_02-512x512.png",
"/planetTextures/Grassland/Grassland_03-512x512.png",
"/planetTextures/Grassland/Grassland_04-512x512.png",
"/planetTextures/Grassland/Grassland_05-512x512.png",
],
jungle: [
"/planetTextures/Jungle/Jungle_01-512x512.png",
"/planetTextures/Jungle/Jungle_02-512x512.png",
"/planetTextures/Jungle/Jungle_03-512x512.png",
"/planetTextures/Jungle/Jungle_04-512x512.png",
"/planetTextures/Jungle/Jungle_05-512x512.png",
],
marshy: [
"/planetTextures/Marshy/Marshy_01-512x512.png",
"/planetTextures/Marshy/Marshy_02-512x512.png",
"/planetTextures/Marshy/Marshy_03-512x512.png",
"/planetTextures/Marshy/Marshy_04-512x512.png",
"/planetTextures/Marshy/Marshy_05-512x512.png",
],
martian: [
"/planetTextures/Martian/Martian_01-512x512.png",
"/planetTextures/Martian/Martian_02-512x512.png",
"/planetTextures/Martian/Martian_03-512x512.png",
"/planetTextures/Martian/Martian_04-512x512.png",
"/planetTextures/Martian/Martian_05-512x512.png",
],
methane: [
"/planetTextures/Methane/Methane_01-512x512.png",
"/planetTextures/Methane/Methane_02-512x512.png",
"/planetTextures/Methane/Methane_03-512x512.png",
"/planetTextures/Methane/Methane_04-512x512.png",
"/planetTextures/Methane/Methane_05-512x512.png",
],
sandy: [
"/planetTextures/Sandy/Sandy_01-512x512.png",
"/planetTextures/Sandy/Sandy_02-512x512.png",
"/planetTextures/Sandy/Sandy_03-512x512.png",
"/planetTextures/Sandy/Sandy_04-512x512.png",
"/planetTextures/Sandy/Sandy_05-512x512.png",
],
snowy: [
"/planetTextures/Snowy/Snowy_01-512x512.png",
"/planetTextures/Snowy/Snowy_02-512x512.png",
"/planetTextures/Snowy/Snowy_03-512x512.png",
"/planetTextures/Snowy/Snowy_04-512x512.png",
"/planetTextures/Snowy/Snowy_05-512x512.png",
],
tundra: [
"/planetTextures/Tundra/Tundra_01-512x512.png",
"/planetTextures/Tundra/Tundra_02-512x512.png",
"/planetTextures/Tundra/Tundra_03-512x512.png",
"/planetTextures/Tundra/Tundra_04-512x512.png",
"/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
],
};
/**
* Get a random texture from a specific planet type
*/
export function getRandomTextureByType(type: keyof typeof PLANET_TEXTURES_BY_TYPE): string {
const textures = PLANET_TEXTURES_BY_TYPE[type];
return textures[Math.floor(Math.random() * textures.length)];
}

6
src/setLoadingMessage.ts Normal file
View File

@ -0,0 +1,6 @@
let loadingDiv: HTMLElement = document.querySelector('#loadingDiv') as HTMLElement;
export default function setLoadingMessage(message:string) {
if (loadingDiv) {
loadingDiv.innerText = message;
}
}

View File

@ -10,7 +10,6 @@ import {
PhysicsMotionType,
PhysicsShapeType,
SceneLoader,
Sound,
SpotLight,
StandardMaterial,
TransformNode,
@ -20,6 +19,7 @@ import {
WebXRControllerComponent,
WebXRInputSource
} from "@babylonjs/core";
import type {AudioEngineV2, StaticSound} from "@babylonjs/core";
import {DefaultScene} from "./defaultScene";
const MAX_FORWARD_THRUST = 40;
@ -58,16 +58,20 @@ export class Ship {
private _forwardNode: TransformNode;
private _rotationNode: TransformNode;
private _glowLayer: GlowLayer;
private _primaryThrustVectorSound: Sound;
private _secondaryThrustVectorSound: Sound;
private _shot: Sound;
private _primaryThrustVectorSound: StaticSound;
private _secondaryThrustVectorSound: StaticSound;
private _shot: StaticSound;
private _primaryThrustPlaying: boolean = false;
private _secondaryThrustPlaying: boolean = false;
private _shooting: boolean = false;
private _camera: FreeCamera;
private _ammoBaseMesh: AbstractMesh;
private _controllerMode: ControllerStickMode;
private _active = false;
constructor(mode: ControllerStickMode = ControllerStickMode.BEGINNER) {
this._controllerMode = mode
private _audioEngine: AudioEngineV2;
constructor(mode: ControllerStickMode = ControllerStickMode.BEGINNER, audioEngine?: AudioEngineV2) {
this._controllerMode = mode;
this._audioEngine = audioEngine;
this.setup();
this.initialize();
}
@ -75,8 +79,25 @@ export class Ship {
this._controllerMode = mode;
}
private async initializeSounds() {
if (!this._audioEngine) return;
this._primaryThrustVectorSound = await this._audioEngine.createSoundAsync("thrust", "/thrust5.mp3", {
loop: true,
volume: .2
});
this._secondaryThrustVectorSound = await this._audioEngine.createSoundAsync("thrust2", "/thrust5.mp3", {
loop: true,
volume: 0.5
});
this._shot = await this._audioEngine.createSoundAsync("shot", "/shot.mp3", {
loop: false,
volume: 0.5
});
}
private shoot() {
this._shot.play();
this._shot?.play();
const ammo = new InstancedMesh("ammo", this._ammoBaseMesh as Mesh);
ammo.parent = this._ship;
ammo.position.y = 2;
@ -113,28 +134,22 @@ export class Ship {
this._ship = new TransformNode("ship", DefaultScene.MainScene);
this._glowLayer = new GlowLayer('bullets', DefaultScene.MainScene);
this._glowLayer.intensity = 1;
this._primaryThrustVectorSound = new Sound("thrust", "/thrust5.mp3", DefaultScene.MainScene, null, {
loop: true,
autoplay: false
});
this._secondaryThrustVectorSound = new Sound("thrust2", "/thrust5.mp3", DefaultScene.MainScene, null, {
loop: true,
autoplay: false,
volume: .5
});
this._shot = new Sound("shot", "/shot.mp3", DefaultScene.MainScene, null,
{loop: false, autoplay: false, volume: .5});
// Create sounds asynchronously if audio engine is available
if (this._audioEngine) {
this.initializeSounds();
}
this._ammoMaterial = new StandardMaterial("ammoMaterial", DefaultScene.MainScene);
this._ammoMaterial.emissiveColor = new Color3(1, 1, 0);
this._ammoBaseMesh = MeshBuilder.CreateCapsule("bullet", {radius: .1, height: 2.5}, DefaultScene.MainScene);
this._ammoBaseMesh.material = this._ammoMaterial;
this._ammoBaseMesh.setEnabled(false);
const light = new DirectionalLight("light", new Vector3(.1, -1, 0), DefaultScene.MainScene);
//const light = new DirectionalLight("light", new Vector3(.1, -1, 0), DefaultScene.MainScene);
const landingLight = new SpotLight("landingLight", new Vector3(0, 0, 0), new Vector3(0, -.5, .5), 1.5, .5, DefaultScene.MainScene);
landingLight.parent = this._ship;
landingLight.position.z = 5;
//const landingLight = new SpotLight("landingLight", new Vector3(0, 0, 0), new Vector3(0, -.5, .5), 1.5, .5, DefaultScene.MainScene);
// landingLight.parent = this._ship;
// landingLight.position.z = 5;
const agg = new PhysicsAggregate(this._ship, PhysicsShapeType.BOX, {
mass: 100,
extents: new Vector3(4, 4, 7.4),
@ -165,6 +180,7 @@ export class Ship {
sight.parent = this._ship
const signtMaterial = new StandardMaterial("sightMaterial", DefaultScene.MainScene);
signtMaterial.emissiveColor = Color3.Yellow();
signtMaterial.ambientColor = Color3.Yellow();
sight.material = signtMaterial;
sight.position = new Vector3(0, 2, 125);
let i = Date.now();
@ -218,14 +234,18 @@ export class Ship {
//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.isPlaying) {
if (this._primaryThrustVectorSound && !this._primaryThrustPlaying) {
this._primaryThrustVectorSound.play();
this._primaryThrustPlaying = true;
}
if (this._primaryThrustVectorSound) {
this._primaryThrustVectorSound.volume = Math.abs(this._leftStickVector.y);
}
this._primaryThrustVectorSound.setVolume(Math.abs(this._leftStickVector.y));
this._forwardValue += this._leftStickVector.y * .8;
} else {
if (this._primaryThrustVectorSound.isPlaying) {
this._primaryThrustVectorSound.pause();
if (this._primaryThrustVectorSound && this._primaryThrustPlaying) {
this._primaryThrustVectorSound.stop();
this._primaryThrustPlaying = false;
}
this._forwardValue = decrementValue(this._forwardValue, .98);
}
@ -245,13 +265,17 @@ export class Ship {
Math.abs(this._leftStickVector.x);
if (thrust2 > .01) {
if (!this._secondaryThrustVectorSound.isPlaying) {
if (this._secondaryThrustVectorSound && !this._secondaryThrustPlaying) {
this._secondaryThrustVectorSound.play();
this._secondaryThrustPlaying = true;
}
if (this._secondaryThrustVectorSound) {
this._secondaryThrustVectorSound.volume = thrust2 * .4;
}
this._secondaryThrustVectorSound.setVolume(thrust2 * .4);
} else {
if (this._secondaryThrustVectorSound.isPlaying) {
this._secondaryThrustVectorSound.pause();
if (this._secondaryThrustVectorSound && this._secondaryThrustPlaying) {
this._secondaryThrustVectorSound.stop();
this._secondaryThrustPlaying = false;
}
}
@ -260,6 +284,12 @@ export class Ship {
}
private controllerCallback = (controllerEvent: ControllerEvent) => {
// Log first few events to verify they're firing
if (Math.random() < 0.01) { // Only log 1% to avoid spam
console.log('Controller event:', controllerEvent.type, controllerEvent.hand,
controllerEvent.type === 'thumbstick' ? controllerEvent.axisData : controllerEvent.value);
}
if (controllerEvent.type == 'thumbstick') {
if (controllerEvent.hand == 'left') {
this._leftStickVector.x = controllerEvent.axisData.x;
@ -365,23 +395,50 @@ export class Ship {
private _rightInputSource: WebXRInputSource;
public addController(controller: WebXRInputSource) {
console.log('Ship.addController called for:', controller.inputSource.handedness);
if (controller.inputSource.handedness == "left") {
console.log('Adding left controller');
this._leftInputSource = controller;
this._leftInputSource.onMotionControllerInitObservable.add((motionController) => {
console.log('Left motion controller initialized:', motionController.handness);
this.mapMotionController(motionController);
});
// Check if motion controller is already initialized
if (controller.motionController) {
console.log('Left motion controller already initialized, mapping now');
this.mapMotionController(controller.motionController);
}
}
if (controller.inputSource.handedness == "right") {
console.log('Adding right controller');
this._rightInputSource = controller;
this._rightInputSource.onMotionControllerInitObservable.add((motionController) => {
console.log('Right motion controller initialized:', motionController.handness);
this.mapMotionController(motionController);
});
// Check if motion controller is already initialized
if (controller.motionController) {
console.log('Right motion controller already initialized, mapping now');
this.mapMotionController(controller.motionController);
}
}
}
private mapMotionController(controller: WebXRAbstractMotionController) {
console.log('Mapping motion controller:', controller.handness, 'Profile:', controller.profileId);
controllerComponents.forEach((component) => {
const comp = controller.components[component];
if (!comp) {
console.log(` Component ${component} not found on ${controller.handness} controller`);
return;
}
console.log(` Found component ${component} on ${controller.handness} controller`);
const observable = this._controllerObservable;
if (comp && comp.onAxisValueChangedObservable) {

View File

@ -9,13 +9,15 @@ import {
PBRMaterial,
PhysicsAggregate, PhysicsBody,
PhysicsMotionType,
PhysicsShapeType,
PhysicsShapeType, PhysicsViewer,
SceneLoader,
Vector3
} from "@babylonjs/core";
import {DefaultScene} from "./defaultScene";
import {ScoreEvent} from "./scoreboard";
import {Debug} from "@babylonjs/core/Legacy/legacy";
let _particleData: any = null;
export class Rock {
private _rockMesh: AbstractMesh;
constructor(mesh: AbstractMesh) {
@ -34,7 +36,7 @@ export class RockFactory {
private static _rockMaterial: PBRMaterial;
private static _explosionPool: ParticleSystemSet[] = [];
private static _poolSize: number = 10;
private static _viewer: PhysicsViewer = null;
public static async init() {
// Pre-create explosion particle systems for pooling
console.log("Pre-creating explosion particle systems...");
@ -84,7 +86,7 @@ export class RockFactory {
score: Observable<ScoreEvent>): Promise<Rock> {
const rock = new InstancedMesh("asteroid-" +i, this._rockMesh as Mesh);
console.log(rock.id);
rock.scaling = size;
rock.position = position;
//rock.material = this._rockMaterial;
@ -92,11 +94,18 @@ export class RockFactory {
rock.id = "asteroid-" + i;
rock.metadata = {type: 'asteroid'};
rock.setEnabled(true);
console.log(rock.getBoundingInfo());
const agg = new PhysicsAggregate(rock, PhysicsShapeType.CONVEX_HULL, {
mass: 10000,
restitution: .5,
}, DefaultScene.MainScene);
const body =agg.body;
if (!this._viewer) {
this._viewer = new PhysicsViewer(DefaultScene.MainScene);
}
//this._viewer.showBody(body);
body.setLinearDamping(0);
body.setMotionType(PhysicsMotionType.DYNAMIC);
body.setCollisionCallbackEnabled(true);