Update gameplay mechanics, asteroid model, and star base physics
All checks were successful
Build / build (push) Successful in 1m18s

- 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>
This commit is contained in:
Michael Mainguy 2025-11-03 12:26:25 -06:00
parent 3e36662031
commit 37128d8fbd
10 changed files with 78 additions and 53 deletions

BIN
public/asteroid4.glb Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1,20 +1,18 @@
import {DefaultScene} from "./defaultScene"; import {DefaultScene} from "./defaultScene";
import type {AudioEngineV2} from "@babylonjs/core";
import { import {
AbstractMesh, AbstractMesh,
Color3, DistanceConstraint, Engine, InstancedMesh, LinesMesh, Mesh, Color3,
DistanceConstraint,
MeshBuilder, MeshBuilder,
Observable, Observable,
ParticleHelper,
PhysicsAggregate, PhysicsAggregate,
PhysicsMotionType, PhysicsMotionType,
PhysicsShapeType, PointsCloudSystem, PhysicsShapeType,
StandardMaterial, TransformNode, StandardMaterial,
Vector3 Vector3
} from "@babylonjs/core"; } from "@babylonjs/core";
import type {AudioEngineV2} from "@babylonjs/core";
import {Ship} from "./ship"; import {Ship} from "./ship";
import {RockFactory} from "./rockFactory";
import Level from "./level"; import Level from "./level";
import {Scoreboard} from "./scoreboard"; import {Scoreboard} from "./scoreboard";
import setLoadingMessage from "./setLoadingMessage"; import setLoadingMessage from "./setLoadingMessage";
@ -55,12 +53,7 @@ export class Level1 implements Level {
this._ship.addController(controller); this._ship.addController(controller);
}); });
}); });
//console.log('Controller observable registered, observer:', !!observer);
this.initialize(); this.initialize();
} }
getReadyObservable(): Observable<Level> { getReadyObservable(): Observable<Level> {
@ -94,6 +87,7 @@ export class Level1 implements Level {
}); });
}, 2000); }, 2000);
} }
public dispose() { public dispose() {
this._startBase.dispose(); this._startBase.dispose();
this._endBase.dispose(); this._endBase.dispose();
@ -101,6 +95,7 @@ export class Level1 implements Level {
this._backgroundStars.dispose(); this._backgroundStars.dispose();
} }
} }
public async initialize() { public async initialize() {
debugLog('Initializing level from config:', this._levelConfig.difficulty); debugLog('Initializing level from config:', this._levelConfig.difficulty);
if (this._initialized) { if (this._initialized) {
@ -133,6 +128,7 @@ export class Level1 implements Level {
// Calculate distance from start base // Calculate distance from start base
const dist = Vector3.Distance(asteroidMesh.position, this._startBase.position); const dist = Vector3.Distance(asteroidMesh.position, this._startBase.position);
const constraint = new DistanceConstraint(dist, DefaultScene.MainScene); const constraint = new DistanceConstraint(dist, DefaultScene.MainScene);
// constraint.isCollisionsEnabled = true;
this._startBase.physicsBody.addConstraint(asteroidMesh.physicsBody, constraint); this._startBase.physicsBody.addConstraint(asteroidMesh.physicsBody, constraint);
} }
} }
@ -155,6 +151,7 @@ export class Level1 implements Level {
} }
}); });
this._initialized = true; this._initialized = true;
// Notify that initialization is complete // Notify that initialization is complete

View File

@ -1,6 +1,6 @@
import { import {
AbstractMesh, AbstractMesh,
Color3, Color3, DirectionalLight,
GlowLayer, GlowLayer,
MeshBuilder, MeshBuilder,
Observable, Observable,
@ -66,6 +66,10 @@ export class LevelDeserializer {
const planets = this.createPlanets(); const planets = this.createPlanets();
const asteroids = await this.createAsteroids(scoreObservable); const asteroids = await this.createAsteroids(scoreObservable);
const dir = new Vector3(-1,-2,-1)
const light = new DirectionalLight("dirLight", dir, DefaultScene.MainScene);
const light2 = new DirectionalLight("dirLight2", dir.negate(), DefaultScene.MainScene);
light2.intensity = .5;
return { return {
startBase, startBase,
sun, sun,

View File

@ -214,29 +214,29 @@ export class LevelGenerator {
case 'recruit': case 'recruit':
return { return {
rockCount: 5, rockCount: 5,
forceMultiplier: .5, forceMultiplier: .8,
rockSizeMin: 10, rockSizeMin: 10,
rockSizeMax: 15, rockSizeMax: 15,
distanceMin: 80, distanceMin: 220,
distanceMax: 100 distanceMax: 250
}; };
case 'pilot': case 'pilot':
return { return {
rockCount: 10, rockCount: 10,
forceMultiplier: 1, forceMultiplier: 1,
rockSizeMin: 8, rockSizeMin: 8,
rockSizeMax: 12, rockSizeMax: 20,
distanceMin: 80, distanceMin: 225,
distanceMax: 150 distanceMax: 300
}; };
case 'captain': case 'captain':
return { return {
rockCount: 20, rockCount: 20,
forceMultiplier: 1.2, forceMultiplier: 1.2,
rockSizeMin: 2, rockSizeMin: 5,
rockSizeMax: 7, rockSizeMax: 40,
distanceMin: 100, distanceMin: 230,
distanceMax: 250 distanceMax: 450
}; };
case 'commander': case 'commander':
return { return {

View File

@ -4,7 +4,7 @@ import {
CreateAudioEngineAsync, CreateAudioEngineAsync,
DirectionalLight, DirectionalLight,
Engine, Engine,
HavokPlugin, HavokPlugin, HemisphericLight,
ParticleHelper, ParticleHelper,
Scene, Scene,
ScenePerformancePriority, ScenePerformancePriority,
@ -255,7 +255,12 @@ export class Main {
const havok = await HavokPhysics(); const havok = await HavokPhysics();
const havokPlugin = new HavokPlugin(true, havok); const havokPlugin = new HavokPlugin(true, havok);
//DefaultScene.MainScene.ambientColor = new Color3(.1, .1, .1); //DefaultScene.MainScene.ambientColor = new Color3(.1, .1, .1);
const light = new DirectionalLight("dirLight", new Vector3(-1, -2, -1), DefaultScene.MainScene);
//const light = new HemisphericLight("mainlight", new Vector3(-1, -1, 0), DefaultScene.MainScene);
//light.diffuse = new Color3(.4, .4, .3);
//light.groundColor = new Color3(.2, .2, .1);
//light.intensity = .5;
//light.specular = new Color3(0,0,0);
DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin); DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin);
DefaultScene.MainScene.getPhysicsEngine().setTimeStep(1/60); DefaultScene.MainScene.getPhysicsEngine().setTimeStep(1/60);
DefaultScene.MainScene.getPhysicsEngine().setSubTimeStep(5); DefaultScene.MainScene.getPhysicsEngine().setSubTimeStep(5);

View File

@ -52,7 +52,7 @@ export class RockFactory {
} }
private static async loadMesh() { private static async loadMesh() {
debugLog('loading mesh'); debugLog('loading mesh');
const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "asteroid3.glb", DefaultScene.MainScene); const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "asteroid4.glb", DefaultScene.MainScene);
this._rockMesh = importMesh.meshes[1].clone("asteroid", null, false); this._rockMesh = importMesh.meshes[1].clone("asteroid", null, false);
this._rockMesh.setParent(null); this._rockMesh.setParent(null);
this._rockMesh.setEnabled(false); this._rockMesh.setEnabled(false);
@ -62,16 +62,17 @@ export class RockFactory {
if (!this._rockMaterial) { if (!this._rockMaterial) {
// Clone the original material from GLB to preserve all textures // Clone the original material from GLB to preserve all textures
this._originalMaterial = this._rockMesh.material.clone("asteroid-original") as PBRMaterial; this._originalMaterial = this._rockMesh.material.clone("asteroid-original") as PBRMaterial;
this._rockMaterial = this._rockMesh.material.clone("asteroid-original") as PBRMaterial;
debugLog('Cloned original material from GLB:', this._originalMaterial); debugLog('Cloned original material from GLB:', this._originalMaterial);
// Create material using GameConfig texture level // Create material using GameConfig texture level
const config = GameConfig.getInstance(); /*const config = GameConfig.getInstance();
this._rockMaterial = MaterialFactory.createAsteroidMaterial( this._rockMaterial = MaterialFactory.createAsteroidMaterial(
'asteroid-material', 'asteroid-material',
config.asteroidTextureLevel, config.asteroidTextureLevel,
DefaultScene.MainScene, DefaultScene.MainScene,
this._originalMaterial this._originalMaterial
) as PBRMaterial; ) as PBRMaterial;*/
this._rockMaterial.freeze(); this._rockMaterial.freeze();
this._rockMesh.material = this._rockMaterial; this._rockMesh.material = this._rockMaterial;
@ -113,12 +114,13 @@ export class RockFactory {
body.setLinearDamping(0) body.setLinearDamping(0)
body.setMotionType(PhysicsMotionType.DYNAMIC); body.setMotionType(PhysicsMotionType.DYNAMIC);
body.setCollisionCallbackEnabled(true); body.setCollisionCallbackEnabled(true);
body.getCollisionObservable().add((eventData) => { body.getCollisionObservable().add((eventData) => {
if (eventData.type == 'COLLISION_STARTED') { if (eventData.type == 'COLLISION_STARTED') {
debugLog('[RockFactory] Collision detected:', { /*debugLog('[RockFactory] Collision detected:', {
collidedWith: eventData.collidedAgainst.transformNode.id, collidedWith: eventData.collidedAgainst.transformNode.id,
asteroidName: eventData.collider.transformNode.name asteroidName: eventData.collider.transformNode.name
}); });*/
if ( eventData.collidedAgainst.transformNode.id == 'ammo') { if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
debugLog('[RockFactory] ASTEROID HIT! Triggering explosion...'); debugLog('[RockFactory] ASTEROID HIT! Triggering explosion...');
@ -147,8 +149,15 @@ export class RockFactory {
eventData.collidedAgainst.dispose(); eventData.collidedAgainst.dispose();
debugLog('[RockFactory] Disposal complete'); debugLog('[RockFactory] Disposal complete');
} }
} else {
/*debugLog('[RockFactory] Collision ended between:', {
collider: eventData.collider.transformNode.id,
collidedWith: eventData.collidedAgainst.transformNode.id
});*/
} }
}); });
//body.setAngularVelocity(new Vector3(Math.random(), Math.random(), Math.random())); //body.setAngularVelocity(new Vector3(Math.random(), Math.random(), Math.random()));
// body.setLinearVelocity(Vector3.Random(-10, 10)); // body.setLinearVelocity(Vector3.Random(-10, 10));
} }

View File

@ -82,8 +82,8 @@ export class Scoreboard {
scoreText.text = `Score: ${this.calculateScore()}`; scoreText.text = `Score: ${this.calculateScore()}`;
remainingText.text = `Remaining: ${this._remaining}`; remainingText.text = `Remaining: ${this._remaining}`;
const elapsed = Date.now() - this._startTime; const elapsed = Date.now() - this._startTime;
if (this._active && i++%40 == 0) { if (this._active && i++%30 == 0) {
timeRemainingText.text = `Time: ${Math.floor(elapsed/60).toString().padStart(2,"0")}:${(elapsed%60).toString().padStart(2,"0")}`; timeRemainingText.text = `Time: ${Math.floor(elapsed/60000).toString().padStart(2,"0")}:${(Math.floor(elapsed/1000)%60).toString().padStart(2,"0")}`;
fpsText.text = `FPS: ${Math.floor(scene.getEngine().getFps())}`; fpsText.text = `FPS: ${Math.floor(scene.getEngine().getFps())}`;
} }
}); });

View File

@ -231,16 +231,16 @@ export class Ship {
//shipMesh.position.y = 1; //shipMesh.position.y = 1;
shipMesh.position.z = -1; shipMesh.position.z = -1;
// shipMesh.renderingGroupId = 3; // shipMesh.renderingGroupId = 3;
const light = new PointLight("ship.light", new Vector3(0, .5, .1), DefaultScene.MainScene); //const light = new PointLight("ship.light", new Vector3(0, .5, .1), DefaultScene.MainScene);
light.intensity = 4; //light.intensity = 4;
light.includedOnlyMeshes = [shipMesh]; /*light.includedOnlyMeshes = [shipMesh];
for (const mesh of shipMesh.getChildMeshes()) { for (const mesh of shipMesh.getChildMeshes()) {
// mesh.renderingGroupId = 3; // mesh.renderingGroupId = 3;
if (mesh.material.id.indexOf('glass') === -1) { if (mesh.material.id.indexOf('glass') === -1) {
light.includedOnlyMeshes.push(mesh); light.includedOnlyMeshes.push(mesh);
} }
} }
light.parent = this._ship; light.parent = this._ship;*/
//DefaultScene.MainScene.getMaterialById('glass_mat.002').alpha = .4; //DefaultScene.MainScene.getMaterialById('glass_mat.002').alpha = .4;
} }

View File

@ -1,14 +1,12 @@
import { import {
AbstractMesh, AbstractMesh, LoadAssetContainerAsync, Mesh,
PhysicsAggregate, PhysicsAggregate,
PhysicsMotionType, PhysicsMotionType,
PhysicsShapeType, PhysicsShapeType, Scene,
SceneLoader,
Vector3 Vector3
} from "@babylonjs/core"; } from "@babylonjs/core";
import {DefaultScene} from "./defaultScene"; import {DefaultScene} from "./defaultScene";
import {GameConfig} from "./gameConfig"; import {GameConfig} from "./gameConfig";
import {debug} from "openai/core";
import debugLog from "./debug"; import debugLog from "./debug";
/** /**
@ -20,21 +18,33 @@ export default async function buildStarBase(position: Vector3): Promise<Abstract
const scene = DefaultScene.MainScene; const scene = DefaultScene.MainScene;
// Load the base model // Load the base model
const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "base.glb", scene); const importMesh = await LoadAssetContainerAsync('/base.glb', DefaultScene.MainScene,
const baseMesh = importMesh.meshes[0].getChildMeshes()[0]; {
debugLog('Star base mesh loaded:', baseMesh); pluginOptions: {
baseMesh.id = "starBase"; gltf: {
baseMesh.name = "starBase"; enabled: true,
baseMesh.position = position; }
debugLog('Ship Bounds radius', baseMesh.getBoundingInfo().boundingSphere.radiusWorld); }
// Create physics if enabled });
importMesh.addAllToScene();
const starBase = Mesh.MergeMeshes(importMesh.rootNodes[0].getChildMeshes(false), true, false, null, false, true);
starBase.id = 'starBase';
starBase.name = 'starBase';
DefaultScene.MainScene.addMesh(starBase);
debugLog('imported base mesh', importMesh.meshes[0]);
starBase.position = position;
const config = GameConfig.getInstance(); const config = GameConfig.getInstance();
if (config.physicsEnabled) { if (config.physicsEnabled) {
const agg = new PhysicsAggregate(baseMesh, PhysicsShapeType.MESH, { const agg = new PhysicsAggregate(starBase, PhysicsShapeType.MESH, {
mass: 0 mass: 10000
}, scene); }, scene);
agg.body.setMotionType(PhysicsMotionType.STATIC); agg.body.setMotionType(PhysicsMotionType.ANIMATED);
agg.body.setCollisionCallbackEnabled(true);
agg.body.getCollisionObservable().add((collidedBody) => {
debugLog('collidedBody', collidedBody);
})
} }
importMesh.rootNodes[0].dispose();
return baseMesh; return starBase;
} }