Add difficulty levels and upgrade BabylonJS
Some checks failed
Build / build (push) Failing after 20s

Implemented a level selection system with 5 difficulty modes (Recruit, Pilot, Captain, Commander, Test), each with different asteroid counts, sizes, speeds, and constraints. Upgraded BabylonJS from 7.13.1 to 8.32.0 and fixed particle system animation compatibility issues.

- Add card-based level selection UI with 5 difficulty options
- Create difficulty configuration system in Level1
- Fix explosion particle animations for mesh emitters (emitter.y → emitter.position.y)
- Implement particle system pooling for improved explosion performance
- Upgrade @babylonjs packages to 8.32.0
- Fix audio engine unlock after Babylon upgrade
- Add test mode with 100 large, slow-moving asteroids
- Add styles.css for level selection cards with hover effects

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-10-17 09:05:18 -05:00
parent 4d9b678f70
commit d2aec0a87b
9 changed files with 363 additions and 126 deletions

View File

@ -19,9 +19,37 @@
<canvas id="gameCanvas"></canvas> <canvas id="gameCanvas"></canvas>
<div id="mainDiv"> <div id="mainDiv">
<div id="loadingDiv">Loading...</div> <div id="loadingDiv">Loading...</div>
<button id="startButton">Start New Game</button> <div id="levelSelect">
<h1>Select Your Level</h1>
<div class="card-container">
<div class="level-card">
<h2>Recruit</h2>
<p>Perfect for beginners. Learn the basics of space combat.</p>
<button class="level-button" data-level="recruit">Start as Recruit</button>
</div>
<div class="level-card">
<h2>Pilot</h2>
<p>Intermediate challenge. Face tougher enemies and obstacles.</p>
<button class="level-button" data-level="pilot">Start as Pilot</button>
</div>
<div class="level-card">
<h2>Captain</h2>
<p>Advanced difficulty. Command your ship with precision.</p>
<button class="level-button" data-level="captain">Start as Captain</button>
</div>
<div class="level-card">
<h2>Commander</h2>
<p>Expert mode. Only for the most skilled space warriors.</p>
<button class="level-button" data-level="commander">Start as Commander</button>
</div>
<div class="level-card">
<h2>Test</h2>
<p>Testing mode. Many large, slow-moving asteroids.</p>
<button class="level-button" data-level="test">Start Test Mode</button>
</div>
</div>
</div>
</div> </div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

131
package-lock.json generated
View File

@ -8,14 +8,14 @@
"name": "space-game", "name": "space-game",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@babylonjs/core": "7.13.1", "@babylonjs/core": "8.32.0",
"@babylonjs/gui": "^7.13.1", "@babylonjs/gui": "^8.32.0",
"@babylonjs/havok": "1.3.5", "@babylonjs/havok": "1.3.5",
"@babylonjs/inspector": "^7.13.1", "@babylonjs/inspector": "8.32.0",
"@babylonjs/loaders": "^7.13.1", "@babylonjs/loaders": "8.32.0",
"@babylonjs/materials": "^7.13.1", "@babylonjs/materials": "8.32.0",
"@babylonjs/procedural-textures": "^7.13.1", "@babylonjs/procedural-textures": "8.32.0",
"@babylonjs/serializers": "^7.13.1", "@babylonjs/serializers": "8.32.0",
"openai": "4.52.3" "openai": "4.52.3"
}, },
"devDependencies": { "devDependencies": {
@ -23,27 +23,36 @@
"vite": "^5.2.13" "vite": "^5.2.13"
} }
}, },
"node_modules/@babylonjs/addons": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/addons/-/addons-8.32.0.tgz",
"integrity": "sha512-mv3rF6slOYcArpQbWJzVlN7Qb71/um4HpZRYIyA37wmmoX1behxk+1Of9J6PqdrwLQjng9e8gxfrIA1OUxIEpw==",
"peer": true,
"peerDependencies": {
"@babylonjs/core": "^8.0.0"
}
},
"node_modules/@babylonjs/core": { "node_modules/@babylonjs/core": {
"version": "7.13.1", "version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-7.13.1.tgz", "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-8.32.0.tgz",
"integrity": "sha512-limlsRIhRBH9xsuUNsy9xAyi0jhfQxfvhlMzMjFK3Ugq4c7joYpoZMkQU038esOQ3aq3q8VPv1+CshE3NASEMQ==" "integrity": "sha512-Z83WIe2eZEAOo5bb9Tjd+lY4ru6N8qgtZJGjWcoXOiP3BrtbatPUXdVKqm7m60ItQABFaVdMGygvIXY+wNXU/Q=="
}, },
"node_modules/@babylonjs/gui": { "node_modules/@babylonjs/gui": {
"version": "7.13.1", "version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/gui/-/gui-7.13.1.tgz", "resolved": "https://registry.npmjs.org/@babylonjs/gui/-/gui-8.32.0.tgz",
"integrity": "sha512-FW2QOqpzoJI2q/hFKOa7O7xnj6oKzHWmTGHVhqjg7jzZy//bMZ5AbxNdMpQgDa+QoENpd7r+yx1O4kc4Lw1UaQ==", "integrity": "sha512-nR+E3u3hgGky+/6k1h8F5B4tS4OW6x3y63hf88kS3GgANd8as2iL54NC1Z74a25/8/nlaSRhodEYwx5O7lZKlQ==",
"peerDependencies": { "peerDependencies": {
"@babylonjs/core": "^7.0.0" "@babylonjs/core": "^8.0.0"
} }
}, },
"node_modules/@babylonjs/gui-editor": { "node_modules/@babylonjs/gui-editor": {
"version": "7.13.1", "version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/gui-editor/-/gui-editor-7.13.1.tgz", "resolved": "https://registry.npmjs.org/@babylonjs/gui-editor/-/gui-editor-8.32.0.tgz",
"integrity": "sha512-1piYccMR4FRqMXVu/OMqCgc6Z5uRpt49u6yUYyRY627aZkarBM7oh310A6sgTkfuk77c9ZNoIgtgbq5P/dB48Q==", "integrity": "sha512-sRql2tCW+dUulQxWbzSOskGsgDUGYHJ5mM0eLbsKPFTCggURse3drNtynOw/24dO/OCaES0zO0Ob/nDH0qrFxA==",
"peer": true, "peer": true,
"peerDependencies": { "peerDependencies": {
"@babylonjs/core": "^7.0.0", "@babylonjs/core": "^8.0.0",
"@babylonjs/gui": "^7.0.0", "@babylonjs/gui": "^8.0.0",
"@types/react": ">=16.7.3", "@types/react": ">=16.7.3",
"@types/react-dom": ">=16.0.9" "@types/react-dom": ">=16.0.9"
} }
@ -57,57 +66,58 @@
} }
}, },
"node_modules/@babylonjs/inspector": { "node_modules/@babylonjs/inspector": {
"version": "7.13.1", "version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/inspector/-/inspector-7.13.1.tgz", "resolved": "https://registry.npmjs.org/@babylonjs/inspector/-/inspector-8.32.0.tgz",
"integrity": "sha512-BM2ZQu15ESbWFQPwU/iqCx1P/KiJX9qX9dCVU/+5bbLqzemja5aDajLMnM+vb4uimAtbokkUdFqNckGnZVo7VQ==", "integrity": "sha512-7crFgaQmKmcgh69lf0cB6r47UjhL8SYZgL6UbHjmHy3VPrj6Xde+oN86S+BG+kalKfPLoaTdBLz1+b0AYa4Xxw==",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.0", "@fortawesome/fontawesome-svg-core": "^6.1.0",
"@fortawesome/free-regular-svg-icons": "^6.0.0", "@fortawesome/free-regular-svg-icons": "^6.0.0",
"@fortawesome/free-solid-svg-icons": "^6.0.0" "@fortawesome/free-solid-svg-icons": "^6.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@babylonjs/core": "^7.0.0", "@babylonjs/addons": "^8.0.0",
"@babylonjs/gui": "^7.0.0", "@babylonjs/core": "^8.0.0",
"@babylonjs/gui-editor": "^7.0.0", "@babylonjs/gui": "^8.0.0",
"@babylonjs/loaders": "^7.0.0", "@babylonjs/gui-editor": "^8.0.0",
"@babylonjs/materials": "^7.0.0", "@babylonjs/loaders": "^8.0.0",
"@babylonjs/serializers": "^7.0.0", "@babylonjs/materials": "^8.0.0",
"@babylonjs/serializers": "^8.0.0",
"@types/react": ">=16.7.3", "@types/react": ">=16.7.3",
"@types/react-dom": ">=16.0.9" "@types/react-dom": ">=16.0.9"
} }
}, },
"node_modules/@babylonjs/loaders": { "node_modules/@babylonjs/loaders": {
"version": "7.13.1", "version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-7.13.1.tgz", "resolved": "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-8.32.0.tgz",
"integrity": "sha512-17MCwYpMM4EF69wzsclIvs7Ci84lYdkcH2NSM3hD3phl8k373yNKVvBNrs68p9yZUCFbXIVEH1tl5QQxr2T37g==", "integrity": "sha512-H2tKP2z5la0cWkkhVDEVUNSW3n187G2ti6G9OlFXOjr2SBzEWvDfsxL0je94z2SLw8LGH0Y6hBPGP1k/p2/YSg==",
"peerDependencies": { "peerDependencies": {
"@babylonjs/core": "^7.0.0", "@babylonjs/core": "^8.0.0",
"babylonjs-gltf2interface": "^7.0.0" "babylonjs-gltf2interface": "^8.0.0"
} }
}, },
"node_modules/@babylonjs/materials": { "node_modules/@babylonjs/materials": {
"version": "7.13.1", "version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/materials/-/materials-7.13.1.tgz", "resolved": "https://registry.npmjs.org/@babylonjs/materials/-/materials-8.32.0.tgz",
"integrity": "sha512-CkjHyhsMglr9Aqf5NNsqwCgVPdMxnu1yRD4I6CGDXAp814ghqb5/JwkoL4lgelSYinsSh4HXpFr4Jgays5W2hw==", "integrity": "sha512-p+RvvzC4o01quumcNOwTgkiYn/v1BTDTRJTEQp80GcCi95weKLdzhwuR2FsUlL5xaPRLrOrd2ld634ZSjOW9tA==",
"peerDependencies": { "peerDependencies": {
"@babylonjs/core": "^7.0.0" "@babylonjs/core": "^8.6.0"
} }
}, },
"node_modules/@babylonjs/procedural-textures": { "node_modules/@babylonjs/procedural-textures": {
"version": "7.13.1", "version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/procedural-textures/-/procedural-textures-7.13.1.tgz", "resolved": "https://registry.npmjs.org/@babylonjs/procedural-textures/-/procedural-textures-8.32.0.tgz",
"integrity": "sha512-FzPtNsEglAi/0rqSEGBsnCAMn6Ggnf0phYSTOL7iHec+Rhx+1DVwN5PvUBYAJT5Sy5PIq2mPZI1uhq1A5QySxA==", "integrity": "sha512-NiE+F2x1Cc1IvUPwEGV4ckAUsq457G0Wqx0w4vpdEyWhbdYaGa78OpQO2RwT4I02Y58zxrpaBJPydQTn8XN23g==",
"peerDependencies": { "peerDependencies": {
"@babylonjs/core": "^7.0.0" "@babylonjs/core": "^8.0.0"
} }
}, },
"node_modules/@babylonjs/serializers": { "node_modules/@babylonjs/serializers": {
"version": "7.13.1", "version": "8.32.0",
"resolved": "https://registry.npmjs.org/@babylonjs/serializers/-/serializers-7.13.1.tgz", "resolved": "https://registry.npmjs.org/@babylonjs/serializers/-/serializers-8.32.0.tgz",
"integrity": "sha512-ct1MTjRZaj4EEyMhDiHJkhSSKJRGValGPLNv3UOYh7EYc4XN0HbEz4wuMkwlgENFP7Hz5okWVBQB9yQWObR3Sg==", "integrity": "sha512-U+D10S6i4fzukfCBPJD8e1NmjUSNDWS46AUf9v1/3O7XiVOt3yN5g5jZl50oBiYwQvyYs0FBWZbLdBfuREj1cg==",
"peerDependencies": { "peerDependencies": {
"@babylonjs/core": "^7.0.0", "@babylonjs/core": "^8.0.0",
"babylonjs-gltf2interface": "^7.0.0" "babylonjs-gltf2interface": "^8.0.0"
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
@ -759,29 +769,22 @@
"form-data": "^4.0.0" "form-data": "^4.0.0"
} }
}, },
"node_modules/@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
"peer": true
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.3", "version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/@types/react-dom": { "node_modules/@types/react-dom": {
"version": "18.3.0", "version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"peer": true, "peer": true,
"dependencies": { "peerDependencies": {
"@types/react": "*" "@types/react": "^19.2.0"
} }
}, },
"node_modules/abort-controller": { "node_modules/abort-controller": {
@ -812,9 +815,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
}, },
"node_modules/babylonjs-gltf2interface": { "node_modules/babylonjs-gltf2interface": {
"version": "7.13.1", "version": "8.32.0",
"resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-7.13.1.tgz", "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-8.32.0.tgz",
"integrity": "sha512-kgMZrek1Gul22+Igy43pYdofg89odPv5uxYrjzryVvMxmzPI7NwgxickXT3tM/SGoyF0AoXlPrKLCK5zHT0/eg==", "integrity": "sha512-OECfOlxbIXHp4kYzqZNj42e0I5MfVAmgAgBkQaFGJdojK+hc/iIg9LQfAhYbKjr+Rpb1v1HnQ8tWBjXLwgtyXg==",
"peer": true "peer": true
}, },
"node_modules/combined-stream": { "node_modules/combined-stream": {

View File

@ -12,14 +12,14 @@
"speech": "tsc && node ./dist/server/voices.js" "speech": "tsc && node ./dist/server/voices.js"
}, },
"dependencies": { "dependencies": {
"@babylonjs/core": "7.13.1", "@babylonjs/core": "8.32.0",
"@babylonjs/gui": "^7.13.1", "@babylonjs/gui": "^8.32.0",
"@babylonjs/havok": "1.3.5", "@babylonjs/havok": "1.3.5",
"@babylonjs/inspector": "^7.13.1", "@babylonjs/inspector": "8.32.0",
"@babylonjs/loaders": "^7.13.1", "@babylonjs/loaders": "8.32.0",
"@babylonjs/materials": "^7.13.1", "@babylonjs/materials": "8.32.0",
"@babylonjs/serializers": "^7.13.1", "@babylonjs/serializers": "8.32.0",
"@babylonjs/procedural-textures": "^7.13.1", "@babylonjs/procedural-textures": "8.32.0",
"openai": "4.52.3" "openai": "4.52.3"
}, },
"devDependencies": { "devDependencies": {

View File

@ -434,7 +434,7 @@
[ [
{ {
"name": "plumeAnimation", "name": "plumeAnimation",
"property": "emitter.y", "property": "emitter.position.y",
"framePerSecond": 60, "framePerSecond": 60,
"dataType": 0, "dataType": 0,
"loopBehavior": 2, "loopBehavior": 2,

View File

@ -24,8 +24,19 @@ export class Level1 implements Level {
private _startBase: AbstractMesh; private _startBase: AbstractMesh;
private _endBase: AbstractMesh; private _endBase: AbstractMesh;
private _scoreboard: Scoreboard; private _scoreboard: Scoreboard;
private _difficulty: string;
private _difficultyConfig: {
rockCount: number;
forceMultiplier: number;
rockSizeMin: number;
rockSizeMax: number;
distanceMin: number;
distanceMax: number;
};
constructor() { constructor(difficulty: string = 'recruit') {
this._difficulty = difficulty;
this._difficultyConfig = this.getDifficultyConfig(difficulty);
this._ship = new Ship(); this._ship = new Ship();
this._scoreboard = new Scoreboard(); this._scoreboard = new Scoreboard();
const xr = DefaultScene.XR; const xr = DefaultScene.XR;
@ -41,6 +52,65 @@ export class Level1 implements Level {
} }
private getDifficultyConfig(difficulty: string) {
switch (difficulty) {
case 'recruit':
return {
rockCount: 5,
forceMultiplier: 1,
rockSizeMin: 4,
rockSizeMax: 10,
distanceMin: 150,
distanceMax: 180
};
case 'pilot':
return {
rockCount: 10,
forceMultiplier: 1.6,
rockSizeMin: 3,
rockSizeMax: 8,
distanceMin: 120,
distanceMax: 220
};
case 'captain':
return {
rockCount: 20,
forceMultiplier: 2.0,
rockSizeMin: 2,
rockSizeMax: 7,
distanceMin: 100,
distanceMax: 250
};
case 'commander':
return {
rockCount: 50,
forceMultiplier: 2.5,
rockSizeMin: 2,
rockSizeMax: 8,
distanceMin: 90,
distanceMax: 280
};
case 'test':
return {
rockCount: 100,
forceMultiplier: 0.3,
rockSizeMin: 8,
rockSizeMax: 15,
distanceMin: 150,
distanceMax: 200
};
default:
return {
rockCount: 5,
forceMultiplier: 1.0,
rockSizeMin: 4,
rockSizeMax: 8,
distanceMin: 170,
distanceMax: 220
};
}
}
getReadyObservable(): Observable<Level> { getReadyObservable(): Observable<Level> {
return this._onReadyObservable; return this._onReadyObservable;
} }
@ -65,9 +135,13 @@ export class Level1 implements Level {
this._ship.position = new Vector3(0, 1, 0); this._ship.position = new Vector3(0, 1, 0);
await RockFactory.init(); await RockFactory.init();
for (let i = 0; i < 5; i++) { const config = this._difficultyConfig;
const dist = (Math.random() * 50) + 190; console.log(config);
const size = Vector3.Random(1,1.3).scale(Math.random() * 5 + 5) for (let i = 0; i < config.rockCount; i++) {
const distRange = config.distanceMax - config.distanceMin;
const dist = (Math.random() * distRange) + config.distanceMin;
const sizeRange = config.rockSizeMax - config.rockSizeMin;
const size = Vector3.Random(1,1.3).scale(Math.random() * sizeRange + config.rockSizeMin)
const rock = await RockFactory.createRock(i, new Vector3(Math.random() * 200 +50 * Math.sign(Math.random() -.5),200,200), const rock = await RockFactory.createRock(i, new Vector3(Math.random() * 200 +50 * Math.sign(Math.random() -.5),200,200),
size, size,
@ -94,7 +168,7 @@ export class Level1 implements Level {
message: "Get Ready" message: "Get Ready"
}); });
this._startBase.physicsBody.addConstraint(rock.physicsBody, constraint); this._startBase.physicsBody.addConstraint(rock.physicsBody, constraint);
rock.physicsBody.applyForce(Vector3.Random(-1, 1).scale(5000000), rock.position); rock.physicsBody.applyForce(Vector3.Random(-1, 1).scale(5000000 * config.forceMultiplier), rock.position);
} }
} }

View File

@ -28,6 +28,8 @@ export class Main {
private _loadingDiv: HTMLElement; private _loadingDiv: HTMLElement;
private _currentLevel: Level; private _currentLevel: Level;
private _gameState: GameState = GameState.DEMO; private _gameState: GameState = GameState.DEMO;
private _selectedDifficulty: string = 'recruit';
private _engine: Engine | WebGPUEngine;
constructor() { constructor() {
this._loadingDiv = document.querySelector('#loadingDiv'); this._loadingDiv = document.querySelector('#loadingDiv');
if (!navigator.xr) { if (!navigator.xr) {
@ -36,10 +38,19 @@ export class Main {
} }
this.initialize(); this.initialize();
document.querySelector('#startButton').addEventListener('click', () => { document.querySelectorAll('.level-button').forEach(button => {
Engine.audioEngine.unlock(); button.addEventListener('click', (e) => {
this.play(); const levelButton = e.target as HTMLButtonElement;
document.querySelector('#mainDiv').remove(); this._selectedDifficulty = levelButton.dataset.level;
this.setLoadingMessage("Initializing Level...");
this._currentLevel = new Level1(this._selectedDifficulty);
// Unlock audio engine if it exists
if (this._engine?.audioEngine) {
this._engine.audioEngine.unlock();
}
this.play();
document.querySelector('#mainDiv').remove();
});
}); });
} }
private _started = false; private _started = false;
@ -63,11 +74,6 @@ export class Main {
}); });
this.setLoadingMessage("Get Ready!"); this.setLoadingMessage("Get Ready!");
this.setLoadingMessage("Initializing Level...");
this._currentLevel = new Level1();
this._currentLevel.getReadyObservable().add(() => {
});
const photoDome1 = new PhotoDome("testdome", '/8192.webp', {size: 1000}, DefaultScene.MainScene); const photoDome1 = new PhotoDome("testdome", '/8192.webp', {size: 1000}, DefaultScene.MainScene);
photoDome1.material.diffuseTexture.hasAlpha = true; photoDome1.material.diffuseTexture.hasAlpha = true;
@ -86,30 +92,32 @@ export class Main {
} }
private async setupScene() { private async setupScene() {
let engine: WebGPUEngine | Engine = null;
if (webGpu) { if (webGpu) {
engine = new WebGPUEngine(canvas); this._engine = new WebGPUEngine(canvas);
await (engine as WebGPUEngine).initAsync(); await (this._engine as WebGPUEngine).initAsync();
} else { } else {
engine = new Engine(canvas, true); this._engine = new Engine(canvas, true);
} }
engine.setHardwareScalingLevel(1 / window.devicePixelRatio); this._engine.setHardwareScalingLevel(1 / window.devicePixelRatio);
window.onresize = () => { window.onresize = () => {
engine.resize(); this._engine.resize();
} }
DefaultScene.DemoScene = new Scene(engine); DefaultScene.DemoScene = new Scene(this._engine);
DefaultScene.MainScene = new Scene(engine); DefaultScene.MainScene = new Scene(this._engine);
DefaultScene.MainScene.ambientColor = new Color3(.2, .2, .2); DefaultScene.MainScene.ambientColor = new Color3(.2, .2, .2);
this.setLoadingMessage("Initializing Physics Engine.."); this.setLoadingMessage("Initializing Physics Engine..");
await this.setupPhysics(); await this.setupPhysics();
this.setLoadingMessage("Physics Engine Ready!");
this.setupInspector(); this.setupInspector();
engine.runRenderLoop(() => { this._engine.runRenderLoop(() => {
if (!this._started) { if (!this._started) {
this._started = true; this._started = true;
this._loadingDiv.remove(); this._loadingDiv.remove();
const start = document.querySelector('#startButton'); const levelSelect = document.querySelector('#levelSelect');
start.classList.add('ready'); if (levelSelect) {
levelSelect.classList.add('ready');
}
} }
if (this._gameState == GameState.PLAY) { if (this._gameState == GameState.PLAY) {
DefaultScene.MainScene.render(); DefaultScene.MainScene.render();

View File

@ -32,14 +32,18 @@ export class Rock {
export class RockFactory { export class RockFactory {
private static _rockMesh: AbstractMesh; private static _rockMesh: AbstractMesh;
private static _rockMaterial: PBRMaterial; private static _rockMaterial: PBRMaterial;
private static _explosion: ParticleSystemSet; private static _explosionPool: ParticleSystemSet[] = [];
public static async init() { private static _poolSize: number = 10;
if (!this._explosion) { public static async init() {
const set = await ParticleHelper.CreateAsync("explosion", DefaultScene.MainScene); // Pre-create explosion particle systems for pooling
this._explosion = set.serialize(true); console.log("Pre-creating explosion particle systems...");
set.dispose(); for (let i = 0; i < this._poolSize; i++) {
const set = await ParticleHelper.CreateAsync("explosion", DefaultScene.MainScene);
this._explosionPool.push(set);
} }
console.log(`Created ${this._poolSize} explosion particle systems in pool`);
if (!this._rockMesh) { if (!this._rockMesh) {
console.log('loading mesh'); console.log('loading mesh');
const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "asteroid2.glb", DefaultScene.MainScene); const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "asteroid2.glb", DefaultScene.MainScene);
@ -64,6 +68,18 @@ export class RockFactory {
} }
} }
} }
private static getExplosionFromPool(): ParticleSystemSet | null {
return this._explosionPool.pop() || null;
}
private static returnExplosionToPool(explosion: ParticleSystemSet) {
explosion.dispose();
ParticleHelper.CreateAsync("explosion", DefaultScene.MainScene).then((set) => {
this._explosionPool.push(set);
})
}
public static async createRock(i: number, position: Vector3, size: Vector3, public static async createRock(i: number, position: Vector3, size: Vector3,
score: Observable<ScoreEvent>): Promise<Rock> { score: Observable<ScoreEvent>): Promise<Rock> {
@ -89,9 +105,7 @@ export class RockFactory {
if (eventData.type == 'COLLISION_STARTED') { if (eventData.type == 'COLLISION_STARTED') {
if ( eventData.collidedAgainst.transformNode.id == 'ammo') { if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"}); score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"});
const explosion = ParticleSystemSet.Parse(this._explosion, DefaultScene.MainScene, false, 10);
const position = eventData.point; const position = eventData.point;
// _explosion.emitterNode = position;
eventData.collider.shape.dispose(); eventData.collider.shape.dispose();
eventData.collider.transformNode.dispose(); eventData.collider.transformNode.dispose();
@ -101,27 +115,46 @@ export class RockFactory {
eventData.collidedAgainst.transformNode.dispose(); eventData.collidedAgainst.transformNode.dispose();
eventData.collidedAgainst.dispose(); eventData.collidedAgainst.dispose();
const ball = MeshBuilder.CreateBox("ball", {size: .01}, DefaultScene.MainScene); // Get explosion from pool (or create new if pool empty)
let explosion = RockFactory.getExplosionFromPool();
ball.scaling = new Vector3(.4, .4, .4); if (!explosion) {
ball.position = position; console.log("Pool empty, creating new explosion");
//const material = new StandardMaterial("ball-material", DefaultScene.MainScene); ParticleHelper.CreateAsync("explosion", DefaultScene.MainScene).then((set) => {
//material.emissiveColor = Color3.Yellow(); const point = MeshBuilder.CreateSphere("point", {diameter: 0.1}, DefaultScene.MainScene);
//ball.material = material; point.position = position.clone();
//point.isVisible = false;
explosion.start(ball); set.start(point);
setTimeout(() => { setTimeout(() => {
explosion.systems.forEach((system: ParticleSystem) => { set.dispose();
system.stop(); point.dispose();
system.dispose(true, true, true); }, 2000);
}); });
explosion.dispose(); } else {
if (ball && !ball.isDisposed()) { // Use pooled explosion
ball.dispose(false, true); const point = MeshBuilder.CreateSphere("point", {diameter: 10}, DefaultScene.MainScene);
} point.position = position.clone();
//ball.dispose(); //point.isVisible = false;
}, 1500);
console.log("Using pooled explosion with", explosion.systems.length, "systems at", position);
// Set emitter and start each system individually
explosion.systems.forEach((system: ParticleSystem, idx: number) => {
system.emitter = point; // Set emitter to the collision point
system.start(); // Start this specific system
console.log(` System ${idx}: emitter set to`, system.emitter, "activeCount=", system.getActiveCount());
});
setTimeout(() => {
explosion.systems.forEach((system: ParticleSystem) => {
system.stop();
});
RockFactory.returnExplosionToPool(explosion);
point.dispose();
}, 2000);
}
} }
} }
}); });

91
styles.css Normal file
View File

@ -0,0 +1,91 @@
#levelSelect {
text-align: center;
padding: 20px;
opacity: 0;
transition: opacity 0.5s ease-in;
}
#levelSelect.ready {
opacity: 1;
}
#levelSelect h1 {
color: #fff;
font-size: 2.5em;
margin-bottom: 30px;
text-shadow: 0 0 10px rgba(0, 150, 255, 0.8);
}
.card-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.level-card {
background: rgba(20, 20, 40, 0.9);
border: 2px solid rgba(0, 150, 255, 0.5);
border-radius: 12px;
padding: 30px 20px;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.level-card:hover {
transform: translateY(-5px);
border-color: rgba(0, 200, 255, 0.8);
box-shadow: 0 8px 16px rgba(0, 150, 255, 0.4);
}
.level-card h2 {
color: #00d4ff;
font-size: 1.8em;
margin-bottom: 15px;
text-transform: uppercase;
}
.level-card p {
color: #ccc;
font-size: 1em;
line-height: 1.6;
margin-bottom: 20px;
min-height: 50px;
}
.level-button {
background: linear-gradient(135deg, #0066cc, #0099ff);
color: white;
border: none;
padding: 12px 30px;
font-size: 1.1em;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
}
.level-button:hover {
background: linear-gradient(135deg, #0088ff, #00bbff);
box-shadow: 0 4px 12px rgba(0, 150, 255, 0.6);
transform: scale(1.05);
}
.level-button:active {
transform: scale(0.98);
}
@media (max-width: 768px) {
.card-container {
grid-template-columns: 1fr;
gap: 15px;
}
#levelSelect h1 {
font-size: 2em;
}
}

View File

@ -22,10 +22,10 @@ export default defineConfig({
} }
}, },
server: { server: {
port: 3001, port: 3000,
}, },
preview: { preview: {
port: 3001, port: 3000,
}, },
base: "/" base: "/"