diff --git a/index.html b/index.html
index 460cd6a..ba8849f 100644
--- a/index.html
+++ b/index.html
@@ -57,6 +57,10 @@
+
+
+ Create New Level
diff --git a/public/styles.css b/public/styles.css
index 6d242f9..9d770ef 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -31,9 +31,7 @@ body {
#mainDiv {
position: absolute;
display: block;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
+ top: 48px;
z-index: 1000;
overflow: scroll;
}
@@ -185,6 +183,26 @@ body {
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
+.test-level-button {
+ background: linear-gradient(135deg, #FF6B6B 0%, #C92A2A 100%);
+ color: white;
+ border: 2px solid rgba(255, 107, 107, 0.5);
+ padding: 12px 30px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 1.1em;
+ font-weight: bold;
+ transition: all 0.3s;
+ margin-bottom: 15px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
+}
+
+.test-level-button:hover {
+ transform: scale(1.05);
+ box-shadow: 0 6px 20px rgba(255, 107, 107, 0.6);
+ border-color: rgba(255, 107, 107, 0.8);
+}
+
.editor-link,
.settings-link {
position: absolute;
diff --git a/src/explosionManager.ts b/src/explosionManager.ts
index 4720929..ae31ff2 100644
--- a/src/explosionManager.ts
+++ b/src/explosionManager.ts
@@ -55,7 +55,12 @@ export class ExplosionManager {
* @returns Array of sphere mesh objects
*/
private splitIntoSeparateMeshes(mesh: Mesh, pieces: number = 32): Mesh[] {
- console.log(`Creating ${pieces} sphere debris pieces`);
+ console.log(`[ExplosionManager] Creating ${pieces} sphere debris pieces`);
+ console.log('[ExplosionManager] Base mesh info:', {
+ position: mesh.position.toString(),
+ scaling: mesh.scaling.toString(),
+ hasMaterial: !!mesh.material
+ });
const meshPieces: Mesh[] = [];
const basePosition = mesh.position.clone();
@@ -65,41 +70,62 @@ export class ExplosionManager {
const material = mesh.material?.clone('debris-material');
if (material) {
//(material as any).emissiveColor = Color3.Yellow();
+ console.log('[ExplosionManager] Material cloned successfully');
+ } else {
+ console.warn('[ExplosionManager] WARNING: No material on base mesh');
}
// Create sphere debris scattered around the original mesh position
const avgScale = (baseScale.x + baseScale.y + baseScale.z) / 3;
const debrisSize = avgScale * 0.3; // Size relative to asteroid
+ console.log('[ExplosionManager] Debris parameters:', {
+ avgScale,
+ debrisSize,
+ offsetRadius: avgScale * 0.5
+ });
+
for (let i = 0; i < pieces; i++) {
- // Create a small sphere for debris
- const sphere = MeshBuilder.CreateIcoSphere(
- `${mesh.name}_debris_${i}`,
- {
- radius: debrisSize,
- subdivisions: 2
- }, DefaultScene.MainScene
- );
+ try {
+ // Create a small sphere for debris
+ const sphere = MeshBuilder.CreateIcoSphere(
+ `${mesh.name}_debris_${i}`,
+ {
+ radius: debrisSize,
+ subdivisions: 2
+ }, DefaultScene.MainScene
+ );
- // Position spheres in a small cluster around the original position
- const offsetRadius = avgScale * 0.5;
- const angle1 = (i / pieces) * Math.PI * 2;
- const angle2 = Math.random() * Math.PI;
+ // Position spheres in a small cluster around the original position
+ const offsetRadius = avgScale * 0.5;
+ const angle1 = (i / pieces) * Math.PI * 2;
+ const angle2 = Math.random() * Math.PI;
- sphere.position = new Vector3(
- basePosition.x + Math.sin(angle2) * Math.cos(angle1) * offsetRadius,
- basePosition.y + Math.sin(angle2) * Math.sin(angle1) * offsetRadius,
- basePosition.z + Math.cos(angle2) * offsetRadius
- );
+ sphere.position = new Vector3(
+ basePosition.x + Math.sin(angle2) * Math.cos(angle1) * offsetRadius,
+ basePosition.y + Math.sin(angle2) * Math.sin(angle1) * offsetRadius,
+ basePosition.z + Math.cos(angle2) * offsetRadius
+ );
- sphere.material = material;
- sphere.isVisible = true;
- sphere.setEnabled(true);
+ sphere.material = material;
+ sphere.isVisible = true;
+ sphere.setEnabled(true);
- meshPieces.push(sphere);
+ meshPieces.push(sphere);
+ } catch (error) {
+ console.error(`[ExplosionManager] ERROR creating debris piece ${i}:`, error);
+ }
}
- console.log(`Created ${meshPieces.length} sphere debris pieces`);
+ console.log(`[ExplosionManager] Successfully created ${meshPieces.length}/${pieces} sphere debris pieces`);
+ if (meshPieces.length > 0) {
+ console.log('[ExplosionManager] First piece sample:', {
+ name: meshPieces[0].name,
+ position: meshPieces[0].position.toString(),
+ isVisible: meshPieces[0].isVisible,
+ isEnabled: meshPieces[0].isEnabled()
+ });
+ }
return meshPieces;
}
@@ -108,20 +134,33 @@ export class ExplosionManager {
* @param mesh The mesh to explode (will be cloned internally)
*/
public playExplosion(mesh: AbstractMesh): void {
+ console.log('[ExplosionManager] playExplosion called');
+ console.log('[ExplosionManager] Input mesh:', {
+ name: mesh.name,
+ id: mesh.id,
+ isInstancedMesh: !!(mesh as any).sourceMesh,
+ position: mesh.position.toString(),
+ scaling: mesh.scaling.toString()
+ });
+
// Get the source mesh if this is an instanced mesh
let sourceMesh: Mesh;
if ((mesh as any).sourceMesh) {
sourceMesh = (mesh as any).sourceMesh as Mesh;
+ console.log('[ExplosionManager] Using source mesh from instance:', sourceMesh.name);
} else {
sourceMesh = mesh as Mesh;
+ console.log('[ExplosionManager] Using mesh directly (not instanced)');
}
// Clone the source mesh so we don't affect the original
+ console.log('[ExplosionManager] Cloning mesh...');
const meshToExplode = sourceMesh.clone("exploding-" + mesh.name, null, true, false);
if (!meshToExplode) {
- console.warn("Failed to clone mesh for explosion");
+ console.error('[ExplosionManager] ERROR: Failed to clone mesh for explosion');
return;
}
+ console.log('[ExplosionManager] Mesh cloned successfully');
// Apply the instance's transformation to the cloned mesh
meshToExplode.position = mesh.getAbsolutePosition().clone();
@@ -134,77 +173,130 @@ export class ExplosionManager {
// Check if mesh has proper geometry
if (!meshToExplode.getTotalVertices || meshToExplode.getTotalVertices() === 0) {
- console.warn("Mesh has no vertices, cannot explode");
+ console.error('[ExplosionManager] ERROR: Mesh has no vertices, cannot explode');
meshToExplode.dispose();
return;
}
- console.log(`Exploding mesh: ${meshToExplode.name}, vertices: ${meshToExplode.getTotalVertices()}`);
+ console.log(`[ExplosionManager] Mesh ready for explosion:`, {
+ name: meshToExplode.name,
+ vertices: meshToExplode.getTotalVertices(),
+ position: meshToExplode.position.toString(),
+ scaling: meshToExplode.scaling.toString()
+ });
// Split the mesh into separate mesh objects (MeshExploder requirement)
+ console.log('[ExplosionManager] Splitting mesh into pieces...');
const meshPieces = this.splitIntoSeparateMeshes(meshToExplode, 12);
if (meshPieces.length === 0) {
- console.warn("Failed to split mesh into pieces");
+ console.error('[ExplosionManager] ERROR: Failed to split mesh into pieces');
meshToExplode.dispose();
return;
}
// Original mesh is no longer needed - the pieces replace it
+ console.log('[ExplosionManager] Disposing original cloned mesh');
meshToExplode.dispose();
// Create the exploder with the array of separate meshes
// The second parameter is optional - it's the center mesh to explode from
// If not provided, MeshExploder will auto-calculate the center
- const exploder = new MeshExploder(meshPieces);
+ console.log('[ExplosionManager] Creating MeshExploder...');
+ try {
+ const exploder = new MeshExploder(meshPieces);
+ console.log('[ExplosionManager] MeshExploder created successfully');
- console.log(`Starting explosion animation for ${meshPieces.length} mesh pieces`);
-
- // Animate the explosion by calling explode() each frame with increasing values
- const startTime = Date.now();
- const animationDuration = this.config.duration;
- const maxForce = this.config.explosionForce;
-
- const animate = () => {
- const elapsed = Date.now() - startTime;
- const progress = Math.min(elapsed / animationDuration, 1.0);
-
- // Calculate current explosion value (0 to maxForce)
- const currentValue = progress * maxForce;
- exploder.explode(currentValue);
-
- // Animate debris size to zero (1.0 to 0.0)
- const scale = 1.0 - progress;
- meshPieces.forEach(piece => {
- piece.scaling.set(scale, scale, scale);
+ console.log(`[ExplosionManager] Starting explosion animation:`, {
+ pieceCount: meshPieces.length,
+ duration: this.config.duration,
+ maxForce: this.config.explosionForce
});
- // Continue animation if not complete
- if (progress < 1.0) {
- requestAnimationFrame(animate);
- } else {
- // Animation complete - clean up
- console.log(`Explosion animation complete, cleaning up`);
- this.cleanupExplosion(meshPieces);
- }
- };
+ // Animate the explosion by calling explode() each frame with increasing values
+ const startTime = Date.now();
+ const animationDuration = this.config.duration;
+ const maxForce = this.config.explosionForce;
+ let frameCount = 0;
- // Start the animation
- animate();
+ const animate = () => {
+ const elapsed = Date.now() - startTime;
+ const progress = Math.min(elapsed / animationDuration, 1.0);
+
+ // Calculate current explosion value (0 to maxForce)
+ const currentValue = progress * maxForce;
+
+ try {
+ exploder.explode(currentValue);
+ } catch (error) {
+ console.error('[ExplosionManager] ERROR in explode():', error);
+ }
+
+ // Animate debris size to zero (1.0 to 0.0)
+ const scale = 1.0 - progress;
+ meshPieces.forEach(piece => {
+ if (piece && !piece.isDisposed()) {
+ piece.scaling.set(scale, scale, scale);
+ }
+ });
+
+ frameCount++;
+
+ // Log every 15 frames (approximately every 250ms at 60fps)
+ if (frameCount % 15 === 0 || frameCount === 1) {
+ console.log(`[ExplosionManager] Animation frame ${frameCount}:`, {
+ elapsed: `${elapsed}ms`,
+ progress: progress.toFixed(3),
+ currentValue: currentValue.toFixed(2),
+ scale: scale.toFixed(3),
+ piecesAlive: meshPieces.filter(p => !p.isDisposed()).length
+ });
+ }
+
+ // Continue animation if not complete
+ if (progress < 1.0) {
+ requestAnimationFrame(animate);
+ } else {
+ // Animation complete - clean up
+ console.log(`[ExplosionManager] Animation complete after ${frameCount} frames, cleaning up`);
+ this.cleanupExplosion(meshPieces);
+ }
+ };
+
+ // Start the animation
+ console.log('[ExplosionManager] Starting animation loop...');
+ animate();
+ } catch (error) {
+ console.error('[ExplosionManager] ERROR creating MeshExploder:', error);
+ // Clean up pieces if exploder failed
+ meshPieces.forEach(piece => {
+ if (piece && !piece.isDisposed()) {
+ piece.dispose();
+ }
+ });
+ }
}
/**
* Clean up explosion meshes
*/
private cleanupExplosion(meshPieces: Mesh[]): void {
+ console.log('[ExplosionManager] Starting cleanup of explosion meshes...');
+
+ let disposedCount = 0;
// Dispose all the mesh pieces
- meshPieces.forEach(mesh => {
+ meshPieces.forEach((mesh, index) => {
if (mesh && !mesh.isDisposed()) {
- mesh.dispose();
+ try {
+ mesh.dispose();
+ disposedCount++;
+ } catch (error) {
+ console.error(`[ExplosionManager] ERROR disposing piece ${index}:`, error);
+ }
}
});
- console.log(`Explosion cleaned up - disposed ${meshPieces.length} pieces`);
+ console.log(`[ExplosionManager] Cleanup complete - disposed ${disposedCount}/${meshPieces.length} pieces`);
}
/**
diff --git a/src/main.ts b/src/main.ts
index 853e446..d884192 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,28 +1,31 @@
-import {AudioEngineV2, DirectionalLight} from "@babylonjs/core";
import {
+ AudioEngineV2,
Color3,
CreateAudioEngineAsync,
+ DirectionalLight,
Engine,
HavokPlugin,
ParticleHelper,
Scene,
+ ScenePerformancePriority,
Vector3,
WebGPUEngine,
WebXRDefaultExperience,
- WebXRFeatureName
+ WebXRFeatureName, WebXRFeaturesManager
} from "@babylonjs/core";
import '@babylonjs/loaders';
import HavokPhysics from "@babylonjs/havok";
import {DefaultScene} from "./defaultScene";
import {Level1} from "./level1";
+import {TestLevel} from "./testLevel";
import Demo from "./demo";
import Level from "./level";
import setLoadingMessage from "./setLoadingMessage";
import {RockFactory} from "./rockFactory";
import {ControllerDebug} from "./controllerDebug";
import {router, showView} from "./router";
-import {populateLevelSelector, hasSavedLevels} from "./levelSelector";
+import {hasSavedLevels, populateLevelSelector} from "./levelSelector";
import {LevelConfig} from "./levelConfig";
import {generateDefaultLevels} from "./levelEditor";
@@ -79,11 +82,85 @@ export class Main {
}, 500);
});
});
+
+ // Listen for test level button click
+ window.addEventListener('DOMContentLoaded', () => {
+ console.log('[Main] DOMContentLoaded fired, looking for test button...');
+ const testLevelBtn = document.querySelector('#testLevelBtn');
+ console.log('[Main] Test button found:', !!testLevelBtn);
+
+ if (testLevelBtn) {
+ testLevelBtn.addEventListener('click', async () => {
+ console.log('[Main] ========== TEST LEVEL BUTTON CLICKED ==========');
+
+ // Show loading UI
+ const mainDiv = document.querySelector('#mainDiv');
+ const levelSelect = document.querySelector('#levelSelect') as HTMLElement;
+ console.log('[Main] mainDiv exists:', !!mainDiv);
+ console.log('[Main] levelSelect exists:', !!levelSelect);
+
+ if (levelSelect) {
+ levelSelect.style.display = 'none';
+ console.log('[Main] levelSelect hidden');
+ }
+ setLoadingMessage("Initializing Test Scene...");
+
+ // Unlock audio engine on user interaction
+ if (this._audioEngine) {
+ console.log('[Main] Unlocking audio engine...');
+ await this._audioEngine.unlockAsync();
+ console.log('[Main] Audio engine unlocked');
+ }
+
+ // Create test level
+ console.log('[Main] Creating TestLevel...');
+ this._currentLevel = new TestLevel(this._audioEngine);
+ console.log('[Main] TestLevel created:', !!this._currentLevel);
+
+ // Wait for level to be ready
+ console.log('[Main] Registering ready observable...');
+ this._currentLevel.getReadyObservable().add(() => {
+ console.log('[Main] ========== TEST LEVEL READY OBSERVABLE FIRED ==========');
+ setLoadingMessage("Test Scene Ready! Entering VR...");
+ console.log('[Main] Setting timeout to enter VR...');
+
+ // Small delay to show message
+ setTimeout(() => {
+ console.log('[Main] Timeout fired, removing mainDiv and calling play()');
+ if (mainDiv) {
+ mainDiv.remove();
+ console.log('[Main] mainDiv removed');
+ }
+ console.log('[Main] About to call this.play()...');
+ this.play();
+ }, 500);
+ });
+ console.log('[Main] Ready observable registered');
+
+ // Now initialize the level (after observable is registered)
+ console.log('[Main] Calling TestLevel.initialize()...');
+ await this._currentLevel.initialize();
+ console.log('[Main] TestLevel.initialize() completed');
+ });
+ console.log('[Main] Click listener added to test button');
+ } else {
+ console.warn('[Main] Test level button not found in DOM');
+ }
+ });
}
private _started = false;
public async play() {
+ console.log('[Main] play() called');
+ console.log('[Main] Current level exists:', !!this._currentLevel);
this._gameState = GameState.PLAY;
- await this._currentLevel.play();
+
+ if (this._currentLevel) {
+ console.log('[Main] Calling level.play()...');
+ await this._currentLevel.play();
+ console.log('[Main] level.play() completed');
+ } else {
+ console.error('[Main] ERROR: No current level to play!');
+ }
}
public demo() {
this._gameState = GameState.DEMO;
@@ -97,11 +174,11 @@ export class Main {
disableTeleportation: true,
disableNearInteraction: true,
disableHandTracking: true,
- disableDefaultUI: true,
+ disableDefaultUI: true
});
- DefaultScene.XR.baseExperience.featuresManager.enableFeature(WebXRFeatureName.LAYERS, "stable",
- {preferMultiviewOnInit: true});
+ console.log(WebXRFeaturesManager.GetAvailableFeatures());
+ //DefaultScene.XR.baseExperience.featuresManager.enableFeature(WebXRFeatureName.LAYERS, "latest", {preferMultiviewOnInit: true});
setLoadingMessage("Get Ready!");
@@ -124,16 +201,20 @@ export class Main {
if (webGpu) {
this._engine = new WebGPUEngine(canvas);
+ console.log("Webgpu enabled");
await (this._engine as WebGPUEngine).initAsync();
} else {
+ console.log("Standard WebGL enabled");
this._engine = new Engine(canvas, true);
}
+
this._engine.setHardwareScalingLevel(1 / window.devicePixelRatio);
window.onresize = () => {
this._engine.resize();
}
DefaultScene.DemoScene = new Scene(this._engine);
DefaultScene.MainScene = new Scene(this._engine);
+
DefaultScene.MainScene.ambientColor = new Color3(0,0,0);
DefaultScene.MainScene.clearColor = new Color3(0, 0, 0).toColor4();
@@ -175,7 +256,7 @@ export class Main {
//DefaultScene.MainScene.ambientColor = new Color3(.1, .1, .1);
const light = new DirectionalLight("dirLight", new Vector3(-1, -2, -1), DefaultScene.MainScene);
DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin);
- DefaultScene.MainScene.getPhysicsEngine().setTimeStep(1/30);
+ DefaultScene.MainScene.getPhysicsEngine().setTimeStep(1/60);
DefaultScene.MainScene.getPhysicsEngine().setSubTimeStep(5);
DefaultScene.MainScene.collisionsEnabled = true;
diff --git a/src/materialFactory.ts b/src/materialFactory.ts
index 58d01e6..a9197d5 100644
--- a/src/materialFactory.ts
+++ b/src/materialFactory.ts
@@ -154,6 +154,7 @@ export class MaterialFactory {
material.disableLighting = true;
material.roughness = 1;
material.specularColor = Color3.Black();
+ material.freeze();
return material;
}
diff --git a/src/rockFactory.ts b/src/rockFactory.ts
index 393bbc1..1874b67 100644
--- a/src/rockFactory.ts
+++ b/src/rockFactory.ts
@@ -77,6 +77,7 @@ export class RockFactory {
DefaultScene.MainScene,
this._originalMaterial
) as PBRMaterial;
+ this._rockMaterial.freeze();
this._rockMesh.material = this._rockMaterial;
importMesh.meshes[1].dispose(false, true);
@@ -119,22 +120,37 @@ export class RockFactory {
body.setCollisionCallbackEnabled(true);
body.getCollisionObservable().add((eventData) => {
if (eventData.type == 'COLLISION_STARTED') {
+ console.log('[RockFactory] Collision detected:', {
+ collidedWith: eventData.collidedAgainst.transformNode.id,
+ asteroidName: eventData.collider.transformNode.name
+ });
+
if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
+ console.log('[RockFactory] ASTEROID HIT! Triggering explosion...');
score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"});
// Get the asteroid mesh before disposing
const asteroidMesh = eventData.collider.transformNode as AbstractMesh;
+ console.log('[RockFactory] Asteroid mesh to explode:', {
+ name: asteroidMesh.name,
+ id: asteroidMesh.id,
+ position: asteroidMesh.position.toString()
+ });
// Play explosion using ExplosionManager (clones mesh internally)
+ console.log('[RockFactory] Calling ExplosionManager.playExplosion()...');
RockFactory._explosionManager.playExplosion(asteroidMesh);
+ console.log('[RockFactory] Explosion call completed');
// Now dispose the physics objects and original mesh
+ console.log('[RockFactory] Disposing physics objects and meshes...');
eventData.collider.shape.dispose();
eventData.collider.transformNode.dispose();
eventData.collider.dispose();
eventData.collidedAgainst.shape.dispose();
eventData.collidedAgainst.transformNode.dispose();
eventData.collidedAgainst.dispose();
+ console.log('[RockFactory] Disposal complete');
}
}
});
diff --git a/src/ship.ts b/src/ship.ts
index 6728ce6..2d9021a 100644
--- a/src/ship.ts
+++ b/src/ship.ts
@@ -343,10 +343,7 @@ 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') {
diff --git a/src/testLevel.ts b/src/testLevel.ts
new file mode 100644
index 0000000..1d650a5
--- /dev/null
+++ b/src/testLevel.ts
@@ -0,0 +1,232 @@
+import { DefaultScene } from "./defaultScene";
+import {
+ Color3,
+ DirectionalLight,
+ MeshBuilder,
+ Observable,
+ StandardMaterial,
+ Vector3
+} from "@babylonjs/core";
+import type { AudioEngineV2 } from "@babylonjs/core";
+import Level from "./level";
+
+/**
+ * Minimal test level with just a box and a light for debugging
+ */
+export class TestLevel implements Level {
+ private _onReadyObservable: Observable
= new Observable();
+ private _initialized: boolean = false;
+ private _audioEngine: AudioEngineV2;
+ private _boxCreationInterval: NodeJS.Timeout | null = null;
+ private _totalBoxesCreated: number = 0;
+ private _boxesPerIteration: number = 1;
+
+ constructor(audioEngine: AudioEngineV2) {
+ this._audioEngine = audioEngine;
+ console.log('[TestLevel] Constructor called');
+ // Don't call initialize here - let Main call it after registering the observable
+ }
+
+ getReadyObservable(): Observable {
+ return this._onReadyObservable;
+ }
+
+ public async play() {
+ console.log('[TestLevel] play() called - entering XR');
+ console.log('[TestLevel] XR available:', !!DefaultScene.XR);
+ console.log('[TestLevel] XR baseExperience:', !!DefaultScene.XR?.baseExperience);
+
+ try {
+ // Enter XR mode
+ const xr = await DefaultScene.XR.baseExperience.enterXRAsync('immersive-vr', 'local-floor');
+ console.log('[TestLevel] XR mode entered successfully');
+ console.log('[TestLevel] XR session:', xr);
+ console.log('[TestLevel] Camera position:', DefaultScene.XR.baseExperience.camera.position.toString());
+ this.startBoxCreation();
+ } catch (error) {
+ console.error('[TestLevel] ERROR entering XR:', error);
+ }
+ }
+
+ public dispose() {
+ console.log('[TestLevel] dispose() called');
+
+ // Stop box creation timer
+ if (this._boxCreationInterval) {
+ clearInterval(this._boxCreationInterval);
+ this._boxCreationInterval = null;
+ console.log('[TestLevel] Box creation timer stopped');
+ }
+ }
+
+ /**
+ * Create a box at the specified position with the specified color
+ */
+ private createBox(position: Vector3, color: Color3, name?: string): void {
+ const box = MeshBuilder.CreateBox(
+ name || `box_${this._totalBoxesCreated}`,
+ { size: 0.5 },
+ DefaultScene.MainScene
+ );
+ box.position = position;
+
+ const material = new StandardMaterial(`material_${this._totalBoxesCreated}`, DefaultScene.MainScene);
+ material.diffuseColor = color;
+ material.specularColor = new Color3(0.5, 0.5, 0.5);
+ box.material = material;
+
+ this._totalBoxesCreated++;
+ }
+
+ /**
+ * Start the box creation timer that doubles the number of boxes each iteration
+ */
+ private startBoxCreation(): void {
+ console.log('[TestLevel] Starting box creation timer...');
+
+ const createBatch = () => {
+ const boxesToCreate = Math.min(
+ this._boxesPerIteration,
+ 1000 - this._totalBoxesCreated
+ );
+
+ console.log(`[TestLevel] Creating ${boxesToCreate} boxes (total will be: ${this._totalBoxesCreated + boxesToCreate}/1000)`);
+
+ for (let i = 0; i < boxesToCreate; i++) {
+ // Random position in a 20x20x20 cube around origin
+ const position = new Vector3(
+ Math.random() * 20 - 10,
+ Math.random() * 20,
+ Math.random() * 20 - 10
+ );
+
+ // Random color
+ const color = new Color3(
+ Math.random(),
+ Math.random(),
+ Math.random()
+ );
+
+ this.createBox(position, color);
+ }
+
+ console.log(`[TestLevel] Created ${boxesToCreate} boxes. Total: ${this._totalBoxesCreated}/1000`);
+
+ // Log performance metrics
+ const fps = DefaultScene.MainScene.getEngine().getFps();
+
+ // Directly compute triangle count from all meshes
+ const totalIndices = DefaultScene.MainScene.meshes.reduce((sum, mesh) => {
+ if (mesh.isEnabled() && mesh.isVisible) {
+ return sum + mesh.getTotalIndices();
+ }
+ return sum;
+ }, 0);
+ const triangleCount = Math.floor(totalIndices / 3);
+
+ console.log(`[TestLevel] Performance Metrics:`, {
+ fps: fps.toFixed(2),
+ triangleCount: triangleCount,
+ totalIndices: totalIndices,
+ totalMeshes: DefaultScene.MainScene.meshes.length,
+ activeMeshes: DefaultScene.MainScene.meshes.filter(m => m.isEnabled() && m.isVisible).length,
+ totalBoxes: this._totalBoxesCreated
+ });
+
+ // Check if we've reached 1000 boxes
+ if (this._totalBoxesCreated >= 1000) {
+ console.log('[TestLevel] Reached 1000 boxes, stopping timer');
+ if (this._boxCreationInterval) {
+ clearInterval(this._boxCreationInterval);
+ this._boxCreationInterval = null;
+ }
+ return;
+ }
+
+ // Double the number for next iteration
+ this._boxesPerIteration *= 2;
+ };
+
+ // Create first batch immediately
+ createBatch();
+
+ // Set up interval for subsequent batches
+ this._boxCreationInterval = setInterval(createBatch, 5000);
+ }
+
+ public async initialize() {
+ console.log('[TestLevel] initialize() called');
+ console.log('[TestLevel] Scene info:', {
+ name: DefaultScene.MainScene.name,
+ meshCount: DefaultScene.MainScene.meshes.length,
+ lightCount: DefaultScene.MainScene.lights.length
+ });
+
+ if (this._initialized) {
+ console.log('[TestLevel] Already initialized, skipping');
+ return;
+ }
+
+ // Create a simple directional light
+ const light = new DirectionalLight(
+ "testLight",
+ new Vector3(-1, -2, 1),
+ DefaultScene.MainScene
+ );
+ light.intensity = 1.0;
+ console.log('[TestLevel] Created directional light:', {
+ name: light.name,
+ direction: light.direction.toString(),
+ intensity: light.intensity
+ });
+
+ // Create a simple colored box
+ const box = MeshBuilder.CreateBox(
+ "testBox",
+ { size: 2 },
+ DefaultScene.MainScene
+ );
+ box.position = new Vector3(0, 1, 5); // In front of camera
+
+ // Create a simple material
+ const material = new StandardMaterial("testMaterial", DefaultScene.MainScene);
+ material.diffuseColor = new Color3(1, 0, 0); // Red
+ material.specularColor = new Color3(0.5, 0.5, 0.5);
+ box.material = material;
+ console.log('[TestLevel] Created test box:', {
+ name: box.name,
+ position: box.position.toString(),
+ size: 2,
+ color: 'red'
+ });
+
+ // Create a ground plane for reference
+ const ground = MeshBuilder.CreateGround(
+ "testGround",
+ { width: 10, height: 10 },
+ DefaultScene.MainScene
+ );
+ ground.position.y = 0;
+
+ const groundMaterial = new StandardMaterial("groundMaterial", DefaultScene.MainScene);
+ groundMaterial.diffuseColor = new Color3(0.3, 0.3, 0.3); // Grey
+ ground.material = groundMaterial;
+ console.log('[TestLevel] Created ground plane:', {
+ name: ground.name,
+ dimensions: '10x10',
+ position: ground.position.toString()
+ });
+
+ console.log('[TestLevel] Final scene state:', {
+ totalMeshes: DefaultScene.MainScene.meshes.length,
+ totalLights: DefaultScene.MainScene.lights.length,
+ meshNames: DefaultScene.MainScene.meshes.map(m => m.name)
+ });
+
+ this._initialized = true;
+ console.log('[TestLevel] Initialization complete - scene ready for XR');
+
+ // Notify that initialization is complete
+ this._onReadyObservable.notifyObservers(this);
+ }
+}