Refactor weapon system to shape casting and parent status screen to ship
All checks were successful
Build / build (push) Successful in 1m47s
All checks were successful
Build / build (push) Successful in 1m47s
- Replace physics-based projectiles with shape cast collision detection - Add ignoreBody to shape cast to prevent projectiles hitting ship mesh - Parent StatusScreen to ship TransformNode instead of XR camera - Add velocity check to victory condition (must be < 5 m/s) - Adjust projectile spawn offset and velocity 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5cdbf22e67
commit
5ce26c64ff
@ -39,6 +39,11 @@ export class RockFactory {
|
|||||||
private static _explosionManager: ExplosionManager | null = null;
|
private static _explosionManager: ExplosionManager | null = null;
|
||||||
private static _orbitCenter: PhysicsAggregate | null = null;
|
private static _orbitCenter: PhysicsAggregate | null = null;
|
||||||
|
|
||||||
|
/** Public getter for explosion manager (used by WeaponSystem for shape-cast hits) */
|
||||||
|
public static get explosionManager(): ExplosionManager | null {
|
||||||
|
return this._explosionManager;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize non-audio assets (meshes, explosion manager)
|
* Initialize non-audio assets (meshes, explosion manager)
|
||||||
* Call this before audio engine is unlocked
|
* Call this before audio engine is unlocked
|
||||||
|
|||||||
11
src/ship/projectile.ts
Normal file
11
src/ship/projectile.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { InstancedMesh, Vector3 } from "@babylonjs/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for tracking active projectiles in the shape-cast system
|
||||||
|
*/
|
||||||
|
export interface Projectile {
|
||||||
|
mesh: InstancedMesh;
|
||||||
|
velocity: Vector3;
|
||||||
|
lastPosition: Vector3;
|
||||||
|
lifetime: number;
|
||||||
|
}
|
||||||
@ -274,6 +274,10 @@ export class Ship {
|
|||||||
this._weapons.initialize();
|
this._weapons.initialize();
|
||||||
this._weapons.setShipStatus(this._scoreboard.shipStatus);
|
this._weapons.setShipStatus(this._scoreboard.shipStatus);
|
||||||
this._weapons.setGameStats(this._gameStats);
|
this._weapons.setGameStats(this._gameStats);
|
||||||
|
this._weapons.setScoreObservable(this._scoreboard.onScoreObservable);
|
||||||
|
if (this._ship.physicsBody) {
|
||||||
|
this._weapons.setShipBody(this._ship.physicsBody);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize input systems (skip in replay mode)
|
// Initialize input systems (skip in replay mode)
|
||||||
if (!this._isReplayMode) {
|
if (!this._isReplayMode) {
|
||||||
@ -359,6 +363,11 @@ export class Ship {
|
|||||||
if (this._voiceAudio) {
|
if (this._voiceAudio) {
|
||||||
this._voiceAudio.update();
|
this._voiceAudio.update();
|
||||||
}
|
}
|
||||||
|
// Update projectiles (shape casting collision detection)
|
||||||
|
if (this._weapons) {
|
||||||
|
const deltaTime = DefaultScene.MainScene.getEngine().getDeltaTime() / 1000;
|
||||||
|
this._weapons.update(deltaTime);
|
||||||
|
}
|
||||||
// Check game end conditions every 30 frames (~0.5 sec at 60fps)
|
// Check game end conditions every 30 frames (~0.5 sec at 60fps)
|
||||||
if (renderFrameCount++ % 30 === 0) {
|
if (renderFrameCount++ % 30 === 0) {
|
||||||
this.checkGameEndConditions();
|
this.checkGameEndConditions();
|
||||||
@ -441,6 +450,7 @@ export class Ship {
|
|||||||
// Initialize status screen with callbacks
|
// Initialize status screen with callbacks
|
||||||
this._statusScreen = new StatusScreen(
|
this._statusScreen = new StatusScreen(
|
||||||
DefaultScene.MainScene,
|
DefaultScene.MainScene,
|
||||||
|
this._ship,
|
||||||
this._gameStats,
|
this._gameStats,
|
||||||
() => this.handleReplayRequest(),
|
() => this.handleReplayRequest(),
|
||||||
() => this.handleExitVR(),
|
() => this.handleExitVR(),
|
||||||
@ -557,7 +567,7 @@ export class Ship {
|
|||||||
|
|
||||||
// Check condition 3: Victory (all asteroids destroyed, inside landing zone)
|
// Check condition 3: Victory (all asteroids destroyed, inside landing zone)
|
||||||
// Must have had asteroids to destroy in the first place (prevents false victory on init)
|
// Must have had asteroids to destroy in the first place (prevents false victory on init)
|
||||||
if (asteroidsRemaining <= 0 && this._isInLandingZone && this._scoreboard.hasAsteroidsToDestroy) {
|
if (asteroidsRemaining <= 0 && this._isInLandingZone && this._scoreboard.hasAsteroidsToDestroy && this._ship.physicsBody.getLinearVelocity().length() < 5) {
|
||||||
log.debug('Game end condition met: Victory (all asteroids destroyed)');
|
log.debug('Game end condition met: Victory (all asteroids destroyed)');
|
||||||
this._statusScreen.show(true, true, 'victory'); // Game ended, VICTORY!
|
this._statusScreen.show(true, true, 'victory'); // Game ended, VICTORY!
|
||||||
// InputControlManager will handle disabling controls when status screen shows
|
// InputControlManager will handle disabling controls when status screen shows
|
||||||
@ -660,7 +670,7 @@ export class Ship {
|
|||||||
if (this._isInLandingZone && this._scoreboard?.shipStatus) {
|
if (this._isInLandingZone && this._scoreboard?.shipStatus) {
|
||||||
// Physics update runs every 10 frames at 60fps = 6 times per second
|
// Physics update runs every 10 frames at 60fps = 6 times per second
|
||||||
// 0.1 per second / 6 updates per second = 0.01666... per update
|
// 0.1 per second / 6 updates per second = 0.01666... per update
|
||||||
const resupplyRate = 0.1 / 6;
|
const resupplyRate = 1 / 600;
|
||||||
|
|
||||||
const status = this._scoreboard.shipStatus;
|
const status = this._scoreboard.shipStatus;
|
||||||
|
|
||||||
@ -697,31 +707,34 @@ export class Ship {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this._weapons && this._ship && this._ship.physicsBody) {
|
if (this._weapons && this._ship && this._ship.physicsBody) {
|
||||||
|
// Clone world matrix to ensure consistent calculations
|
||||||
|
const worldMatrix = this._ship.getWorldMatrix().clone();
|
||||||
|
|
||||||
// Get ship velocities
|
// Get ship velocities
|
||||||
const linearVelocity = this._ship.physicsBody.getLinearVelocity();
|
const linearVelocity = this._ship.physicsBody.getLinearVelocity().clone();
|
||||||
const angularVelocity = this._ship.physicsBody.getAngularVelocity();
|
const angularVelocity = this._ship.physicsBody.getAngularVelocity();
|
||||||
|
|
||||||
// Spawn offset in local space (must match weaponSystem.ts)
|
// Spawn offset in local space (must match weaponSystem.ts)
|
||||||
const localSpawnOffset = new Vector3(0, 0.5, 8.4);
|
const localSpawnOffset = new Vector3(0, 0.5, 9.4);
|
||||||
|
|
||||||
// Transform spawn offset to world space (direction only)
|
// Transform spawn offset to world space
|
||||||
const worldSpawnOffset = Vector3.TransformNormal(
|
const worldSpawnOffset = Vector3.TransformCoordinates(localSpawnOffset, worldMatrix);
|
||||||
localSpawnOffset,
|
|
||||||
this._ship.getWorldMatrix()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate tangential velocity at spawn point: ω × r
|
// Calculate tangential velocity at spawn point: ω × r
|
||||||
const tangentialVelocity = angularVelocity.cross(worldSpawnOffset);
|
//const tangentialVelocity = angularVelocity.cross(worldSpawnOffset);
|
||||||
|
|
||||||
// Velocity at spawn point = linear + tangential
|
// Velocity at spawn point = ship velocity + tangential from rotation
|
||||||
const velocityAtSpawn = linearVelocity.add(tangentialVelocity);
|
//const velocityAtSpawn = linearVelocity.add(tangentialVelocity);
|
||||||
|
|
||||||
// Final projectile velocity: forward direction + spawn point velocity
|
// Get forward direction using world matrix (same method as thrust)
|
||||||
const projectileVelocity = this._ship.forward
|
const localForward = new Vector3(0, 0, 1);
|
||||||
.scale(200000)
|
const worldForward = Vector3.TransformNormal(localForward, worldMatrix);
|
||||||
.add(velocityAtSpawn);
|
log.debug(worldForward);
|
||||||
|
// Final projectile velocity: muzzle velocity in forward direction + ship velocity
|
||||||
|
const projectileVelocity = worldForward.scale(1000).add(linearVelocity);
|
||||||
|
log.debug(`Velocity - ${projectileVelocity}`);
|
||||||
|
|
||||||
this._weapons.fire(this._ship, projectileVelocity);
|
this._weapons.fire(worldSpawnOffset, projectileVelocity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,23 +1,28 @@
|
|||||||
import {
|
import {
|
||||||
AbstractMesh,
|
AbstractMesh,
|
||||||
Color3,
|
Color3,
|
||||||
|
HavokPlugin,
|
||||||
InstancedMesh,
|
InstancedMesh,
|
||||||
Mesh,
|
Mesh,
|
||||||
MeshBuilder,
|
MeshBuilder,
|
||||||
PhysicsAggregate,
|
Observable,
|
||||||
PhysicsMotionType,
|
PhysicsBody,
|
||||||
PhysicsShapeType,
|
PhysicsShapeSphere,
|
||||||
|
Quaternion,
|
||||||
Scene,
|
Scene,
|
||||||
|
ShapeCastResult,
|
||||||
StandardMaterial,
|
StandardMaterial,
|
||||||
TransformNode,
|
|
||||||
Vector3,
|
Vector3,
|
||||||
} from "@babylonjs/core";
|
} from "@babylonjs/core";
|
||||||
import { GameConfig } from "../core/gameConfig";
|
import { GameConfig } from "../core/gameConfig";
|
||||||
import { ShipStatus } from "./shipStatus";
|
import { ShipStatus } from "./shipStatus";
|
||||||
import { GameStats } from "../game/gameStats";
|
import { GameStats } from "../game/gameStats";
|
||||||
|
import { Projectile } from "./projectile";
|
||||||
|
import { RockFactory } from "../environment/asteroids/rockFactory";
|
||||||
|
import log from "../core/logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles weapon firing and projectile lifecycle
|
* Handles weapon firing and projectile lifecycle using shape casting
|
||||||
*/
|
*/
|
||||||
export class WeaponSystem {
|
export class WeaponSystem {
|
||||||
private _ammoBaseMesh: AbstractMesh;
|
private _ammoBaseMesh: AbstractMesh;
|
||||||
@ -26,145 +31,207 @@ export class WeaponSystem {
|
|||||||
private _shipStatus: ShipStatus | null = null;
|
private _shipStatus: ShipStatus | null = null;
|
||||||
private _gameStats: GameStats | null = null;
|
private _gameStats: GameStats | null = null;
|
||||||
|
|
||||||
|
// Shape casting properties
|
||||||
|
private _activeProjectiles: Projectile[] = [];
|
||||||
|
private _bulletCastShape: PhysicsShapeSphere;
|
||||||
|
private _localCastResult: ShapeCastResult;
|
||||||
|
private _worldCastResult: ShapeCastResult;
|
||||||
|
private _havokPlugin: HavokPlugin | null = null;
|
||||||
|
|
||||||
|
// Observable for score updates when asteroids are destroyed
|
||||||
|
private _scoreObservable: Observable<{score: number, remaining: number, message: string}> | null = null;
|
||||||
|
|
||||||
|
// Ship body to ignore in shape casts
|
||||||
|
private _shipBody: PhysicsBody | null = null;
|
||||||
|
|
||||||
constructor(scene: Scene) {
|
constructor(scene: Scene) {
|
||||||
this._scene = scene;
|
this._scene = scene;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the ship status instance for ammo tracking
|
|
||||||
*/
|
|
||||||
public setShipStatus(shipStatus: ShipStatus): void {
|
public setShipStatus(shipStatus: ShipStatus): void {
|
||||||
this._shipStatus = shipStatus;
|
this._shipStatus = shipStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the game stats instance for tracking shots fired
|
|
||||||
*/
|
|
||||||
public setGameStats(gameStats: GameStats): void {
|
public setGameStats(gameStats: GameStats): void {
|
||||||
this._gameStats = gameStats;
|
this._gameStats = gameStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public setScoreObservable(observable: Observable<{score: number, remaining: number, message: string}>): void {
|
||||||
* Initialize weapon system (create ammo template)
|
this._scoreObservable = observable;
|
||||||
*/
|
}
|
||||||
|
|
||||||
|
public setShipBody(body: PhysicsBody): void {
|
||||||
|
this._shipBody = body;
|
||||||
|
}
|
||||||
|
|
||||||
public initialize(): void {
|
public initialize(): void {
|
||||||
this._ammoMaterial = new StandardMaterial("ammoMaterial", this._scene);
|
this._ammoMaterial = new StandardMaterial("ammoMaterial", this._scene);
|
||||||
this._ammoMaterial.emissiveColor = new Color3(1, 1, 0);
|
this._ammoMaterial.emissiveColor = new Color3(1, 1, 0);
|
||||||
|
|
||||||
this._ammoBaseMesh = MeshBuilder.CreateIcoSphere(
|
this._ammoBaseMesh = MeshBuilder.CreateIcoSphere(
|
||||||
"bullet",
|
"bullet",
|
||||||
{ radius: 0.1, subdivisions: 2 },
|
{ radius: 0.5, subdivisions: 2 },
|
||||||
this._scene
|
this._scene
|
||||||
);
|
);
|
||||||
this._ammoBaseMesh.material = this._ammoMaterial;
|
this._ammoBaseMesh.material = this._ammoMaterial;
|
||||||
this._ammoBaseMesh.setEnabled(false);
|
this._ammoBaseMesh.setEnabled(false);
|
||||||
|
|
||||||
|
// Create reusable shape for casting (matches bullet radius)
|
||||||
|
this._bulletCastShape = new PhysicsShapeSphere(
|
||||||
|
Vector3.Zero(),
|
||||||
|
0.1,
|
||||||
|
this._scene
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reusable result objects (avoid allocations)
|
||||||
|
this._localCastResult = new ShapeCastResult();
|
||||||
|
this._worldCastResult = new ShapeCastResult();
|
||||||
|
|
||||||
|
// Get Havok plugin reference
|
||||||
|
const engine = this._scene.getPhysicsEngine();
|
||||||
|
if (engine) {
|
||||||
|
this._havokPlugin = engine.getPhysicsPlugin() as HavokPlugin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fire a projectile from the ship
|
* Fire a projectile from the ship (no physics body - uses shape casting)
|
||||||
* @param shipTransform - Ship transform node for position/orientation
|
|
||||||
* @param velocityVector - Complete velocity vector for the projectile (ship forward + ship velocity)
|
|
||||||
*/
|
*/
|
||||||
public fire(
|
public fire(position: Vector3, velocityVector: Vector3): void {
|
||||||
shipTransform: TransformNode,
|
|
||||||
velocityVector: Vector3
|
|
||||||
): void {
|
|
||||||
// Only allow shooting if physics is enabled
|
|
||||||
const config = GameConfig.getInstance();
|
const config = GameConfig.getInstance();
|
||||||
if (!config.physicsEnabled) {
|
if (!config.physicsEnabled) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have ammo before firing
|
if (this._shipStatus && this._shipStatus.ammo <= 0) return;
|
||||||
if (this._shipStatus && this._shipStatus.ammo <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create projectile instance
|
// Create visual-only projectile (no physics body)
|
||||||
const ammo = new InstancedMesh("ammo", this._ammoBaseMesh as Mesh);
|
const ammo = new InstancedMesh("ammo", this._ammoBaseMesh as Mesh);
|
||||||
ammo.parent = shipTransform;
|
ammo.position = position.clone();
|
||||||
ammo.position.y = 0.5;
|
|
||||||
ammo.position.z = 8.4;
|
|
||||||
|
|
||||||
// Detach from parent to move independently
|
// Track projectile for update loop
|
||||||
ammo.setParent(null);
|
this._activeProjectiles.push({
|
||||||
|
mesh: ammo,
|
||||||
|
velocity: velocityVector.clone(),
|
||||||
|
lastPosition: position.clone(),
|
||||||
|
lifetime: 0
|
||||||
|
});
|
||||||
|
|
||||||
// Create physics for projectile
|
|
||||||
const ammoAggregate = new PhysicsAggregate(
|
|
||||||
ammo,
|
|
||||||
PhysicsShapeType.SPHERE,
|
|
||||||
{
|
|
||||||
mass: 1000,
|
|
||||||
restitution: 0,
|
|
||||||
},
|
|
||||||
this._scene
|
|
||||||
);
|
|
||||||
ammoAggregate.body.setAngularDamping(1);
|
|
||||||
ammoAggregate.body.setMotionType(PhysicsMotionType.DYNAMIC);
|
|
||||||
ammoAggregate.body.setCollisionCallbackEnabled(true);
|
|
||||||
|
|
||||||
// Set projectile velocity (already includes ship velocity)
|
|
||||||
// Clone to capture current direction - prevents curving if source vector updates
|
|
||||||
ammoAggregate.body.setLinearVelocity(velocityVector.clone());
|
|
||||||
|
|
||||||
// Consume ammo
|
|
||||||
if (this._shipStatus) {
|
if (this._shipStatus) {
|
||||||
this._shipStatus.consumeAmmo(0.01);
|
this._shipStatus.consumeAmmo(0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track shot fired
|
|
||||||
if (this._gameStats) {
|
if (this._gameStats) {
|
||||||
this._gameStats.recordShotFired();
|
this._gameStats.recordShotFired();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track hits via collision detection
|
|
||||||
let hitRecorded = false; // Prevent multiple hits from same projectile
|
|
||||||
const gameStats = this._gameStats; // Capture in closure
|
|
||||||
|
|
||||||
const collisionObserver = ammoAggregate.body.getCollisionObservable().add((collisionEvent) => {
|
|
||||||
// Check if projectile hit something (not ship, not another projectile)
|
|
||||||
// Asteroids/rocks are the targets
|
|
||||||
if (!hitRecorded && gameStats && collisionEvent.collidedAgainst) {
|
|
||||||
// Record as hit - assumes collision with asteroid
|
|
||||||
gameStats.recordShotHit();
|
|
||||||
hitRecorded = true;
|
|
||||||
|
|
||||||
// Remove collision observer after first hit
|
|
||||||
if (collisionObserver && ammoAggregate.body) {
|
|
||||||
try {
|
|
||||||
ammoAggregate.body.getCollisionObservable().remove(collisionObserver);
|
|
||||||
} catch (_e) {
|
|
||||||
// Body may have been disposed during collision handling, ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-dispose after 2 seconds
|
|
||||||
window.setTimeout(() => {
|
|
||||||
// Clean up collision observer if body still exists
|
|
||||||
if (collisionObserver && ammoAggregate.body) {
|
|
||||||
try {
|
|
||||||
ammoAggregate.body.getCollisionObservable().remove(collisionObserver);
|
|
||||||
} catch (_e) {
|
|
||||||
// Body may have already been disposed, ignore error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispose if not already disposed
|
|
||||||
try {
|
|
||||||
ammoAggregate.dispose();
|
|
||||||
ammo.dispose();
|
|
||||||
} catch (_e) {
|
|
||||||
// Already disposed, ignore
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup weapon system resources
|
* Update all active projectiles - call each frame
|
||||||
*/
|
*/
|
||||||
|
public update(deltaTime: number): void {
|
||||||
|
if (!this._havokPlugin) return;
|
||||||
|
|
||||||
|
const toRemove: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < this._activeProjectiles.length; i++) {
|
||||||
|
const proj = this._activeProjectiles[i];
|
||||||
|
proj.lifetime += deltaTime;
|
||||||
|
|
||||||
|
// Remove if exceeded lifetime (2 seconds)
|
||||||
|
if (proj.lifetime > 2) {
|
||||||
|
toRemove.push(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate next position
|
||||||
|
const currentPos = proj.mesh.position.clone();
|
||||||
|
const nextPos = currentPos.add(proj.velocity.scale(deltaTime));
|
||||||
|
|
||||||
|
// Shape cast from current to next position (ignore ship body)
|
||||||
|
this._havokPlugin.shapeCast({
|
||||||
|
shape: this._bulletCastShape,
|
||||||
|
rotation: Quaternion.Identity(),
|
||||||
|
startPosition: currentPos,
|
||||||
|
endPosition: nextPos,
|
||||||
|
shouldHitTriggers: false,
|
||||||
|
ignoreBody: this._shipBody ?? undefined
|
||||||
|
}, this._localCastResult, this._worldCastResult);
|
||||||
|
|
||||||
|
if (this._worldCastResult.hasHit) {
|
||||||
|
// Calculate exact hit point
|
||||||
|
const hitPoint = Vector3.Lerp(
|
||||||
|
currentPos,
|
||||||
|
nextPos,
|
||||||
|
this._worldCastResult.hitFraction
|
||||||
|
);
|
||||||
|
|
||||||
|
this._onProjectileHit(proj, hitPoint, this._worldCastResult);
|
||||||
|
toRemove.push(i);
|
||||||
|
} else {
|
||||||
|
// No hit - update position
|
||||||
|
proj.lastPosition.copyFrom(currentPos);
|
||||||
|
proj.mesh.position.copyFrom(nextPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove hit/expired projectiles (reverse order to preserve indices)
|
||||||
|
for (let i = toRemove.length - 1; i >= 0; i--) {
|
||||||
|
const proj = this._activeProjectiles[toRemove[i]];
|
||||||
|
proj.mesh.dispose();
|
||||||
|
this._activeProjectiles.splice(toRemove[i], 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onProjectileHit(
|
||||||
|
proj: Projectile,
|
||||||
|
hitPoint: Vector3,
|
||||||
|
result: ShapeCastResult
|
||||||
|
): void {
|
||||||
|
if (this._gameStats) {
|
||||||
|
this._gameStats.recordShotHit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.body) return;
|
||||||
|
|
||||||
|
const hitMesh = result.body.transformNode as AbstractMesh;
|
||||||
|
const isAsteroid = hitMesh?.name?.startsWith("asteroid-");
|
||||||
|
|
||||||
|
if (isAsteroid) {
|
||||||
|
log.debug('[WeaponSystem] Asteroid hit! Triggering destruction...');
|
||||||
|
|
||||||
|
// Update score
|
||||||
|
if (this._scoreObservable) {
|
||||||
|
this._scoreObservable.notifyObservers({
|
||||||
|
score: 1,
|
||||||
|
remaining: -1,
|
||||||
|
message: "Asteroid Destroyed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose asteroid physics before explosion
|
||||||
|
if (result.shape) {
|
||||||
|
result.shape.dispose();
|
||||||
|
}
|
||||||
|
result.body.dispose();
|
||||||
|
|
||||||
|
// Play explosion effect
|
||||||
|
if (RockFactory.explosionManager) {
|
||||||
|
RockFactory.explosionManager.playExplosion(hitMesh);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-asteroid hit - just apply impulse
|
||||||
|
const impulse = proj.velocity.normalize().scale(50000);
|
||||||
|
result.body.applyImpulse(impulse, hitPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
|
// Dispose all active projectiles
|
||||||
|
for (const proj of this._activeProjectiles) {
|
||||||
|
proj.mesh.dispose();
|
||||||
|
}
|
||||||
|
this._activeProjectiles = [];
|
||||||
|
|
||||||
|
this._bulletCastShape?.dispose();
|
||||||
this._ammoBaseMesh?.dispose();
|
this._ammoBaseMesh?.dispose();
|
||||||
this._ammoMaterial?.dispose();
|
this._ammoMaterial?.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,11 +7,11 @@ import {
|
|||||||
TextBlock
|
TextBlock
|
||||||
} from "@babylonjs/gui";
|
} from "@babylonjs/gui";
|
||||||
import {
|
import {
|
||||||
Camera,
|
|
||||||
Mesh,
|
Mesh,
|
||||||
MeshBuilder,
|
MeshBuilder,
|
||||||
Scene,
|
Scene,
|
||||||
StandardMaterial,
|
StandardMaterial,
|
||||||
|
TransformNode,
|
||||||
Vector3
|
Vector3
|
||||||
} from "@babylonjs/core";
|
} from "@babylonjs/core";
|
||||||
import { GameStats } from "../../game/gameStats";
|
import { GameStats } from "../../game/gameStats";
|
||||||
@ -34,7 +34,7 @@ export class StatusScreen {
|
|||||||
private _screenMesh: Mesh | null = null;
|
private _screenMesh: Mesh | null = null;
|
||||||
private _texture: AdvancedDynamicTexture | null = null;
|
private _texture: AdvancedDynamicTexture | null = null;
|
||||||
private _isVisible: boolean = false;
|
private _isVisible: boolean = false;
|
||||||
private _camera: Camera | null = null;
|
private _shipNode: TransformNode;
|
||||||
private _parTime: number = 120; // Default par time in seconds
|
private _parTime: number = 120; // Default par time in seconds
|
||||||
|
|
||||||
// Text blocks for statistics
|
// Text blocks for statistics
|
||||||
@ -74,8 +74,9 @@ export class StatusScreen {
|
|||||||
// Track if result has been recorded (prevent duplicates)
|
// Track if result has been recorded (prevent duplicates)
|
||||||
private _resultRecorded: boolean = false;
|
private _resultRecorded: boolean = false;
|
||||||
|
|
||||||
constructor(scene: Scene, gameStats: GameStats, onReplay?: () => void, onExit?: () => void, onResume?: () => void, onNextLevel?: () => void) {
|
constructor(scene: Scene, shipNode: TransformNode, gameStats: GameStats, onReplay?: () => void, onExit?: () => void, onResume?: () => void, onNextLevel?: () => void) {
|
||||||
this._scene = scene;
|
this._scene = scene;
|
||||||
|
this._shipNode = shipNode;
|
||||||
this._gameStats = gameStats;
|
this._gameStats = gameStats;
|
||||||
this._onReplayCallback = onReplay || null;
|
this._onReplayCallback = onReplay || null;
|
||||||
this._onExitCallback = onExit || null;
|
this._onExitCallback = onExit || null;
|
||||||
@ -87,8 +88,6 @@ export class StatusScreen {
|
|||||||
* Initialize the status screen mesh and UI
|
* Initialize the status screen mesh and UI
|
||||||
*/
|
*/
|
||||||
public initialize(): void {
|
public initialize(): void {
|
||||||
this._camera = DefaultScene.XR.baseExperience.camera;
|
|
||||||
|
|
||||||
// Create a plane mesh for the status screen
|
// Create a plane mesh for the status screen
|
||||||
this._screenMesh = MeshBuilder.CreatePlane(
|
this._screenMesh = MeshBuilder.CreatePlane(
|
||||||
"statusScreen",
|
"statusScreen",
|
||||||
@ -96,10 +95,9 @@ export class StatusScreen {
|
|||||||
this._scene
|
this._scene
|
||||||
);
|
);
|
||||||
|
|
||||||
// Parent to camera for automatic following
|
// Parent to ship for fixed cockpit position
|
||||||
this._screenMesh.parent = this._camera;
|
this._screenMesh.parent = this._shipNode;
|
||||||
this._screenMesh.position = new Vector3(0, 0, 2); // 2 meters forward in local space
|
this._screenMesh.position = new Vector3(0, 1, 2); // 2 meters forward in local space
|
||||||
//this._screenMesh.rotation.y = Math.PI; // Face backward (toward user)
|
|
||||||
this._screenMesh.renderingGroupId = 3; // Always render on top
|
this._screenMesh.renderingGroupId = 3; // Always render on top
|
||||||
this._screenMesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
|
this._screenMesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user