Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ Characters in parentheses indicate shortcut keys.
* Chase camera - If it's turned on, the camera will automatically face the directon of vehicle's acceleration.
* Nonlinear scale - Renders objects in a fake scale so that they appear even if they would be smaller than a pixel in real scale. Don't worry - the simulation will always be performed in real scale.
* Units in KM - Shows distances in Orbital Elements panel in Kilometers instead of AUs.
* Bounce on Collision - If checked, the rocket will bounce off the surface of an object instead of crashing.
This is a state of saved game, so if you save it, it will be restored on load.


## How to build
Expand Down
Binary file modified screenshots/settings.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
190 changes: 112 additions & 78 deletions src/CelestialBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import apoapsisUrl from './images/apoapsis.png';
import periapsisUrl from './images/periapsis.png';
import { Settings } from './SettingsControl';
import { RotationButtons } from './RotationControl';
import { SendMessageCallback } from './MessageControl';
import GameState from './GameState';

export const AU = 149597871; // Astronomical unit in kilometers
const GMsun = 1.327124400e11 / AU / AU/ AU; // Product of gravitational constant (G) and Sun's mass (Msun)
Expand Down Expand Up @@ -39,6 +41,7 @@ export class CelestialBody{
getParent(){ return this.parent; }
GM: number;
radius: number;
stuck: boolean = false; /// Whether the object is stuck to the parent object, typically as a result of collision
apoapsis?: THREE.Sprite = null;
periapsis?: THREE.Sprite = null;
vertex?: THREE.Vector3 = null;
Expand Down Expand Up @@ -122,6 +125,7 @@ export class CelestialBody{
velocity: this.velocity,
quaternion: this.quaternion,
angularVelocity: this.angularVelocity,
stuck: this.stuck || undefined,
totalDeltaV: this.totalDeltaV || undefined,
ignitionCount: this.ignitionCount || undefined,
};
Expand All @@ -142,6 +146,7 @@ export class CelestialBody{
this.velocity = deserializeVector3(json.velocity);
this.quaternion = deserializeQuaternion(json.quaternion);
this.angularVelocity = deserializeVector3(json.angularVelocity);
this.stuck = json.stuck ?? false;
this.totalDeltaV = json.totalDeltaV || 0;
this.ignitionCount = json.ignitionCount || 0;
}
Expand Down Expand Up @@ -254,13 +259,6 @@ export class CelestialBody{
// Total rotation of the orbit
const rotation = planeRot.clone().multiply(AxisAngleQuaternion(0, 0, 1, this.argument_of_perihelion));

// Show orbit information
if(this === select_obj){
// Yes, building a whole table markup by string manipulation.
// I know it's inefficient, but it's easy to implement. I'm lazy.

}

// If eccentricity is over 1, the trajectory is a hyperbola.
// It could be parabola in case of eccentricity == 1, but we ignore
// this impractical case for now.
Expand Down Expand Up @@ -349,87 +347,123 @@ export class CelestialBody{

};

simulateBody(deltaTime: number, div: number, timescale: number, buttons: RotationButtons, select_obj?: CelestialBody){
simulateBody(gameState: GameState, deltaTime: number, div: number, timescale: number, buttons: RotationButtons, sendMessage?: SendMessageCallback){

function checkCollision(source: CelestialBody, target: CelestialBody, destination: THREE.Vector3,
targetPositionLocal: THREE.Vector3)
{
// Non-controllable objects will not collide for now
if(!source.controllable)
return;
const delta = destination.clone().sub(targetPositionLocal);
const rad2 = (source.radius + target.radius) * (source.radius + target.radius) / AU / AU;
if(delta.lengthSq() < rad2 && delta.dot(source.velocity) < 0.){
if(gameState.simulationSettings.bounce_on_collision){
const normal = delta.normalize();
// Perfect elastic collision (coefficient of restitution = 1.)
source.velocity.add(normal.multiplyScalar(-2. * normal.dot(source.velocity)));
}
else{
source.stuck = true;
sendMessage('<span style="color: #ff7f7f">The rocket has crashed to an object!<br>'
+ 'Load another scenario and try again!</span>', 20);
}
}
}

const select_obj = gameState.select_obj;

const children = this.children;
for(let i = 0; i < children.length;){
const a = children[i];
const sl = a.position.lengthSq();
if(sl !== 0){
const angleAcceleration = 1e-0;
const accel = a.position.clone().negate().normalize().multiplyScalar(deltaTime / div * a.parent.GM / sl);
if(select_obj === a && select_obj.controllable && timescale <= 1){
if(buttons.up) select_obj.angularVelocity.add(new THREE.Vector3(0, 0, 1).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(buttons.down) select_obj.angularVelocity.add(new THREE.Vector3(0, 0, -1).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(buttons.left) select_obj.angularVelocity.add(new THREE.Vector3(0, 1, 0).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(buttons.right) select_obj.angularVelocity.add(new THREE.Vector3(0, -1, 0).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(buttons.counterclockwise) select_obj.angularVelocity.add(new THREE.Vector3(1, 0, 0).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(buttons.clockwise) select_obj.angularVelocity.add(new THREE.Vector3(-1, 0, 0).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(!buttons.up && !buttons.down && !buttons.left && !buttons.right && !buttons.counterclockwise && !buttons.clockwise){
// Immediately stop micro-rotation if the body is controlled.
// This is done to make it still in larger timescale, since micro-rotation cannot be canceled
// by product of angularVelocity and quaternion which underflows by square.
// Think that the vehicle has a momentum wheels that cancels micro-rotation continuously working.
if(1e-6 < select_obj.angularVelocity.lengthSq())
select_obj.angularVelocity.add(select_obj.angularVelocity.clone().normalize().multiplyScalar(-angleAcceleration * deltaTime / div));
else
select_obj.angularVelocity.set(0, 0, 0);
if(!a.stuck){
if(sl !== 0){
const angleAcceleration = 1e-0;
const accel = a.position.clone().negate().normalize().multiplyScalar(deltaTime / div * a.parent.GM / sl);
if(select_obj === a && select_obj.controllable && timescale <= 1){
if(buttons.up) select_obj.angularVelocity.add(new THREE.Vector3(0, 0, 1).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(buttons.down) select_obj.angularVelocity.add(new THREE.Vector3(0, 0, -1).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(buttons.left) select_obj.angularVelocity.add(new THREE.Vector3(0, 1, 0).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(buttons.right) select_obj.angularVelocity.add(new THREE.Vector3(0, -1, 0).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(buttons.counterclockwise) select_obj.angularVelocity.add(new THREE.Vector3(1, 0, 0).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(buttons.clockwise) select_obj.angularVelocity.add(new THREE.Vector3(-1, 0, 0).applyQuaternion(select_obj.quaternion).multiplyScalar(angleAcceleration * deltaTime / div));
if(!buttons.up && !buttons.down && !buttons.left && !buttons.right && !buttons.counterclockwise && !buttons.clockwise){
// Immediately stop micro-rotation if the body is controlled.
// This is done to make it still in larger timescale, since micro-rotation cannot be canceled
// by product of angularVelocity and quaternion which underflows by square.
// Think that the vehicle has a momentum wheels that cancels micro-rotation continuously working.
if(1e-6 < select_obj.angularVelocity.lengthSq())
select_obj.angularVelocity.add(select_obj.angularVelocity.clone().normalize().multiplyScalar(-angleAcceleration * deltaTime / div));
else
select_obj.angularVelocity.set(0, 0, 0);
}
if(0 < select_obj.throttle){
const deltaV = acceleration * select_obj.throttle * deltaTime / div;
select_obj.velocity.add(new THREE.Vector3(1, 0, 0).applyQuaternion(select_obj.quaternion).multiplyScalar(deltaV));
select_obj.totalDeltaV += deltaV;
}
}
if(0 < select_obj.throttle){
const deltaV = acceleration * select_obj.throttle * deltaTime / div;
select_obj.velocity.add(new THREE.Vector3(1, 0, 0).applyQuaternion(select_obj.quaternion).multiplyScalar(deltaV));
select_obj.totalDeltaV += deltaV;
const dvelo = accel.clone().multiplyScalar(0.5);
const vec0 = a.position.clone().add(a.velocity.clone().multiplyScalar(deltaTime / div / 2.));
const accel1 = vec0.clone().negate().normalize().multiplyScalar(deltaTime / div * a.parent.GM / vec0.lengthSq());
const deltaPosition = a.velocity.clone().add(dvelo);
const destination = a.position.clone().add(deltaPosition);

checkCollision(a, this, destination, new THREE.Vector3(0, 0, 0));
for(let j = 0; j < children.length; j++){
const child = children[j];
if(child === a)
continue;
checkCollision(a, child, destination, child.position);
}

a.velocity.add(accel1);
a.position.add(deltaPosition.multiplyScalar(deltaTime / div));
if(0 < a.angularVelocity.lengthSq()){
const axis = a.angularVelocity.clone().normalize();
// We have to multiply in this order!
a.quaternion.multiplyQuaternions(AxisAngleQuaternion(axis.x, axis.y, axis.z, a.angularVelocity.length() * deltaTime / div), a.quaternion);
}
}
const dvelo = accel.clone().multiplyScalar(0.5);
const vec0 = a.position.clone().add(a.velocity.clone().multiplyScalar(deltaTime / div / 2.));
const accel1 = vec0.clone().negate().normalize().multiplyScalar(deltaTime / div * a.parent.GM / vec0.lengthSq());
const velo1 = a.velocity.clone().add(dvelo);

a.velocity.add(accel1);
a.position.add(velo1.multiplyScalar(deltaTime / div));
if(0 < a.angularVelocity.lengthSq()){
const axis = a.angularVelocity.clone().normalize();
// We have to multiply in this order!
a.quaternion.multiplyQuaternions(AxisAngleQuaternion(axis.x, axis.y, axis.z, a.angularVelocity.length() * deltaTime / div), a.quaternion);
}
}
// Only controllable objects can change orbiting body
if(a.controllable){
// Check if we are leaving sphere of influence of current parent.
if(a.parent.parent && a.parent.soi && a.parent.soi * 1.01 < a.position.length()){
a.position.add(this.position);
a.velocity.add(this.velocity);
const j = children.indexOf(a);
if(0 <= j)
children.splice(j, 1);
a.parent = this.parent;
a.parent.children.push(a);
continue; // Continue but not increment i
}
let skip = false;
// Check if we are entering sphere of influence of another sibling.
for(let j = 0; j < children.length; j++){
const aj = children[j];
if(aj === a)
continue;
if(!aj.soi)
continue;
if(aj.position.distanceTo(a.position) < aj.soi * .99){
a.position.sub(aj.position);
a.velocity.sub(aj.velocity);
const k = children.indexOf(a);
if(0 <= k)
children.splice(k, 1);
a.parent = aj;
aj.children.push(a);
skip = true;
break;
// Only controllable objects can change orbiting body
if(a.controllable){
// Check if we are leaving sphere of influence of current parent.
if(a.parent.parent && a.parent.soi && a.parent.soi * 1.01 < a.position.length()){
a.position.add(this.position);
a.velocity.add(this.velocity);
const j = children.indexOf(a);
if(0 <= j)
children.splice(j, 1);
a.parent = this.parent;
a.parent.children.push(a);
continue; // Continue but not increment i
}
let skip = false;
// Check if we are entering sphere of influence of another sibling.
for(let j = 0; j < children.length; j++){
const aj = children[j];
if(aj === a)
continue;
if(!aj.soi)
continue;
if(aj.position.distanceTo(a.position) < aj.soi * .99){
a.position.sub(aj.position);
a.velocity.sub(aj.velocity);
const k = children.indexOf(a);
if(0 <= k)
children.splice(k, 1);
a.parent = aj;
aj.children.push(a);
skip = true;
break;
}
}
if(skip)
continue; // Continue but not increment i
}
if(skip)
continue; // Continue but not increment i
}
a.simulateBody(deltaTime, div, timescale, buttons, select_obj);
a.simulateBody(gameState, deltaTime, div, timescale, buttons, sendMessage);
i++;
}
}
Expand Down
14 changes: 11 additions & 3 deletions src/GameState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as THREE from 'three/src/Three';
import { CelestialBody, addPlanet } from './CelestialBody';
import { Settings } from './SettingsControl';
import { Settings, SimulationSettings } from './SettingsControl';
import { SendMessageCallback } from './MessageControl';
import Universe from './Universe';
import { RotationButtons } from './RotationControl';

Expand All @@ -16,6 +17,7 @@ export default class GameState{
universe: Universe;
onStateLoad?: () => void = null;
sendMessage: (text: string) => void;
readonly simulationSettings: SimulationSettings;

constructor(scene: THREE.Scene, viewScale: number, overlay: THREE.Scene, settings: Settings, camera: THREE.Camera, windowHalfX: number, windowHalfY: number, sendMessage: (text: string) => void){
this.sendMessage = sendMessage;
Expand All @@ -26,6 +28,7 @@ export default class GameState{

this.universe = new Universe(scene, AddPlanet, settings.center_select, viewScale, settings, camera, windowHalfX, windowHalfY);
this.select_obj = this.universe.rocket;
this.simulationSettings = new SimulationSettings;
window.addEventListener( 'keydown', (event: KeyboardEvent) => this.onKeyDown(event), false );
}

Expand All @@ -40,6 +43,7 @@ export default class GameState{
simTime: this.simTime,
startTime: this.startTime,
bodies: this.universe.sun.serializeTree(),
simulationSettings: this.simulationSettings,
};
}

Expand All @@ -55,6 +59,10 @@ export default class GameState{
}
if(this.select_obj && this.onStateLoad)
this.onStateLoad();
const merger = state.simulationSettings ?? new SimulationSettings();
const names = Object.keys(this.simulationSettings);
for(let key of names)
(this.simulationSettings as any)[key] = merger[key];
}

startTicking(){
Expand All @@ -79,8 +87,8 @@ export default class GameState{
return this.simTime.getTime() - this.startTime.getTime();
}

simulateBody(deltaTime: number, div: number, buttons: RotationButtons){
this.universe.simulateBody(deltaTime, div, this.timescale, buttons, this.select_obj);
simulateBody(deltaTime: number, div: number, buttons: RotationButtons, sendMessage?: SendMessageCallback){
this.universe.simulateBody(this, deltaTime, div, this.timescale, buttons, sendMessage);
}

setTimeScale(scale: number){
Expand Down
7 changes: 5 additions & 2 deletions src/MessageControl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@


export type SendMessageCallback = (text: string, timeout: number) => void;

export class MessageControl{
protected element: HTMLElement;
get domElement(): HTMLElement{ return this.element; }
Expand All @@ -15,20 +17,21 @@ export class MessageControl{
element.style.fontWeight = 'bold';
element.style.textShadow = '0px 0px 5px rgba(0,0,0,1)';
element.style.zIndex = '20';
element.style.pointerEvents = 'none';

// Register event handlers
element.ondragstart = (event) => event.preventDefault();
// Disable text selection
element.onselectstart = () => false;
}

setText(text: string){
setText(text: string, timeout: number = 5){
this.element.innerHTML = text;
this.element.style.display = 'block';
this.element.style.opacity = '1';
this.element.style.marginTop = -this.element.getBoundingClientRect().height / 2 + 'px';
this.element.style.marginLeft = -this.element.getBoundingClientRect().width / 2 + 'px';
this.showTime = 5; // Seconds to show should depend on text length!
this.showTime = timeout; // Seconds to show should depend on text length!
}

timeStep(deltaTime: number){
Expand Down
1 change: 1 addition & 0 deletions src/ScenarioSelectorControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export class ScenarioSelectorControl extends MenuControl{
select_obj.setOrbitingVelocity(scenario.semimajor_axis, rotation);
select_obj.totalDeltaV = 0.;
select_obj.ignitionCount = 0;
select_obj.stuck = false;
resetTime();
sendMessage('Scenario ' + scenario.title + ' Loaded!');
this.title.style.display = 'none';
Expand Down
Loading