Add physics-based collision damage, spatial audio, and synchronized audio loading
All checks were successful
Build / build (push) Successful in 1m17s
All checks were successful
Build / build (push) Successful in 1m17s
Audio Loading Improvements: - Ensure all sounds load before gameplay starts - Wrap RockFactory explosion sound in Promise for async/await support - Move background music loading from play() to initialize() in Level1 - Update loading messages to reflect audio loading progress Collision and Audio Features: - Implement energy-based collision damage using reduced mass and kinetic energy - Add ship velocity property and display on scoreboard HUD - Add collision sound effect with volume-adjusted playback (0.35) on ship impacts - Move explosion sound to RockFactory with spatial audio positioning - Configure explosion sound with exponential rolloff for better audibility Technical Changes: - Reorder audio engine initialization to load before RockFactory - Background music now preloaded and ready when play() is called - All audio assets guaranteed loaded before ready observable fires 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
31b498da7d
commit
56e900d93a
BIN
public/assets/themes/default/audio/collision.mp3
Normal file
BIN
public/assets/themes/default/audio/collision.mp3
Normal file
Binary file not shown.
BIN
public/assets/themes/default/audio/explosion.mp3
Normal file
BIN
public/assets/themes/default/audio/explosion.mp3
Normal file
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
import {DefaultScene} from "./defaultScene";
|
||||
import type {AudioEngineV2} from "@babylonjs/core";
|
||||
import type {AudioEngineV2, StaticSound} from "@babylonjs/core";
|
||||
import {
|
||||
AbstractMesh,
|
||||
Observable,
|
||||
@ -29,6 +29,7 @@ export class Level1 implements Level {
|
||||
private _backgroundStars: BackgroundStars;
|
||||
private _physicsRecorder: PhysicsRecorder;
|
||||
private _isReplayMode: boolean;
|
||||
private _backgroundMusic: StaticSound;
|
||||
|
||||
constructor(levelConfig: LevelConfig, audioEngine: AudioEngineV2, isReplayMode: boolean = false) {
|
||||
this._levelConfig = levelConfig;
|
||||
@ -78,13 +79,10 @@ export class Level1 implements Level {
|
||||
throw new Error("Cannot call play() in replay mode");
|
||||
}
|
||||
|
||||
// Create background music using AudioEngineV2
|
||||
if (this._audioEngine) {
|
||||
const background = await this._audioEngine.createSoundAsync("background", "/song1.mp3", {
|
||||
loop: true,
|
||||
volume: 0.5
|
||||
});
|
||||
background.play();
|
||||
// Play background music (already loaded during initialization)
|
||||
if (this._backgroundMusic) {
|
||||
this._backgroundMusic.play();
|
||||
debugLog('Started playing background music');
|
||||
}
|
||||
|
||||
// If XR is available and session is active, check for controllers
|
||||
@ -212,6 +210,16 @@ export class Level1 implements Level {
|
||||
});
|
||||
}
|
||||
|
||||
// Load background music before marking as ready
|
||||
if (this._audioEngine) {
|
||||
setLoadingMessage("Loading background music...");
|
||||
this._backgroundMusic = await this._audioEngine.createSoundAsync("background", "/song1.mp3", {
|
||||
loop: true,
|
||||
volume: 0.5
|
||||
});
|
||||
debugLog('Background music loaded successfully');
|
||||
}
|
||||
|
||||
this._initialized = true;
|
||||
|
||||
// Notify that initialization is complete
|
||||
|
||||
12
src/main.ts
12
src/main.ts
@ -397,15 +397,15 @@ export class Main {
|
||||
await this.setupPhysics();
|
||||
setLoadingMessage("Physics Engine Ready!");
|
||||
|
||||
setLoadingMessage("Loading Assets and animations...");
|
||||
ParticleHelper.BaseAssetsUrl = window.location.href;
|
||||
await RockFactory.init();
|
||||
setLoadingMessage("Ready!");
|
||||
|
||||
// Initialize AudioEngineV2
|
||||
// Initialize AudioEngineV2 first
|
||||
setLoadingMessage("Initializing Audio Engine...");
|
||||
this._audioEngine = await CreateAudioEngineAsync();
|
||||
|
||||
setLoadingMessage("Loading audio and visual assets...");
|
||||
ParticleHelper.BaseAssetsUrl = window.location.href;
|
||||
await RockFactory.init(this._audioEngine);
|
||||
setLoadingMessage("All assets loaded!");
|
||||
|
||||
|
||||
window.setTimeout(()=>{
|
||||
if (!this._started) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
AbstractMesh,
|
||||
AudioEngineV2,
|
||||
DistanceConstraint,
|
||||
InstancedMesh,
|
||||
Mesh,
|
||||
@ -7,7 +8,9 @@ import {
|
||||
PhysicsAggregate,
|
||||
PhysicsBody,
|
||||
PhysicsMotionType,
|
||||
PhysicsShapeType, TransformNode,
|
||||
PhysicsShapeType,
|
||||
Sound,
|
||||
TransformNode,
|
||||
Vector3
|
||||
} from "@babylonjs/core";
|
||||
import {DefaultScene} from "./defaultScene";
|
||||
@ -34,13 +37,18 @@ export class RockFactory {
|
||||
private static _asteroidMesh: AbstractMesh;
|
||||
private static _explosionManager: ExplosionManager;
|
||||
private static _orbitCenter: PhysicsAggregate;
|
||||
private static _explosionSound: Sound;
|
||||
private static _audioEngine: AudioEngineV2 | null = null;
|
||||
|
||||
public static async init() {
|
||||
public static async init(audioEngine?: AudioEngineV2) {
|
||||
if (audioEngine) {
|
||||
this._audioEngine = audioEngine;
|
||||
}
|
||||
// Initialize explosion manager
|
||||
const node = new TransformNode('orbitCenter', DefaultScene.MainScene);
|
||||
node.position = Vector3.Zero();
|
||||
this._orbitCenter = new PhysicsAggregate(node, PhysicsShapeType.SPHERE, {radius: .1, mass: 1000}, DefaultScene.MainScene );
|
||||
|
||||
this._orbitCenter.body.setMotionType(PhysicsMotionType.ANIMATED);
|
||||
this._explosionManager = new ExplosionManager(DefaultScene.MainScene, {
|
||||
duration: 800,
|
||||
explosionForce: 20.0,
|
||||
@ -48,6 +56,35 @@ export class RockFactory {
|
||||
});
|
||||
await this._explosionManager.initialize();
|
||||
|
||||
// Load explosion sound with spatial audio (only if audio engine available)
|
||||
if (this._audioEngine) {
|
||||
debugLog('[RockFactory] Loading explosion sound with AudioEngineV2...');
|
||||
|
||||
// Wrap Sound loading in a Promise to ensure it's loaded before continuing
|
||||
await new Promise<void>((resolve) => {
|
||||
this._explosionSound = new Sound(
|
||||
"explosionSound",
|
||||
"/assets/themes/default/audio/explosion.mp3",
|
||||
DefaultScene.MainScene,
|
||||
() => {
|
||||
debugLog('[RockFactory] Explosion sound loaded successfully');
|
||||
resolve();
|
||||
},
|
||||
{
|
||||
loop: false,
|
||||
autoplay: false,
|
||||
spatialSound: true,
|
||||
maxDistance: 500,
|
||||
distanceModel: "exponential",
|
||||
rolloffFactor: 1,
|
||||
volume: 5.0
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
debugLog('[RockFactory] WARNING: No audio engine provided, explosion sounds will be disabled');
|
||||
}
|
||||
|
||||
if (!this._asteroidMesh) {
|
||||
await this.loadMesh();
|
||||
}
|
||||
@ -100,12 +137,23 @@ export class RockFactory {
|
||||
|
||||
// Get the asteroid mesh before disposing
|
||||
const asteroidMesh = eventData.collider.transformNode as AbstractMesh;
|
||||
const asteroidPosition = asteroidMesh.getAbsolutePosition();
|
||||
debugLog('[RockFactory] Asteroid mesh to explode:', {
|
||||
name: asteroidMesh.name,
|
||||
id: asteroidMesh.id,
|
||||
position: asteroidMesh.position.toString()
|
||||
position: asteroidPosition.toString()
|
||||
});
|
||||
|
||||
// Play spatial explosion sound at asteroid position
|
||||
if (RockFactory._explosionSound) {
|
||||
debugLog('[RockFactory] Explosion sound exists, isReady:', RockFactory._explosionSound.isReady());
|
||||
RockFactory._explosionSound.setPosition(asteroidPosition);
|
||||
const playResult = RockFactory._explosionSound.play();
|
||||
debugLog('[RockFactory] Playing explosion sound at position:', asteroidPosition.toString(), 'play() returned:', playResult);
|
||||
} else {
|
||||
debugLog('[RockFactory] WARNING: Explosion sound not loaded!');
|
||||
}
|
||||
|
||||
// Play explosion using ExplosionManager (clones mesh internally)
|
||||
debugLog('[RockFactory] Calling ExplosionManager.playExplosion()...');
|
||||
RockFactory._explosionManager.playExplosion(asteroidMesh);
|
||||
|
||||
@ -32,6 +32,9 @@ export class Scoreboard {
|
||||
// Ship status manager
|
||||
private _shipStatus: ShipStatus;
|
||||
|
||||
// Reference to ship for velocity reading
|
||||
private _ship: any = null;
|
||||
|
||||
constructor() {
|
||||
this._shipStatus = new ShipStatus();
|
||||
|
||||
@ -64,9 +67,24 @@ export class Scoreboard {
|
||||
return this._shipStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of asteroids remaining
|
||||
*/
|
||||
public get remaining(): number {
|
||||
return this._remaining;
|
||||
}
|
||||
|
||||
public setRemainingCount(count: number) {
|
||||
this._remaining = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ship reference for velocity reading
|
||||
*/
|
||||
public setShip(ship: any): void {
|
||||
this._ship = ship;
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
const scene = DefaultScene.MainScene;
|
||||
|
||||
@ -140,7 +158,11 @@ export class Scoreboard {
|
||||
remainingText.text = 'Remaining: 0';
|
||||
|
||||
const timeRemainingText = this.createText();
|
||||
timeRemainingText.text = 'Time: 2:00';
|
||||
timeRemainingText.text = 'Time: 00:00';
|
||||
|
||||
const velocityText = this.createText();
|
||||
velocityText.text = 'Velocity: 0 m/s';
|
||||
|
||||
|
||||
const panel = new StackPanel();
|
||||
panel.isVertical = true;
|
||||
@ -151,11 +173,21 @@ export class Scoreboard {
|
||||
panel.addControl(fpsText);
|
||||
panel.addControl(hullText);
|
||||
panel.addControl(timeRemainingText);
|
||||
panel.addControl(velocityText);
|
||||
advancedTexture.addControl(panel);
|
||||
let i = 0;
|
||||
const afterRender = scene.onAfterRenderObservable.add(() => {
|
||||
scoreText.text = `Score: ${this.calculateScore()}`;
|
||||
remainingText.text = `Remaining: ${this._remaining}`;
|
||||
|
||||
// Update velocity from ship if available
|
||||
if (this._ship && this._ship.velocity) {
|
||||
const velocityMagnitude = this._ship.velocity.length();
|
||||
velocityText.text = `Velocity: ${velocityMagnitude.toFixed(1)} m/s`;
|
||||
} else {
|
||||
velocityText.text = `Velocity: 0.0 m/s`;
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - this._startTime;
|
||||
if (this._active && i++%30 == 0) {
|
||||
timeRemainingText.text = `Time: ${Math.floor(elapsed/60000).toString().padStart(2,"0")}:${(Math.floor(elapsed/1000)%60).toString().padStart(2,"0")}`;
|
||||
|
||||
54
src/ship.ts
54
src/ship.ts
@ -80,6 +80,13 @@ export class Ship {
|
||||
return this._isInLandingZone;
|
||||
}
|
||||
|
||||
public get velocity(): Vector3 {
|
||||
if (this._ship?.physicsBody) {
|
||||
return this._ship.physicsBody.getLinearVelocity();
|
||||
}
|
||||
return Vector3.Zero();
|
||||
}
|
||||
|
||||
public set position(newPosition: Vector3) {
|
||||
const body = this._ship.physicsBody;
|
||||
|
||||
@ -99,6 +106,7 @@ export class Ship {
|
||||
|
||||
public async initialize() {
|
||||
this._scoreboard = new Scoreboard();
|
||||
this._scoreboard.setShip(this); // Pass ship reference for velocity reading
|
||||
this._gameStats = new GameStats();
|
||||
this._ship = new TransformNode("shipBase", DefaultScene.MainScene);
|
||||
const data = await loadAsset("ship.glb");
|
||||
@ -126,12 +134,48 @@ export class Ship {
|
||||
agg.body.setAngularVelocity(new Vector3(0, 0, 0));
|
||||
agg.body.setCollisionCallbackEnabled(true);
|
||||
|
||||
// Register collision handler for hull damage
|
||||
// Register collision handler for energy-based hull damage
|
||||
const observable = agg.body.getCollisionObservable();
|
||||
observable.add(() => {
|
||||
// Damage hull on any collision
|
||||
if (this._scoreboard?.shipStatus) {
|
||||
this._scoreboard.shipStatus.damageHull(0.01);
|
||||
observable.add((collisionEvent) => {
|
||||
// Only calculate damage on collision start to avoid double-counting
|
||||
if (collisionEvent.type === 'COLLISION_STARTED') {
|
||||
// Get collision bodies
|
||||
const shipBody = collisionEvent.collider;
|
||||
const otherBody = collisionEvent.collidedAgainst;
|
||||
|
||||
// Get velocities
|
||||
const shipVelocity = shipBody.getLinearVelocity();
|
||||
const otherVelocity = otherBody.getLinearVelocity();
|
||||
|
||||
// Calculate relative velocity
|
||||
const relativeVelocity = shipVelocity.subtract(otherVelocity);
|
||||
const relativeSpeed = relativeVelocity.length();
|
||||
|
||||
// Get masses
|
||||
const shipMass = 10; // Known ship mass from aggregate creation
|
||||
const otherMass = otherBody.getMassProperties().mass;
|
||||
|
||||
// Calculate reduced mass for collision
|
||||
const reducedMass = (shipMass * otherMass) / (shipMass + otherMass);
|
||||
|
||||
// Calculate kinetic energy of collision
|
||||
const kineticEnergy = 0.5 * reducedMass * relativeSpeed * relativeSpeed;
|
||||
|
||||
// Convert energy to damage (tuning factor)
|
||||
// 1000 units of energy = 0.01 (1%) damage
|
||||
const ENERGY_TO_DAMAGE_FACTOR = 0.01 / 1000;
|
||||
const damage = Math.min(kineticEnergy * ENERGY_TO_DAMAGE_FACTOR, 0.5); // Cap at 50% per hit
|
||||
|
||||
// Apply damage if above minimum threshold
|
||||
if (this._scoreboard?.shipStatus && damage > 0.001) {
|
||||
this._scoreboard.shipStatus.damageHull(damage);
|
||||
debugLog(`Collision damage: ${damage.toFixed(4)} (energy: ${kineticEnergy.toFixed(1)}, speed: ${relativeSpeed.toFixed(1)} m/s)`);
|
||||
|
||||
// Play collision sound
|
||||
if (this._audio) {
|
||||
this._audio.playCollisionSound();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@ -8,6 +8,7 @@ export class ShipAudio {
|
||||
private _primaryThrustSound: StaticSound;
|
||||
private _secondaryThrustSound: StaticSound;
|
||||
private _weaponSound: StaticSound;
|
||||
private _collisionSound: StaticSound;
|
||||
private _primaryThrustPlaying: boolean = false;
|
||||
private _secondaryThrustPlaying: boolean = false;
|
||||
|
||||
@ -47,6 +48,15 @@ export class ShipAudio {
|
||||
volume: 0.5,
|
||||
}
|
||||
);
|
||||
|
||||
this._collisionSound = await this._audioEngine.createSoundAsync(
|
||||
"collision",
|
||||
"/assets/themes/default/audio/collision.mp3",
|
||||
{
|
||||
loop: false,
|
||||
volume: 0.35,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,6 +108,13 @@ export class ShipAudio {
|
||||
this._weaponSound?.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Play collision sound
|
||||
*/
|
||||
public playCollisionSound(): void {
|
||||
this._collisionSound?.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup audio resources
|
||||
*/
|
||||
@ -105,5 +122,6 @@ export class ShipAudio {
|
||||
this._primaryThrustSound?.dispose();
|
||||
this._secondaryThrustSound?.dispose();
|
||||
this._weaponSound?.dispose();
|
||||
this._collisionSound?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user