diff --git a/CLAUDE.md b/CLAUDE.md index 31bee1c..1bead15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,7 +119,7 @@ src/ level.ts - Level interface level1.ts - Main game level implementation ship.ts - Player ship, controls, weapons - starfield.ts - Rock factory and collision handling + rockFactory.ts - Rock factory and collision handling scoreboard.ts - In-cockpit HUD display createSun.ts - Sun mesh generation createPlanets.ts - Procedural planet generation diff --git a/public/asteroid3.blend b/public/asteroid3.blend new file mode 100644 index 0000000..85e1aaf Binary files /dev/null and b/public/asteroid3.blend differ diff --git a/public/asteroid3.glb b/public/asteroid3.glb new file mode 100644 index 0000000..8f67993 Binary files /dev/null and b/public/asteroid3.glb differ diff --git a/src/backgroundStars.ts b/src/backgroundStars.ts index fa9a825..4bde1f2 100644 --- a/src/backgroundStars.ts +++ b/src/backgroundStars.ts @@ -1,4 +1,4 @@ -import {Color3, Color4, PointsCloudSystem, Scene, Vector3} from "@babylonjs/core"; +import {Color3, Color4, PointsCloudSystem, Scene, StandardMaterial, Vector3} from "@babylonjs/core"; /** * Configuration options for background stars @@ -95,11 +95,12 @@ export class BackgroundStars { const mesh = this.pcs.mesh; if (mesh) { // Stars should not receive lighting - mesh.material.disableLighting = true; - mesh.material.emissiveColor = new Color3(1,1,1); + const mat = (mesh.material as StandardMaterial) + mat.disableLighting = true; + mat.emissiveColor = new Color3(1,1,1); // Disable depth write so stars don't occlude other objects - mesh.material.disableDepthWrite = true; + mat.disableDepthWrite = true; // Stars should be in the background mesh.renderingGroupId = 0; diff --git a/src/level1.ts b/src/level1.ts index 4a105c1..e443565 100644 --- a/src/level1.ts +++ b/src/level1.ts @@ -14,7 +14,7 @@ import { import type {AudioEngineV2} from "@babylonjs/core"; import {Ship} from "./ship"; -import {RockFactory} from "./starfield"; +import {RockFactory} from "./rockFactory"; import Level from "./level"; import {Scoreboard} from "./scoreboard"; import setLoadingMessage from "./setLoadingMessage"; diff --git a/src/levelDeserializer.ts b/src/levelDeserializer.ts index 0a51cf2..534481f 100644 --- a/src/levelDeserializer.ts +++ b/src/levelDeserializer.ts @@ -13,7 +13,7 @@ import { Vector3 } from "@babylonjs/core"; import { DefaultScene } from "./defaultScene"; -import { RockFactory } from "./starfield"; +import { RockFactory } from "./rockFactory"; import { ScoreEvent } from "./scoreboard"; import { LevelConfig, diff --git a/src/main.ts b/src/main.ts index dfd3a4e..853e446 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,7 +19,7 @@ import {Level1} from "./level1"; import Demo from "./demo"; import Level from "./level"; import setLoadingMessage from "./setLoadingMessage"; -import {RockFactory} from "./starfield"; +import {RockFactory} from "./rockFactory"; import {ControllerDebug} from "./controllerDebug"; import {router, showView} from "./router"; import {populateLevelSelector, hasSavedLevels} from "./levelSelector"; diff --git a/src/starfield.ts b/src/rockFactory.ts similarity index 99% rename from src/starfield.ts rename to src/rockFactory.ts index 85271f7..bb15456 100644 --- a/src/starfield.ts +++ b/src/rockFactory.ts @@ -59,7 +59,7 @@ export class RockFactory { } private static async loadMesh() { console.log('loading mesh'); - const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "asteroid2.glb", DefaultScene.MainScene); + const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "asteroid3.glb", DefaultScene.MainScene); this._rockMesh = importMesh.meshes[1].clone("asteroid", null, false); this._rockMesh.setParent(null); this._rockMesh.setEnabled(false); diff --git a/src/ship.ts b/src/ship.ts index d72976f..6728ce6 100644 --- a/src/ship.ts +++ b/src/ship.ts @@ -22,6 +22,7 @@ import { import type {AudioEngineV2, StaticSound} from "@babylonjs/core"; import {DefaultScene} from "./defaultScene"; import { GameConfig } from "./gameConfig"; +import { Sight } from "./sight"; const MAX_FORWARD_THRUST = 40; const controllerComponents = [ @@ -69,6 +70,7 @@ export class Ship { private _controllerMode: ControllerStickMode; private _active = false; private _audioEngine: AudioEngineV2; + private _sight: Sight; constructor(mode: ControllerStickMode = ControllerStickMode.BEGINNER, audioEngine?: AudioEngineV2) { this._controllerMode = mode; this._audioEngine = audioEngine; @@ -154,7 +156,7 @@ export class Ship { } this._ammoMaterial = new StandardMaterial("ammoMaterial", DefaultScene.MainScene); this._ammoMaterial.emissiveColor = new Color3(1, 1, 0); - this._ammoBaseMesh = MeshBuilder.CreateSphere("bullet", {diameter: .2}, DefaultScene.MainScene); + this._ammoBaseMesh = MeshBuilder.CreateIcoSphere("bullet", {radius: .1, subdivisions: 2}, DefaultScene.MainScene); this._ammoBaseMesh.material = this._ammoMaterial; this._ammoBaseMesh.setEnabled(false); @@ -180,17 +182,17 @@ export class Ship { DefaultScene.MainScene.setActiveCameraByName("Flat Camera"); - //const sightPos = this._forwardNode.position.scale(30); - const sight = MeshBuilder.CreateDisc("sight", {radius: 2 }, DefaultScene.MainScene); + // Create sight reticle + this._sight = new Sight(DefaultScene.MainScene, this._ship, { + position: new Vector3(0, 2, 125), + circleRadius: 2, + crosshairLength: 1.5, + lineThickness: 0.1, + color: Color3.Green(), + renderingGroupId: 3, + centerGap: 0.5 + }); - sight.parent = this._ship - //sight.rotation.x = -Math.PI / 2; - const signtMaterial = new StandardMaterial("sightMaterial", DefaultScene.MainScene); - signtMaterial.emissiveColor = Color3.Yellow(); - signtMaterial.ambientColor = Color3.Yellow(); - sight.material = signtMaterial; - sight.position = new Vector3(0, 2, 125); - sight.renderingGroupId = 3; let i = 0; DefaultScene.MainScene.onBeforeRenderObservable.add(() => { if (i++ % 10 == 0) { @@ -527,6 +529,16 @@ export class Ship { } }); } + + /** + * Dispose of ship resources + */ + public dispose(): void { + if (this._sight) { + this._sight.dispose(); + } + // Add other cleanup as needed + } } function decrementValue(value: number, increment: number = .8): number { if (Math.abs(value) < .01) { diff --git a/src/sight.ts b/src/sight.ts new file mode 100644 index 0000000..412d77d --- /dev/null +++ b/src/sight.ts @@ -0,0 +1,188 @@ + +import { + Color3, + Mesh, + MeshBuilder, + Scene, + StandardMaterial, + TransformNode, + Vector3 +} from "@babylonjs/core"; + +/** + * Configuration options for the sight reticle + */ +export interface SightConfig { + /** Position relative to parent */ + position?: Vector3; + /** Circle radius */ + circleRadius?: number; + /** Crosshair line length */ + crosshairLength?: number; + /** Line thickness */ + lineThickness?: number; + /** Reticle color */ + color?: Color3; + /** Rendering group ID */ + renderingGroupId?: number; + /** Gap size in the center of the crosshair */ + centerGap?: number; +} + +/** + * Gun sight reticle with crosshair and circle + */ +export class Sight { + private reticleGroup: TransformNode; + private circle: Mesh; + private crosshairLines: Mesh[] = []; + private scene: Scene; + private config: Required; + + // Default configuration + private static readonly DEFAULT_CONFIG: Required = { + position: new Vector3(0, 2, 125), + circleRadius: 2, + crosshairLength: 1.5, + lineThickness: 0.1, + color: Color3.Green(), + renderingGroupId: 3, + centerGap: 0.5 + }; + + constructor(scene: Scene, parent: TransformNode, config?: SightConfig) { + this.scene = scene; + this.config = { ...Sight.DEFAULT_CONFIG, ...config }; + this.createReticle(parent); + } + + /** + * Create the reticle (circle + crosshair) + */ + private createReticle(parent: TransformNode): void { + // Create a parent node for the entire reticle + this.reticleGroup = new TransformNode("sightReticle", this.scene); + this.reticleGroup.parent = parent; + this.reticleGroup.position = this.config.position; + + // Create material + const material = new StandardMaterial("sightMaterial", this.scene); + material.emissiveColor = this.config.color; + material.disableLighting = true; + material.alpha = 0.8; + + // Create outer circle + this.circle = MeshBuilder.CreateTorus("sightCircle", { + diameter: this.config.circleRadius * 2, + thickness: this.config.lineThickness, + tessellation: 64 + }, this.scene); + this.circle.parent = this.reticleGroup; + this.circle.material = material; + this.circle.renderingGroupId = this.config.renderingGroupId; + + // Create crosshair lines (4 lines extending from center gap) + this.createCrosshairLines(material); + } + + /** + * Create the crosshair lines (top, bottom, left, right) + */ + private createCrosshairLines(material: StandardMaterial): void { + const gap = this.config.centerGap; + const length = this.config.crosshairLength; + const thickness = this.config.lineThickness; + + // Top line + const topLine = MeshBuilder.CreateBox("crosshairTop", { + width: thickness, + height: length, + depth: thickness + }, this.scene); + topLine.parent = this.reticleGroup; + topLine.position.y = gap + length / 2; + topLine.material = material; + topLine.renderingGroupId = this.config.renderingGroupId; + this.crosshairLines.push(topLine); + + // Bottom line + const bottomLine = MeshBuilder.CreateBox("crosshairBottom", { + width: thickness, + height: length, + depth: thickness + }, this.scene); + bottomLine.parent = this.reticleGroup; + bottomLine.position.y = -(gap + length / 2); + bottomLine.material = material; + bottomLine.renderingGroupId = this.config.renderingGroupId; + this.crosshairLines.push(bottomLine); + + // Left line + const leftLine = MeshBuilder.CreateBox("crosshairLeft", { + width: length, + height: thickness, + depth: thickness + }, this.scene); + leftLine.parent = this.reticleGroup; + leftLine.position.x = -(gap + length / 2); + leftLine.material = material; + leftLine.renderingGroupId = this.config.renderingGroupId; + this.crosshairLines.push(leftLine); + + // Right line + const rightLine = MeshBuilder.CreateBox("crosshairRight", { + width: length, + height: thickness, + depth: thickness + }, this.scene); + rightLine.parent = this.reticleGroup; + rightLine.position.x = gap + length / 2; + rightLine.material = material; + rightLine.renderingGroupId = this.config.renderingGroupId; + this.crosshairLines.push(rightLine); + + // Center dot (optional, very small) + const centerDot = MeshBuilder.CreateSphere("crosshairCenter", { + diameter: thickness * 1.5 + }, this.scene); + centerDot.parent = this.reticleGroup; + centerDot.material = material; + centerDot.renderingGroupId = this.config.renderingGroupId; + this.crosshairLines.push(centerDot); + } + + /** + * Set visibility of the sight + */ + public setVisible(visible: boolean): void { + this.circle.isVisible = visible; + this.crosshairLines.forEach(line => line.isVisible = visible); + } + + /** + * Change the sight color + */ + public setColor(color: Color3): void { + this.config.color = color; + const material = this.circle.material as StandardMaterial; + if (material) { + material.emissiveColor = color; + } + } + + /** + * Get the reticle group transform node + */ + public getTransformNode(): TransformNode { + return this.reticleGroup; + } + + /** + * Dispose of the sight + */ + public dispose(): void { + this.circle.dispose(); + this.crosshairLines.forEach(line => line.dispose()); + this.reticleGroup.dispose(); + } +}