space-game/src/level1.ts
Michael Mainguy 37128d8fbd
All checks were successful
Build / build (push) Successful in 1m18s
Update gameplay mechanics, asteroid model, and star base physics
- Switch to asteroid4.glb model with updated material handling
- Adjust difficulty parameters: increased spawn distances (220-450m range), updated force multipliers, varied asteroid sizes
- Fix scoreboard timer display (was showing frames instead of actual seconds)
- Refactor star base to use asset container with mesh merging for better performance
- Change star base physics from STATIC to ANIMATED with collision detection enabled
- Add directional lighting in level deserializer for improved scene lighting
- Clean up commented code and optimize debug logging
- Update base.glb 3D model

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 12:26:25 -06:00

189 lines
7.7 KiB
TypeScript

import {DefaultScene} from "./defaultScene";
import type {AudioEngineV2} from "@babylonjs/core";
import {
AbstractMesh,
Color3,
DistanceConstraint,
MeshBuilder,
Observable,
PhysicsAggregate,
PhysicsMotionType,
PhysicsShapeType,
StandardMaterial,
Vector3
} from "@babylonjs/core";
import {Ship} from "./ship";
import Level from "./level";
import {Scoreboard} from "./scoreboard";
import setLoadingMessage from "./setLoadingMessage";
import {LevelConfig} from "./levelConfig";
import {LevelDeserializer} from "./levelDeserializer";
import {BackgroundStars} from "./backgroundStars";
import debugLog from './debug';
export class Level1 implements Level {
private _ship: Ship;
private _onReadyObservable: Observable<Level> = new Observable<Level>();
private _initialized: boolean = false;
private _startBase: AbstractMesh;
private _endBase: AbstractMesh;
private _scoreboard: Scoreboard;
private _levelConfig: LevelConfig;
private _audioEngine: AudioEngineV2;
private _deserializer: LevelDeserializer;
private _backgroundStars: BackgroundStars;
constructor(levelConfig: LevelConfig, audioEngine: AudioEngineV2) {
this._levelConfig = levelConfig;
this._audioEngine = audioEngine;
this._deserializer = new LevelDeserializer(levelConfig);
this._ship = new Ship(audioEngine);
this._scoreboard = new Scoreboard();
const xr = DefaultScene.XR;
debugLog('Level1 constructor - Setting up XR observables');
debugLog('XR input exists:', !!xr.input);
debugLog('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) => {
debugLog('🎮 onControllerAddedObservable FIRED for:', controller.inputSource.handedness);
this._ship.addController(controller);
});
});
this.initialize();
}
getReadyObservable(): Observable<Level> {
return this._onReadyObservable;
}
private scored: Set<string> = new Set<string>();
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
debugLog('Checking for controllers after entering XR. Count:', DefaultScene.XR.input.controllers.length);
DefaultScene.XR.input.controllers.forEach((controller, index) => {
debugLog(`Controller ${index} - handedness: ${controller.inputSource.handedness}`);
this._ship.addController(controller);
});
// Wait and check again after a delay (controllers might connect later)
debugLog('Waiting 2 seconds to check for controllers again...');
setTimeout(() => {
debugLog('After 2 second delay - controller count:', DefaultScene.XR.input.controllers.length);
DefaultScene.XR.input.controllers.forEach((controller, index) => {
debugLog(` Late controller ${index} - handedness: ${controller.inputSource.handedness}`);
});
}, 2000);
}
public dispose() {
this._startBase.dispose();
this._endBase.dispose();
if (this._backgroundStars) {
this._backgroundStars.dispose();
}
}
public async initialize() {
debugLog('Initializing level from config:', this._levelConfig.difficulty);
if (this._initialized) {
return;
}
setLoadingMessage("Loading level from configuration...");
// Use deserializer to create all entities from config
const entities = await this._deserializer.deserialize(this._scoreboard.onScoreObservable);
this._startBase = entities.startBase;
// sun and planets are already created by deserializer
// Initialize scoreboard with total asteroid count
this._scoreboard.setRemainingCount(entities.asteroids.length);
debugLog(`Initialized scoreboard with ${entities.asteroids.length} asteroids`);
// Position ship from config
const shipConfig = this._deserializer.getShipConfig();
this._ship.position = new Vector3(shipConfig.position[0], shipConfig.position[1], shipConfig.position[2]);
// Add distance constraints to asteroids (if physics enabled)
setLoadingMessage("Configuring physics constraints...");
const asteroidMeshes = entities.asteroids;
if (this._startBase.physicsBody) {
for (let i = 0; i < asteroidMeshes.length; i++) {
const asteroidMesh = asteroidMeshes[i];
if (asteroidMesh.physicsBody) {
// Calculate distance from start base
const dist = Vector3.Distance(asteroidMesh.position, this._startBase.position);
const constraint = new DistanceConstraint(dist, DefaultScene.MainScene);
// constraint.isCollisionsEnabled = true;
this._startBase.physicsBody.addConstraint(asteroidMesh.physicsBody, constraint);
}
}
}
// Create background starfield
setLoadingMessage("Creating starfield...");
this._backgroundStars = new BackgroundStars(DefaultScene.MainScene, {
count: 5000,
radius: 5000,
minBrightness: 0.3,
maxBrightness: 1.0,
pointSize: 2
});
// Set up camera follow for stars (keeps stars at infinite distance)
DefaultScene.MainScene.onBeforeRenderObservable.add(() => {
if (this._backgroundStars && DefaultScene.XR.baseExperience.camera) {
this._backgroundStars.followCamera(DefaultScene.XR.baseExperience.camera.position);
}
});
this._initialized = true;
// Notify that initialization is complete
this._onReadyObservable.notifyObservers(this);
}
private createTarget(i: number) {
const target = MeshBuilder.CreateTorus("target" + i, {diameter: 10, tessellation: 72}, DefaultScene.MainScene);
const targetLOD = MeshBuilder.CreateTorus("target" + i, {
diameter: 50,
tessellation: 10
}, DefaultScene.MainScene);
targetLOD.parent = target;
target.addLODLevel(300, targetLOD);
const material = new StandardMaterial("material", DefaultScene.MainScene);
material.diffuseColor = new Color3(1, 0, 0);
material.alpha = .9;
target.material = material;
target.position = Vector3.Random(-1000, 1000);
target.rotation = Vector3.Random(0, Math.PI * 2);
const disc = MeshBuilder.CreateDisc("disc-" + i, {radius: 2, tessellation: 72}, DefaultScene.MainScene);
const discMaterial = new StandardMaterial("material", DefaultScene.MainScene);
discMaterial.ambientColor = new Color3(.1, 1, .1);
discMaterial.alpha = .2;
target.addLODLevel(200, null);
disc.material = discMaterial;
disc.parent = target;
disc.rotation.x = -Math.PI / 2;
const agg = new PhysicsAggregate(disc, PhysicsShapeType.MESH, {mass: 0}, DefaultScene.MainScene);
agg.body.setMotionType(PhysicsMotionType.STATIC);
agg.shape.isTrigger = true;
}
}