From d2aec0a87b7dbc18b1d8cfcb599931760a1c5e1a Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Fri, 17 Oct 2025 09:05:18 -0500 Subject: [PATCH] Add difficulty levels and upgrade BabylonJS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- index.html | 32 ++++++++- package-lock.json | 131 +++++++++++++++++----------------- package.json | 14 ++-- public/systems/explosion.json | 2 +- src/level1.ts | 84 ++++++++++++++++++++-- src/main.ts | 48 +++++++------ src/starfield.ts | 83 ++++++++++++++------- styles.css | 91 +++++++++++++++++++++++ vite.config.ts | 4 +- 9 files changed, 363 insertions(+), 126 deletions(-) create mode 100644 styles.css diff --git a/index.html b/index.html index 05dd31c..e69a092 100644 --- a/index.html +++ b/index.html @@ -19,9 +19,37 @@
Loading...
- +
+

Select Your Level

+
+
+

Recruit

+

Perfect for beginners. Learn the basics of space combat.

+ +
+
+

Pilot

+

Intermediate challenge. Face tougher enemies and obstacles.

+ +
+
+

Captain

+

Advanced difficulty. Command your ship with precision.

+ +
+
+

Commander

+

Expert mode. Only for the most skilled space warriors.

+ +
+
+

Test

+

Testing mode. Many large, slow-moving asteroids.

+ +
+
+
- diff --git a/package-lock.json b/package-lock.json index a86e91a..1b8be2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,14 @@ "name": "space-game", "version": "0.0.1", "dependencies": { - "@babylonjs/core": "7.13.1", - "@babylonjs/gui": "^7.13.1", + "@babylonjs/core": "8.32.0", + "@babylonjs/gui": "^8.32.0", "@babylonjs/havok": "1.3.5", - "@babylonjs/inspector": "^7.13.1", - "@babylonjs/loaders": "^7.13.1", - "@babylonjs/materials": "^7.13.1", - "@babylonjs/procedural-textures": "^7.13.1", - "@babylonjs/serializers": "^7.13.1", + "@babylonjs/inspector": "8.32.0", + "@babylonjs/loaders": "8.32.0", + "@babylonjs/materials": "8.32.0", + "@babylonjs/procedural-textures": "8.32.0", + "@babylonjs/serializers": "8.32.0", "openai": "4.52.3" }, "devDependencies": { @@ -23,27 +23,36 @@ "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": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-7.13.1.tgz", - "integrity": "sha512-limlsRIhRBH9xsuUNsy9xAyi0jhfQxfvhlMzMjFK3Ugq4c7joYpoZMkQU038esOQ3aq3q8VPv1+CshE3NASEMQ==" + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-8.32.0.tgz", + "integrity": "sha512-Z83WIe2eZEAOo5bb9Tjd+lY4ru6N8qgtZJGjWcoXOiP3BrtbatPUXdVKqm7m60ItQABFaVdMGygvIXY+wNXU/Q==" }, "node_modules/@babylonjs/gui": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@babylonjs/gui/-/gui-7.13.1.tgz", - "integrity": "sha512-FW2QOqpzoJI2q/hFKOa7O7xnj6oKzHWmTGHVhqjg7jzZy//bMZ5AbxNdMpQgDa+QoENpd7r+yx1O4kc4Lw1UaQ==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@babylonjs/gui/-/gui-8.32.0.tgz", + "integrity": "sha512-nR+E3u3hgGky+/6k1h8F5B4tS4OW6x3y63hf88kS3GgANd8as2iL54NC1Z74a25/8/nlaSRhodEYwx5O7lZKlQ==", "peerDependencies": { - "@babylonjs/core": "^7.0.0" + "@babylonjs/core": "^8.0.0" } }, "node_modules/@babylonjs/gui-editor": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@babylonjs/gui-editor/-/gui-editor-7.13.1.tgz", - "integrity": "sha512-1piYccMR4FRqMXVu/OMqCgc6Z5uRpt49u6yUYyRY627aZkarBM7oh310A6sgTkfuk77c9ZNoIgtgbq5P/dB48Q==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@babylonjs/gui-editor/-/gui-editor-8.32.0.tgz", + "integrity": "sha512-sRql2tCW+dUulQxWbzSOskGsgDUGYHJ5mM0eLbsKPFTCggURse3drNtynOw/24dO/OCaES0zO0Ob/nDH0qrFxA==", "peer": true, "peerDependencies": { - "@babylonjs/core": "^7.0.0", - "@babylonjs/gui": "^7.0.0", + "@babylonjs/core": "^8.0.0", + "@babylonjs/gui": "^8.0.0", "@types/react": ">=16.7.3", "@types/react-dom": ">=16.0.9" } @@ -57,57 +66,58 @@ } }, "node_modules/@babylonjs/inspector": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@babylonjs/inspector/-/inspector-7.13.1.tgz", - "integrity": "sha512-BM2ZQu15ESbWFQPwU/iqCx1P/KiJX9qX9dCVU/+5bbLqzemja5aDajLMnM+vb4uimAtbokkUdFqNckGnZVo7VQ==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@babylonjs/inspector/-/inspector-8.32.0.tgz", + "integrity": "sha512-7crFgaQmKmcgh69lf0cB6r47UjhL8SYZgL6UbHjmHy3VPrj6Xde+oN86S+BG+kalKfPLoaTdBLz1+b0AYa4Xxw==", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.1.0", "@fortawesome/free-regular-svg-icons": "^6.0.0", "@fortawesome/free-solid-svg-icons": "^6.0.0" }, "peerDependencies": { - "@babylonjs/core": "^7.0.0", - "@babylonjs/gui": "^7.0.0", - "@babylonjs/gui-editor": "^7.0.0", - "@babylonjs/loaders": "^7.0.0", - "@babylonjs/materials": "^7.0.0", - "@babylonjs/serializers": "^7.0.0", + "@babylonjs/addons": "^8.0.0", + "@babylonjs/core": "^8.0.0", + "@babylonjs/gui": "^8.0.0", + "@babylonjs/gui-editor": "^8.0.0", + "@babylonjs/loaders": "^8.0.0", + "@babylonjs/materials": "^8.0.0", + "@babylonjs/serializers": "^8.0.0", "@types/react": ">=16.7.3", "@types/react-dom": ">=16.0.9" } }, "node_modules/@babylonjs/loaders": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-7.13.1.tgz", - "integrity": "sha512-17MCwYpMM4EF69wzsclIvs7Ci84lYdkcH2NSM3hD3phl8k373yNKVvBNrs68p9yZUCFbXIVEH1tl5QQxr2T37g==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@babylonjs/loaders/-/loaders-8.32.0.tgz", + "integrity": "sha512-H2tKP2z5la0cWkkhVDEVUNSW3n187G2ti6G9OlFXOjr2SBzEWvDfsxL0je94z2SLw8LGH0Y6hBPGP1k/p2/YSg==", "peerDependencies": { - "@babylonjs/core": "^7.0.0", - "babylonjs-gltf2interface": "^7.0.0" + "@babylonjs/core": "^8.0.0", + "babylonjs-gltf2interface": "^8.0.0" } }, "node_modules/@babylonjs/materials": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@babylonjs/materials/-/materials-7.13.1.tgz", - "integrity": "sha512-CkjHyhsMglr9Aqf5NNsqwCgVPdMxnu1yRD4I6CGDXAp814ghqb5/JwkoL4lgelSYinsSh4HXpFr4Jgays5W2hw==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@babylonjs/materials/-/materials-8.32.0.tgz", + "integrity": "sha512-p+RvvzC4o01quumcNOwTgkiYn/v1BTDTRJTEQp80GcCi95weKLdzhwuR2FsUlL5xaPRLrOrd2ld634ZSjOW9tA==", "peerDependencies": { - "@babylonjs/core": "^7.0.0" + "@babylonjs/core": "^8.6.0" } }, "node_modules/@babylonjs/procedural-textures": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@babylonjs/procedural-textures/-/procedural-textures-7.13.1.tgz", - "integrity": "sha512-FzPtNsEglAi/0rqSEGBsnCAMn6Ggnf0phYSTOL7iHec+Rhx+1DVwN5PvUBYAJT5Sy5PIq2mPZI1uhq1A5QySxA==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@babylonjs/procedural-textures/-/procedural-textures-8.32.0.tgz", + "integrity": "sha512-NiE+F2x1Cc1IvUPwEGV4ckAUsq457G0Wqx0w4vpdEyWhbdYaGa78OpQO2RwT4I02Y58zxrpaBJPydQTn8XN23g==", "peerDependencies": { - "@babylonjs/core": "^7.0.0" + "@babylonjs/core": "^8.0.0" } }, "node_modules/@babylonjs/serializers": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@babylonjs/serializers/-/serializers-7.13.1.tgz", - "integrity": "sha512-ct1MTjRZaj4EEyMhDiHJkhSSKJRGValGPLNv3UOYh7EYc4XN0HbEz4wuMkwlgENFP7Hz5okWVBQB9yQWObR3Sg==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@babylonjs/serializers/-/serializers-8.32.0.tgz", + "integrity": "sha512-U+D10S6i4fzukfCBPJD8e1NmjUSNDWS46AUf9v1/3O7XiVOt3yN5g5jZl50oBiYwQvyYs0FBWZbLdBfuREj1cg==", "peerDependencies": { - "@babylonjs/core": "^7.0.0", - "babylonjs-gltf2interface": "^7.0.0" + "@babylonjs/core": "^8.0.0", + "babylonjs-gltf2interface": "^8.0.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -759,29 +769,22 @@ "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": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "peer": true, "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "peer": true, - "dependencies": { - "@types/react": "*" + "peerDependencies": { + "@types/react": "^19.2.0" } }, "node_modules/abort-controller": { @@ -812,9 +815,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/babylonjs-gltf2interface": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-7.13.1.tgz", - "integrity": "sha512-kgMZrek1Gul22+Igy43pYdofg89odPv5uxYrjzryVvMxmzPI7NwgxickXT3tM/SGoyF0AoXlPrKLCK5zHT0/eg==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-8.32.0.tgz", + "integrity": "sha512-OECfOlxbIXHp4kYzqZNj42e0I5MfVAmgAgBkQaFGJdojK+hc/iIg9LQfAhYbKjr+Rpb1v1HnQ8tWBjXLwgtyXg==", "peer": true }, "node_modules/combined-stream": { diff --git a/package.json b/package.json index 7d8ef28..334cf8c 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,14 @@ "speech": "tsc && node ./dist/server/voices.js" }, "dependencies": { - "@babylonjs/core": "7.13.1", - "@babylonjs/gui": "^7.13.1", + "@babylonjs/core": "8.32.0", + "@babylonjs/gui": "^8.32.0", "@babylonjs/havok": "1.3.5", - "@babylonjs/inspector": "^7.13.1", - "@babylonjs/loaders": "^7.13.1", - "@babylonjs/materials": "^7.13.1", - "@babylonjs/serializers": "^7.13.1", - "@babylonjs/procedural-textures": "^7.13.1", + "@babylonjs/inspector": "8.32.0", + "@babylonjs/loaders": "8.32.0", + "@babylonjs/materials": "8.32.0", + "@babylonjs/serializers": "8.32.0", + "@babylonjs/procedural-textures": "8.32.0", "openai": "4.52.3" }, "devDependencies": { diff --git a/public/systems/explosion.json b/public/systems/explosion.json index 192a133..3ffe76c 100644 --- a/public/systems/explosion.json +++ b/public/systems/explosion.json @@ -434,7 +434,7 @@ [ { "name": "plumeAnimation", - "property": "emitter.y", + "property": "emitter.position.y", "framePerSecond": 60, "dataType": 0, "loopBehavior": 2, diff --git a/src/level1.ts b/src/level1.ts index d869045..e005d5b 100644 --- a/src/level1.ts +++ b/src/level1.ts @@ -24,8 +24,19 @@ export class Level1 implements Level { private _startBase: AbstractMesh; private _endBase: AbstractMesh; 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._scoreboard = new Scoreboard(); 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 { return this._onReadyObservable; } @@ -65,9 +135,13 @@ export class Level1 implements Level { this._ship.position = new Vector3(0, 1, 0); await RockFactory.init(); - for (let i = 0; i < 5; i++) { - const dist = (Math.random() * 50) + 190; - const size = Vector3.Random(1,1.3).scale(Math.random() * 5 + 5) + const config = this._difficultyConfig; + console.log(config); + 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), size, @@ -94,7 +168,7 @@ export class Level1 implements Level { message: "Get Ready" }); 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); } } diff --git a/src/main.ts b/src/main.ts index e423d98..5b87615 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,6 +28,8 @@ export class Main { private _loadingDiv: HTMLElement; private _currentLevel: Level; private _gameState: GameState = GameState.DEMO; + private _selectedDifficulty: string = 'recruit'; + private _engine: Engine | WebGPUEngine; constructor() { this._loadingDiv = document.querySelector('#loadingDiv'); if (!navigator.xr) { @@ -36,10 +38,19 @@ export class Main { } this.initialize(); - document.querySelector('#startButton').addEventListener('click', () => { - Engine.audioEngine.unlock(); - this.play(); - document.querySelector('#mainDiv').remove(); + document.querySelectorAll('.level-button').forEach(button => { + button.addEventListener('click', (e) => { + const levelButton = e.target as HTMLButtonElement; + 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; @@ -63,11 +74,6 @@ export class Main { }); 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); photoDome1.material.diffuseTexture.hasAlpha = true; @@ -86,30 +92,32 @@ export class Main { } private async setupScene() { - let engine: WebGPUEngine | Engine = null; if (webGpu) { - engine = new WebGPUEngine(canvas); - await (engine as WebGPUEngine).initAsync(); + this._engine = new WebGPUEngine(canvas); + await (this._engine as WebGPUEngine).initAsync(); } 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 = () => { - engine.resize(); + this._engine.resize(); } - DefaultScene.DemoScene = new Scene(engine); - DefaultScene.MainScene = new Scene(engine); + DefaultScene.DemoScene = new Scene(this._engine); + DefaultScene.MainScene = new Scene(this._engine); DefaultScene.MainScene.ambientColor = new Color3(.2, .2, .2); this.setLoadingMessage("Initializing Physics Engine.."); await this.setupPhysics(); + this.setLoadingMessage("Physics Engine Ready!"); this.setupInspector(); - engine.runRenderLoop(() => { + this._engine.runRenderLoop(() => { if (!this._started) { this._started = true; this._loadingDiv.remove(); - const start = document.querySelector('#startButton'); - start.classList.add('ready'); + const levelSelect = document.querySelector('#levelSelect'); + if (levelSelect) { + levelSelect.classList.add('ready'); + } } if (this._gameState == GameState.PLAY) { DefaultScene.MainScene.render(); diff --git a/src/starfield.ts b/src/starfield.ts index 773248a..fb6d938 100644 --- a/src/starfield.ts +++ b/src/starfield.ts @@ -32,14 +32,18 @@ export class Rock { export class RockFactory { private static _rockMesh: AbstractMesh; private static _rockMaterial: PBRMaterial; - private static _explosion: ParticleSystemSet; - public static async init() { + private static _explosionPool: ParticleSystemSet[] = []; + private static _poolSize: number = 10; - if (!this._explosion) { - const set = await ParticleHelper.CreateAsync("explosion", DefaultScene.MainScene); - this._explosion = set.serialize(true); - set.dispose(); + public static async init() { + // Pre-create explosion particle systems for pooling + console.log("Pre-creating explosion particle systems..."); + 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) { console.log('loading mesh'); 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, score: Observable): Promise { @@ -89,9 +105,7 @@ export class RockFactory { if (eventData.type == 'COLLISION_STARTED') { if ( eventData.collidedAgainst.transformNode.id == 'ammo') { score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"}); - const explosion = ParticleSystemSet.Parse(this._explosion, DefaultScene.MainScene, false, 10); const position = eventData.point; - // _explosion.emitterNode = position; eventData.collider.shape.dispose(); eventData.collider.transformNode.dispose(); @@ -101,27 +115,46 @@ export class RockFactory { eventData.collidedAgainst.transformNode.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); - ball.position = position; - //const material = new StandardMaterial("ball-material", DefaultScene.MainScene); - //material.emissiveColor = Color3.Yellow(); - //ball.material = material; + if (!explosion) { + console.log("Pool empty, creating new explosion"); + ParticleHelper.CreateAsync("explosion", DefaultScene.MainScene).then((set) => { + const point = MeshBuilder.CreateSphere("point", {diameter: 0.1}, DefaultScene.MainScene); + point.position = position.clone(); + //point.isVisible = false; - explosion.start(ball); + set.start(point); - setTimeout(() => { - explosion.systems.forEach((system: ParticleSystem) => { - system.stop(); - system.dispose(true, true, true); + setTimeout(() => { + set.dispose(); + point.dispose(); + }, 2000); }); - explosion.dispose(); - if (ball && !ball.isDisposed()) { - ball.dispose(false, true); - } - //ball.dispose(); - }, 1500); + } else { + // Use pooled explosion + const point = MeshBuilder.CreateSphere("point", {diameter: 10}, DefaultScene.MainScene); + point.position = position.clone(); + //point.isVisible = false; + + 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); + } } } }); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..4c85802 --- /dev/null +++ b/styles.css @@ -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; + } +} diff --git a/vite.config.ts b/vite.config.ts index 7d638ad..f4ac079 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,10 +22,10 @@ export default defineConfig({ } }, server: { - port: 3001, + port: 3000, }, preview: { - port: 3001, + port: 3000, }, base: "/"