space-game/src/ship.ts
Michael Mainguy 5f3fcf6bc0
All checks were successful
Build / build (push) Successful in 1m19s
Adjust lighting, physics, and visual settings for improved gameplay
Increase asteroid sizes, add ship-specific lighting, enhance sun brightness, adjust physics timesteps, and improve planet/asteroid material appearance. Increase planet count to 12 and move them farther from sun for better spatial layout.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 13:19:01 -05:00

498 lines
18 KiB
TypeScript

import {
AbstractMesh,
Color3,
DirectionalLight,
FreeCamera,
GlowLayer, InstancedMesh, Mesh,
MeshBuilder,
Observable,
PhysicsAggregate,
PhysicsMotionType,
PhysicsShapeType, PointLight,
SceneLoader,
SpotLight,
StandardMaterial,
TransformNode,
Vector2,
Vector3,
WebXRAbstractMotionController,
WebXRControllerComponent,
WebXRInputSource
} from "@babylonjs/core";
import type {AudioEngineV2, StaticSound} from "@babylonjs/core";
import {DefaultScene} from "./defaultScene";
const MAX_FORWARD_THRUST = 40;
const controllerComponents = [
'a-button',
'b-button',
'x-button',
'y-button',
'thumbrest',
'xr-standard-squeeze',
'xr-standard-thumbstick',
'xr-standard-trigger',
]
type ControllerEvent = {
hand: 'right' | 'left' | 'none',
type: 'thumbstick' | 'button',
controller: WebXRAbstractMotionController,
component: WebXRControllerComponent,
value: number,
axisData: { x: number, y: number },
pressed: boolean,
touched: boolean
}
enum ControllerStickMode {
BEGINNER,
ARCADE,
REALISTIC
}
export class Ship {
private _ship: TransformNode;
private _controllerObservable: Observable<ControllerEvent> = new Observable<ControllerEvent>();
private _ammoMaterial: StandardMaterial;
private _forwardNode: TransformNode;
private _rotationNode: TransformNode;
private _glowLayer: GlowLayer;
private _primaryThrustVectorSound: StaticSound;
private _secondaryThrustVectorSound: StaticSound;
private _shot: StaticSound;
private _primaryThrustPlaying: boolean = false;
private _secondaryThrustPlaying: boolean = false;
private _shooting: boolean = false;
private _camera: FreeCamera;
private _ammoBaseMesh: AbstractMesh;
private _controllerMode: ControllerStickMode;
private _active = false;
private _audioEngine: AudioEngineV2;
constructor(mode: ControllerStickMode = ControllerStickMode.BEGINNER, audioEngine?: AudioEngineV2) {
this._controllerMode = mode;
this._audioEngine = audioEngine;
this.setup();
this.initialize();
}
public set controllerMode(mode: ControllerStickMode) {
this._controllerMode = mode;
}
private async initializeSounds() {
if (!this._audioEngine) return;
this._primaryThrustVectorSound = await this._audioEngine.createSoundAsync("thrust", "/thrust5.mp3", {
loop: true,
volume: .2
});
this._secondaryThrustVectorSound = await this._audioEngine.createSoundAsync("thrust2", "/thrust5.mp3", {
loop: true,
volume: 0.5
});
this._shot = await this._audioEngine.createSoundAsync("shot", "/shot.mp3", {
loop: false,
volume: 0.5
});
}
private shoot() {
this._shot?.play();
const ammo = new InstancedMesh("ammo", this._ammoBaseMesh as Mesh);
ammo.parent = this._ship;
ammo.position.y = 2;
ammo.rotation.x = Math.PI / 2;
ammo.setParent(null);
const ammoAggregate = new PhysicsAggregate(ammo, PhysicsShapeType.CONVEX_HULL, {
mass: 1000,
restitution: 0
}, DefaultScene.MainScene);
ammoAggregate.body.setAngularDamping(1);
ammoAggregate.body.setMotionType(PhysicsMotionType.DYNAMIC);
ammoAggregate.body.setLinearVelocity(this._ship.forward.scale(10000))
//.add(this._ship.physicsBody.getLinearVelocity()));
window.setTimeout(() => {
ammoAggregate.dispose();
ammo.dispose()
}, 1500);
}
public set position(newPosition: Vector3) {
const body = this._ship.physicsBody;
body.disablePreStep = false;
body.transformNode.position.copyFrom(newPosition);
DefaultScene.MainScene.onAfterRenderObservable.addOnce(() => {
body.disablePreStep = true;
})
}
private setup() {
this._ship = new TransformNode("ship", DefaultScene.MainScene);
this._glowLayer = new GlowLayer('bullets', DefaultScene.MainScene);
this._glowLayer.intensity = 1;
// Create sounds asynchronously if audio engine is available
if (this._audioEngine) {
this.initializeSounds();
}
this._ammoMaterial = new StandardMaterial("ammoMaterial", DefaultScene.MainScene);
this._ammoMaterial.emissiveColor = new Color3(1, 1, 0);
this._ammoBaseMesh = MeshBuilder.CreateCapsule("bullet", {radius: .1, height: 2.5}, DefaultScene.MainScene);
this._ammoBaseMesh.material = this._ammoMaterial;
this._ammoBaseMesh.setEnabled(false);
//const landingLight = new SpotLight("landingLight", new Vector3(0, 0, 0), new Vector3(0, -.5, .5), 1.5, .5, DefaultScene.MainScene);
// landingLight.parent = this._ship;
// landingLight.position.z = 5;
const agg = new PhysicsAggregate(this._ship, PhysicsShapeType.BOX, {
mass: 100,
extents: new Vector3(4, 4, 7.4),
center: new Vector3(0, 1, 1.8)
}, DefaultScene.MainScene);
agg.body.setMotionType(PhysicsMotionType.DYNAMIC);
agg.body.setLinearDamping(.1);
agg.body.setAngularDamping(.2);
agg.body.setAngularVelocity(new Vector3(0, 0, 0));
agg.body.setCollisionCallbackEnabled(true);
this.setupKeyboard();
this.setupMouse();
this._controllerObservable.add(this.controllerCallback);
this._forwardNode = new TransformNode("forward", DefaultScene.MainScene);
this._rotationNode = new TransformNode("rotation", DefaultScene.MainScene);
this._forwardNode.parent = this._ship;
this._rotationNode.parent = this._ship;
this._camera = new FreeCamera("Flat Camera",
new Vector3(0, .5, 0),
DefaultScene.MainScene);
this._camera.parent = this._ship;
DefaultScene.MainScene.setActiveCameraByName("Flat Camera");
//const sightPos = this._forwardNode.position.scale(30);
const sight = MeshBuilder.CreateSphere("sight", {diameter: 1}, DefaultScene.MainScene);
sight.parent = this._ship
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);
let i = Date.now();
DefaultScene.MainScene.onBeforeRenderObservable.add(() => {
if (Date.now() - i > 50 && this._active == true) {
this.applyForce();
i = Date.now();
}
});
this._active = true;
}
private async initialize() {
const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "cockpit2.glb", DefaultScene.MainScene);
const shipMesh = importMesh.meshes[0];
shipMesh.id = "shipMesh";
shipMesh.name = "shipMesh";
shipMesh.parent = this._ship;
shipMesh.rotation.y = Math.PI;
shipMesh.position.y = 1;
shipMesh.position.z = -1;
const light = new PointLight("ship.light", new Vector3(0, 1, .9), DefaultScene.MainScene);
light.intensity = 4;
light.includedOnlyMeshes = [shipMesh];
for (const mesh of shipMesh.getChildMeshes()) {
if (mesh.material.id.indexOf('glass') === -1) {
light.includedOnlyMeshes.push(mesh);
}
}
light.parent = this._ship;
DefaultScene.MainScene.getMaterialById('glass_mat.002').alpha = .4;
}
private _leftStickVector = Vector2.Zero().clone();
private _rightStickVector = Vector2.Zero().clone();
private _forwardValue = 0;
private _yawValue = 0;
private _rollValue = 0;
private _pitchValue = 0;
private _mouseDown = false;
private _mousePos = new Vector2(0, 0);
public get transformNode() {
return this._ship;
}
private applyForce() {
if (!this?._ship?.physicsBody) {
return;
}
const body = this._ship.physicsBody;
//If we're moving over MAX_FORWARD_THRUST, we can't add any more thrust,
//just continue at MAX_FORWARD_THRUST
if (Math.abs(this._forwardValue) > MAX_FORWARD_THRUST) {
this._forwardValue = Math.sign(this._forwardValue) * MAX_FORWARD_THRUST;
}
//if forward thrust is under 40 we can apply more thrust
if (Math.abs(this._forwardValue) <= MAX_FORWARD_THRUST) {
if (Math.abs(this._leftStickVector.y) > .1) {
if (this._primaryThrustVectorSound && !this._primaryThrustPlaying) {
this._primaryThrustVectorSound.play();
this._primaryThrustPlaying = true;
}
if (this._primaryThrustVectorSound) {
this._primaryThrustVectorSound.volume = Math.abs(this._leftStickVector.y);
}
this._forwardValue += this._leftStickVector.y * .8;
} else {
if (this._primaryThrustVectorSound && this._primaryThrustPlaying) {
this._primaryThrustVectorSound.stop();
this._primaryThrustPlaying = false;
}
this._forwardValue = decrementValue(this._forwardValue, .98);
}
}
this._yawValue = adjustStickValue(this._leftStickVector.x, this._yawValue);
this._rollValue = adjustStickValue(this._rightStickVector.x, this._rollValue);
this._pitchValue = adjustStickValue(this._rightStickVector.y, this._pitchValue);
this._forwardNode.position.z = this._forwardValue;
this._rotationNode.position.y = this._yawValue;
this._rotationNode.position.z = -this._rollValue;
this._rotationNode.position.x = -this._pitchValue;
const thrust2 = Math.abs(this._rightStickVector.y) +
Math.abs(this._rightStickVector.x) +
Math.abs(this._leftStickVector.x);
if (thrust2 > .01) {
if (this._secondaryThrustVectorSound && !this._secondaryThrustPlaying) {
this._secondaryThrustVectorSound.play();
this._secondaryThrustPlaying = true;
}
if (this._secondaryThrustVectorSound) {
this._secondaryThrustVectorSound.volume = thrust2 * .4;
}
} else {
if (this._secondaryThrustVectorSound && this._secondaryThrustPlaying) {
this._secondaryThrustVectorSound.stop();
this._secondaryThrustPlaying = false;
}
}
body.setAngularVelocity(this._rotationNode.absolutePosition.subtract(this._ship.absolutePosition));
body.setLinearVelocity(this._forwardNode.absolutePosition.subtract(this._ship.absolutePosition).scale(-1));
}
private controllerCallback = (controllerEvent: ControllerEvent) => {
// Log first few events to verify they're firing
if (Math.random() < 0.01) { // Only log 1% to avoid spam
console.log('Controller event:', controllerEvent.type, controllerEvent.hand,
controllerEvent.type === 'thumbstick' ? controllerEvent.axisData : controllerEvent.value);
}
if (controllerEvent.type == 'thumbstick') {
if (controllerEvent.hand == 'left') {
this._leftStickVector.x = controllerEvent.axisData.x;
this._leftStickVector.y = controllerEvent.axisData.y;
}
if (controllerEvent.hand == 'right') {
this._rightStickVector.x = controllerEvent.axisData.x;
this._rightStickVector.y = controllerEvent.axisData.y;
}
this.applyForce();
}
if (controllerEvent.type == 'button') {
if (controllerEvent.component.type == 'trigger') {
if (controllerEvent.value > .9 && !this._shooting) {
this._shooting = true;
this.shoot();
}
if (controllerEvent.value < .1) {
this._shooting = false;
}
}
}
}
private setupMouse() {
this._ship.getScene().onPointerDown = (evt) => {
this._mousePos.x = evt.x;
this._mousePos.y = evt.y;
this._mouseDown = true;
}
this._ship.getScene().onPointerUp = () => {
this._mouseDown = false;
}
this._ship.getScene().onPointerMove = () => {
};
this._ship.getScene().onPointerMove = (ev) => {
if (!this._mouseDown) {
return
}
;
const xInc = this._rightStickVector.x = (ev.x - this._mousePos.x) / 100;
const yInc = this._rightStickVector.y = (ev.y - this._mousePos.y) / 100;
if (Math.abs(xInc) <= 1) {
this._rightStickVector.x = xInc;
} else {
this._rightStickVector.x = Math.sign(xInc);
}
if (Math.abs(yInc) <= 1) {
this._rightStickVector.y = yInc;
} else {
this._rightStickVector.y = Math.sign(yInc);
}
};
}
private setupKeyboard() {
document.onkeyup = () => {
this._leftStickVector.y = 0;
this._leftStickVector.x = 0;
this._rightStickVector.y = 0;
this._rightStickVector.x = 0;
}
document.onkeydown = (ev) => {
switch (ev.key) {
case '1':
this._camera.position.x = 15;
this._camera.rotation.y = -Math.PI / 2;
break;
case ' ':
this.shoot();
break;
case 'e':
break;
case 'w':
this._leftStickVector.y = -1;
break;
case 's':
this._leftStickVector.y = 1;
break;
case 'a':
this._leftStickVector.x = -1;
break;
case 'd':
this._leftStickVector.x = 1;
break;
case 'ArrowUp':
this._rightStickVector.y = -1;
break;
case 'ArrowDown':
this._rightStickVector.y = 1;
break;
}
};
}
private _leftInputSource: WebXRInputSource;
private _rightInputSource: WebXRInputSource;
public addController(controller: WebXRInputSource) {
console.log('Ship.addController called for:', controller.inputSource.handedness);
if (controller.inputSource.handedness == "left") {
console.log('Adding left controller');
this._leftInputSource = controller;
this._leftInputSource.onMotionControllerInitObservable.add((motionController) => {
console.log('Left motion controller initialized:', motionController.handness);
this.mapMotionController(motionController);
});
// Check if motion controller is already initialized
if (controller.motionController) {
console.log('Left motion controller already initialized, mapping now');
this.mapMotionController(controller.motionController);
}
}
if (controller.inputSource.handedness == "right") {
console.log('Adding right controller');
this._rightInputSource = controller;
this._rightInputSource.onMotionControllerInitObservable.add((motionController) => {
console.log('Right motion controller initialized:', motionController.handness);
this.mapMotionController(motionController);
});
// Check if motion controller is already initialized
if (controller.motionController) {
console.log('Right motion controller already initialized, mapping now');
this.mapMotionController(controller.motionController);
}
}
}
private mapMotionController(controller: WebXRAbstractMotionController) {
console.log('Mapping motion controller:', controller.handness, 'Profile:', controller.profileId);
controllerComponents.forEach((component) => {
const comp = controller.components[component];
if (!comp) {
console.log(` Component ${component} not found on ${controller.handness} controller`);
return;
}
console.log(` Found component ${component} on ${controller.handness} controller`);
const observable = this._controllerObservable;
if (comp && comp.onAxisValueChangedObservable) {
comp.onAxisValueChangedObservable.add((axisData) => {
observable.notifyObservers({
controller: controller,
hand: controller.handness,
type: 'thumbstick',
component: comp,
value: comp.value,
axisData: {x: axisData.x, y: axisData.y},
pressed: comp.pressed,
touched: comp.touched
})
});
}
if (comp && comp.onButtonStateChangedObservable) {
comp.onButtonStateChangedObservable.add((component) => {
observable.notifyObservers({
controller: controller,
hand: controller.handness,
type: 'button',
component: comp,
value: component.value,
axisData: {x: component.axes.x, y: component.axes.y},
pressed: component.pressed,
touched: component.touched
});
});
}
});
}
}
function decrementValue(value: number, increment: number = .8): number {
if (Math.abs(value) < .01) {
return 0;
} else {
return value * increment;
}
}
function adjustStickValue(stickVector: number, thrustValue: number): number {
if (Math.abs(stickVector) > .03) {
return thrustValue + (Math.pow(stickVector, 3) * .1);
} else {
return decrementValue(thrustValue, .85);
}
}