Make physics more fun
This commit is contained in:
parent
155e360c8e
commit
89fafeafd3
41 changed files with 511 additions and 334 deletions
9
backend/src/helper/interpolate-angles.ts
Normal file
9
backend/src/helper/interpolate-angles.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const shortAngleDist = (a0: number, a1: number): number => {
|
||||
const max = Math.PI * 2;
|
||||
const da = (a1 - a0) % max;
|
||||
return ((2 * da) % max) - da;
|
||||
};
|
||||
|
||||
export const interpolateAngles = (a0: number, a1: number, t: number) => {
|
||||
return a0 + shortAngleDist(a0, a1) * t;
|
||||
};
|
||||
|
|
@ -20,7 +20,7 @@ glMatrix.setMatrixArrayType(Array);
|
|||
|
||||
applyArrayPlugins();
|
||||
|
||||
Random.seed = 42;
|
||||
Random.seed = 44;
|
||||
|
||||
const objects = new PhysicalContainer();
|
||||
createDungeon(objects);
|
||||
|
|
@ -94,8 +94,8 @@ const handlePhysics = () => {
|
|||
console.log(players.map((p) => p.latency));
|
||||
}
|
||||
|
||||
objects.stepObjects(delta);
|
||||
players.forEach((p) => p.step(delta));
|
||||
objects.stepObjects(delta / 1000);
|
||||
players.forEach((p) => p.step(delta / 1000));
|
||||
|
||||
const physicsDelta = deltaTimeCalculator.getDeltaTimeInMilliseconds();
|
||||
deltas.push(physicsDelta);
|
||||
|
|
|
|||
|
|
@ -1,36 +1,35 @@
|
|||
import { vec2, vec3 } from 'gl-matrix';
|
||||
import { last, Random, settings } from 'shared';
|
||||
import { Random, settings } from 'shared';
|
||||
import { LampPhysical } from '../objects/lamp-physical';
|
||||
import { StonePhysical } from '../objects/stone-physical';
|
||||
import { PlanetPhysical } from '../objects/planet-physical';
|
||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||
import { Physical } from '../physics/physical';
|
||||
import { evaluateSdf } from '../physics/functions/evaluate-sdf';
|
||||
import { Physical } from '../physics/physicals/physical';
|
||||
|
||||
export const createDungeon = (objectContainer: PhysicalContainer) => {
|
||||
const width = 100000;
|
||||
const height = 10000;
|
||||
const objects: Array<Physical> = [];
|
||||
const lights: Array<Physical> = [];
|
||||
|
||||
let previousEnd = vec2.fromValues(-width / 2, 0);
|
||||
while (previousEnd.x < width / 2) {
|
||||
const { stone, end } = createFloorElement(previousEnd);
|
||||
objects.push(stone);
|
||||
previousEnd = end;
|
||||
}
|
||||
const calculateDistanceField = (target: vec2): number =>
|
||||
objects.reduce((min, i) => (min = Math.min(min, i.distance(target))), 1000);
|
||||
const worldSize = vec2.length(
|
||||
vec2.fromValues(
|
||||
settings.worldTopEdge - settings.worldBottomEdge,
|
||||
settings.worldRightEdge - settings.worldLeftEdge,
|
||||
),
|
||||
);
|
||||
|
||||
for (let i = 0; i < worldSize / 400; i++) {
|
||||
console.log('planet', i);
|
||||
|
||||
for (let i = 0; i < 400; i++) {
|
||||
let position: vec2;
|
||||
|
||||
do {
|
||||
position = vec2.fromValues(
|
||||
Random.getRandomInRange(-width / 2, width / 2),
|
||||
Random.getRandomInRange(0, height),
|
||||
Random.getRandomInRange(settings.worldLeftEdge, settings.worldRightEdge),
|
||||
Random.getRandomInRange(settings.worldBottomEdge, settings.worldTopEdge),
|
||||
);
|
||||
} while (
|
||||
calculateDistanceField(position) < 800 ||
|
||||
calculateDistanceField(position) > 2000
|
||||
evaluateSdf(position, objects) < 800 ||
|
||||
evaluateSdf(position, objects) > 2500
|
||||
);
|
||||
|
||||
objects.push(
|
||||
|
|
@ -38,27 +37,22 @@ export const createDungeon = (objectContainer: PhysicalContainer) => {
|
|||
position,
|
||||
Random.getRandomInRange(300, 800),
|
||||
Random.getRandomInRange(300, 800),
|
||||
Random.getRandomInRange(10, 40),
|
||||
Random.getRandomInRange(20, 40),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let tryCount = 0;
|
||||
lightGeneration: for (let i = 0; i < 300; i++) {
|
||||
console.log(i);
|
||||
for (let i = 0; i < worldSize / 350; i++) {
|
||||
console.log('light', i);
|
||||
let position: vec2;
|
||||
do {
|
||||
position = vec2.fromValues(
|
||||
Random.getRandomInRange(-width / 2, width / 2),
|
||||
Random.getRandomInRange(-1000, height),
|
||||
Random.getRandomInRange(settings.worldLeftEdge, settings.worldRightEdge),
|
||||
Random.getRandomInRange(settings.worldBottomEdge, settings.worldTopEdge),
|
||||
);
|
||||
|
||||
if (tryCount++ > 1e4) {
|
||||
break lightGeneration;
|
||||
}
|
||||
} while (
|
||||
calculateDistanceField(position) < 200 ||
|
||||
lights.find((l) => l.distance(position) < 1200)
|
||||
evaluateSdf(position, objects) < 200 ||
|
||||
lights.find((l) => l.distance(position) < 1800)
|
||||
);
|
||||
lights.push(
|
||||
new LampPhysical(
|
||||
|
|
@ -84,7 +78,7 @@ const createBlob = (
|
|||
width: number,
|
||||
height: number,
|
||||
randomness: number,
|
||||
): StonePhysical => {
|
||||
): PlanetPhysical => {
|
||||
const vertices = [];
|
||||
|
||||
for (let i = 0; i < settings.polygonEdgeCount; i++) {
|
||||
|
|
@ -99,29 +93,5 @@ const createBlob = (
|
|||
),
|
||||
);
|
||||
}
|
||||
return new StonePhysical(vertices);
|
||||
};
|
||||
|
||||
const createFloorElement = (
|
||||
start: vec2,
|
||||
): {
|
||||
stone: StonePhysical;
|
||||
end: vec2;
|
||||
} => {
|
||||
const vertices: Array<vec2> = [vec2.fromValues(start.x, -10000), start];
|
||||
|
||||
let previousX = start.x;
|
||||
let previousY = start.y;
|
||||
for (let i = 0; i < settings.polygonEdgeCount - 3; i++) {
|
||||
previousX += Random.getRandomInRange(200, 800);
|
||||
previousY += Random.getRandomInRange(-100, 100);
|
||||
vertices.push(vec2.fromValues(previousX, previousY));
|
||||
}
|
||||
|
||||
const end = last(vertices)!;
|
||||
vertices.push(vec2.fromValues(end.x, -10000));
|
||||
return {
|
||||
stone: new StonePhysical(vertices),
|
||||
end,
|
||||
};
|
||||
return new PlanetPhysical(vertices);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,20 +8,70 @@ import {
|
|||
clamp,
|
||||
last,
|
||||
GameObject,
|
||||
Circle,
|
||||
} from 'shared';
|
||||
import { DynamicPhysical } from '../physics/conatiners/dynamic-physical';
|
||||
import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
||||
import { CirclePhysical } from './circle-physical';
|
||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||
import { Spring } from './spring';
|
||||
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
||||
import { ProjectilePhysical } from './projectile-physical';
|
||||
import { interpolateAngles } from '../helper/interpolate-angles';
|
||||
import { forceAtPosition } from '../physics/functions/force-at-position';
|
||||
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
||||
import { PlanetPhysical } from './planet-physical';
|
||||
import { ReactsToCollision } from '../physics/physicals/reacts-to-collision';
|
||||
|
||||
@serializesTo(CharacterBase)
|
||||
export class CharacterPhysical extends CharacterBase implements DynamicPhysical {
|
||||
export class CharacterPhysical
|
||||
extends CharacterBase
|
||||
implements DynamicPhysical, ReactsToCollision {
|
||||
public readonly canCollide = true;
|
||||
public readonly canMove = true;
|
||||
|
||||
private static readonly headRadius = 50;
|
||||
private static readonly feetRadius = 20;
|
||||
// offsets are meassured from (0, 0)
|
||||
private static readonly desiredHeadOffset = vec2.fromValues(0, 65);
|
||||
private static readonly desiredLeftFootOffset = vec2.fromValues(-20, 0);
|
||||
private static readonly desiredRightFootOffset = vec2.fromValues(20, 0);
|
||||
private static readonly centerOfMass = vec2.scale(
|
||||
vec2.create(),
|
||||
vec2.add(
|
||||
vec2.create(),
|
||||
vec2.add(
|
||||
vec2.create(),
|
||||
CharacterPhysical.desiredHeadOffset,
|
||||
CharacterPhysical.desiredLeftFootOffset,
|
||||
),
|
||||
CharacterPhysical.desiredRightFootOffset,
|
||||
),
|
||||
1 / 3,
|
||||
);
|
||||
|
||||
private static readonly headOffset = vec2.subtract(
|
||||
vec2.create(),
|
||||
CharacterPhysical.desiredHeadOffset,
|
||||
CharacterPhysical.centerOfMass,
|
||||
);
|
||||
private static readonly leftFootOffset = vec2.subtract(
|
||||
vec2.create(),
|
||||
CharacterPhysical.desiredLeftFootOffset,
|
||||
CharacterPhysical.centerOfMass,
|
||||
);
|
||||
private static readonly rightFootOffset = vec2.subtract(
|
||||
vec2.create(),
|
||||
CharacterPhysical.desiredRightFootOffset,
|
||||
CharacterPhysical.centerOfMass,
|
||||
);
|
||||
|
||||
public static readonly boundRadius =
|
||||
(CharacterPhysical.headRadius + CharacterPhysical.feetRadius * 2) * 2;
|
||||
|
||||
private isDestroyed = false;
|
||||
private jumpEnergyLeft = settings.defaultJumpEnergy;
|
||||
private direction = 0;
|
||||
private currentPlanet?: PlanetPhysical;
|
||||
private lastMovementWasRelative = false;
|
||||
private secondsSinceOnSurface = 1000;
|
||||
|
||||
public head: CirclePhysical;
|
||||
public leftFoot: CirclePhysical;
|
||||
|
|
@ -29,37 +79,31 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
public bound: CirclePhysical;
|
||||
|
||||
private movementActions: Array<MoveActionCommand> = [];
|
||||
private lastMovementAction: MoveActionCommand = new MoveActionCommand(vec2.create());
|
||||
|
||||
public handleMovementAction(c: MoveActionCommand) {
|
||||
this.movementActions.push(c);
|
||||
}
|
||||
|
||||
private static readonly headOffset = vec2.fromValues(0, 40);
|
||||
private static readonly headRadius = 50;
|
||||
private static readonly feetRadius = 20;
|
||||
private static readonly leftFootOffset = vec2.fromValues(-20, -35);
|
||||
private static readonly rightFootOffset = vec2.fromValues(20, -35);
|
||||
private lastMovementAction: MoveActionCommand = new MoveActionCommand(
|
||||
vec2.create(),
|
||||
false,
|
||||
);
|
||||
|
||||
constructor(
|
||||
public readonly colorIndex: number,
|
||||
private readonly container: PhysicalContainer,
|
||||
startPosition: vec2,
|
||||
) {
|
||||
super(id(), colorIndex);
|
||||
this.head = new CirclePhysical(
|
||||
vec2.clone(CharacterPhysical.headOffset),
|
||||
vec2.add(vec2.create(), startPosition, CharacterPhysical.headOffset),
|
||||
CharacterPhysical.headRadius,
|
||||
this,
|
||||
container,
|
||||
);
|
||||
this.leftFoot = new CirclePhysical(
|
||||
vec2.clone(CharacterPhysical.leftFootOffset),
|
||||
vec2.add(vec2.create(), startPosition, CharacterPhysical.leftFootOffset),
|
||||
CharacterPhysical.feetRadius,
|
||||
this,
|
||||
container,
|
||||
);
|
||||
this.rightFoot = new CirclePhysical(
|
||||
vec2.clone(CharacterPhysical.rightFootOffset),
|
||||
vec2.add(vec2.create(), startPosition, CharacterPhysical.rightFootOffset),
|
||||
CharacterPhysical.feetRadius,
|
||||
this,
|
||||
container,
|
||||
|
|
@ -70,12 +114,16 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
|
||||
this.bound = new CirclePhysical(
|
||||
vec2.create(),
|
||||
(CharacterPhysical.headRadius + CharacterPhysical.feetRadius * 2) * 2,
|
||||
CharacterPhysical.boundRadius,
|
||||
this,
|
||||
container,
|
||||
);
|
||||
}
|
||||
|
||||
public handleMovementAction(c: MoveActionCommand) {
|
||||
this.movementActions.push(c);
|
||||
}
|
||||
|
||||
public onCollision(other: GameObject) {
|
||||
if (other instanceof ProjectilePhysical) {
|
||||
other.destroy();
|
||||
|
|
@ -110,7 +158,7 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
);
|
||||
}
|
||||
|
||||
private sumAndResetMovementActions(): vec2 {
|
||||
private averageAndResetMovementActions(): vec2 {
|
||||
let direction: vec2;
|
||||
if (this.movementActions.length === 0) {
|
||||
direction = vec2.clone(this.lastMovementAction.direction);
|
||||
|
|
@ -122,6 +170,9 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
|
||||
vec2.scale(direction, direction, 1 / this.movementActions.length);
|
||||
|
||||
this.lastMovementWasRelative =
|
||||
this.movementActions.find((a) => a.isCharacterRelative) !== undefined;
|
||||
|
||||
this.lastMovementAction = last(this.movementActions)!;
|
||||
this.movementActions = [];
|
||||
}
|
||||
|
|
@ -129,82 +180,104 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
return direction;
|
||||
}
|
||||
|
||||
public step(deltaTimeInMiliseconds: number) {
|
||||
const deltaTime = deltaTimeInMiliseconds / 1000;
|
||||
const direction = this.sumAndResetMovementActions();
|
||||
const feetAirborne = this.leftFoot.isAirborne && this.rightFoot.isAirborne;
|
||||
const isAirborne = feetAirborne && this.head.isAirborne;
|
||||
this.jumpEnergyLeft += isAirborne ? -deltaTime : deltaTime;
|
||||
this.jumpEnergyLeft = clamp(this.jumpEnergyLeft, 0, settings.defaultJumpEnergy);
|
||||
public step(deltaTime: number) {
|
||||
if ((this.secondsSinceOnSurface += deltaTime) > 1) {
|
||||
this.currentPlanet = undefined;
|
||||
}
|
||||
|
||||
const xMax = deltaTime * settings.maxAccelerationX;
|
||||
const yMax = this.jumpEnergyLeft > 0 ? settings.maxAccelerationY : 0;
|
||||
const movementForce = vec2.multiply(
|
||||
direction,
|
||||
direction,
|
||||
vec2.fromValues(xMax, yMax),
|
||||
const intersectingWithForcefield = this.container.findIntersecting(
|
||||
getBoundingBoxOfCircle(
|
||||
new Circle(
|
||||
this.center,
|
||||
CharacterPhysical.boundRadius + settings.maxGravityDistance,
|
||||
),
|
||||
),
|
||||
);
|
||||
const actualGravity = forceAtPosition(this.center, intersectingWithForcefield);
|
||||
|
||||
Spring.step(
|
||||
this.leftFoot,
|
||||
this.rightFoot,
|
||||
vec2.distance(CharacterPhysical.leftFootOffset, CharacterPhysical.rightFootOffset),
|
||||
300,
|
||||
deltaTime,
|
||||
);
|
||||
const direction = this.averageAndResetMovementActions();
|
||||
const movementForce = vec2.scale(direction, direction, settings.maxAcceleration);
|
||||
|
||||
if (!this.currentPlanet) {
|
||||
this.applyForce(this.leftFoot, actualGravity, deltaTime);
|
||||
this.applyForce(this.rightFoot, actualGravity, deltaTime);
|
||||
|
||||
const sumForce = vec2.subtract(vec2.create(), actualGravity, movementForce);
|
||||
|
||||
this.setDirection(
|
||||
vec2.length(sumForce) === 0 ? vec2.fromValues(0, -1) : sumForce,
|
||||
deltaTime,
|
||||
);
|
||||
} else {
|
||||
const leftFootGravity = this.currentPlanet!.getForce(this.leftFoot.center);
|
||||
const rightFootGravity = this.currentPlanet!.getForce(this.rightFoot.center);
|
||||
if (movementForce.y > settings.maxAcceleration / 4) {
|
||||
vec2.scale(leftFootGravity, leftFootGravity, 0.35);
|
||||
vec2.scale(rightFootGravity, rightFootGravity, 0.35);
|
||||
}
|
||||
this.applyForce(this.leftFoot, leftFootGravity, deltaTime);
|
||||
this.applyForce(this.rightFoot, rightFootGravity, deltaTime);
|
||||
|
||||
if (this.lastMovementWasRelative) {
|
||||
vec2.rotate(movementForce, movementForce, vec2.create(), this.direction);
|
||||
}
|
||||
|
||||
const headGravity = this.currentPlanet!.getForce(this.head.center);
|
||||
|
||||
if (vec2.length(headGravity) < vec2.length(actualGravity) / 2) {
|
||||
this.currentPlanet = undefined;
|
||||
}
|
||||
this.setDirection(headGravity, deltaTime);
|
||||
}
|
||||
|
||||
this.applyForce(this.head, movementForce, deltaTime);
|
||||
this.applyForce(this.leftFoot, movementForce, deltaTime);
|
||||
this.applyForce(this.rightFoot, movementForce, deltaTime);
|
||||
|
||||
if (feetAirborne) {
|
||||
this.applyForce(this.head, settings.gravitationalForce, deltaTime);
|
||||
this.stepBodyPart(this.leftFoot, deltaTime);
|
||||
this.stepBodyPart(this.rightFoot, deltaTime);
|
||||
this.keepPosture();
|
||||
}
|
||||
|
||||
private setDirection(direction: vec2, deltaTime: number) {
|
||||
this.direction = interpolateAngles(
|
||||
this.direction,
|
||||
Math.atan2(direction.y, direction.x) + Math.PI / 2,
|
||||
Math.pow(4, deltaTime),
|
||||
);
|
||||
}
|
||||
|
||||
private keepPosture() {
|
||||
const bodyCenter = vec2.add(vec2.create(), this.head.center, this.leftFoot.center);
|
||||
vec2.add(bodyCenter, bodyCenter, this.rightFoot.center);
|
||||
vec2.scale(bodyCenter, bodyCenter, 1 / 3);
|
||||
this.springMove(this.leftFoot, bodyCenter, CharacterPhysical.leftFootOffset);
|
||||
this.springMove(this.rightFoot, bodyCenter, CharacterPhysical.rightFootOffset);
|
||||
this.springMove(this.head, bodyCenter, CharacterPhysical.headOffset);
|
||||
}
|
||||
|
||||
private springMove(object: CirclePhysical, center: vec2, offset: vec2) {
|
||||
// todo: make time-independent
|
||||
const springConstant = 0.35;
|
||||
|
||||
const desiredPosition = vec2.add(vec2.create(), center, offset);
|
||||
vec2.rotate(desiredPosition, desiredPosition, center, this.direction);
|
||||
const positionDelta = vec2.subtract(desiredPosition, desiredPosition, object.center);
|
||||
vec2.scale(positionDelta, positionDelta, springConstant);
|
||||
const hitObject = object.tryMove(positionDelta);
|
||||
|
||||
if (hitObject instanceof PlanetPhysical) {
|
||||
this.secondsSinceOnSurface = 0;
|
||||
this.currentPlanet = hitObject;
|
||||
}
|
||||
this.applyForce(this.leftFoot, settings.gravitationalForce, deltaTime);
|
||||
this.applyForce(this.rightFoot, settings.gravitationalForce, deltaTime);
|
||||
}
|
||||
|
||||
this.head.step2(deltaTime);
|
||||
this.leftFoot.step2(deltaTime);
|
||||
this.rightFoot.step2(deltaTime);
|
||||
private stepBodyPart(part: CirclePhysical, deltaTime: number) {
|
||||
const hitObject = part.step2(deltaTime);
|
||||
|
||||
let sumBody = vec2.add(vec2.create(), this.head.center, this.leftFoot.center);
|
||||
vec2.add(sumBody, sumBody, this.rightFoot.center);
|
||||
vec2.scale(sumBody, sumBody, 1 / 3);
|
||||
|
||||
const headPosition = vec2.add(vec2.create(), sumBody, CharacterPhysical.headOffset);
|
||||
const headDelta = vec2.subtract(headPosition, headPosition, this.head.center);
|
||||
vec2.scale(headDelta, headDelta, 0.5);
|
||||
this.head.tryMove(headDelta);
|
||||
|
||||
sumBody = vec2.add(vec2.create(), this.head.center, this.leftFoot.center);
|
||||
vec2.add(sumBody, sumBody, this.rightFoot.center);
|
||||
vec2.scale(sumBody, sumBody, 1 / 3);
|
||||
|
||||
const leftFootPosition = vec2.add(
|
||||
vec2.create(),
|
||||
sumBody,
|
||||
CharacterPhysical.leftFootOffset,
|
||||
);
|
||||
const leftFootDelta = vec2.subtract(
|
||||
leftFootPosition,
|
||||
leftFootPosition,
|
||||
this.leftFoot.center,
|
||||
);
|
||||
vec2.scale(leftFootDelta, leftFootDelta, 1);
|
||||
this.leftFoot.tryMove(leftFootDelta);
|
||||
|
||||
const rightFootPosition = vec2.add(
|
||||
vec2.create(),
|
||||
sumBody,
|
||||
CharacterPhysical.rightFootOffset,
|
||||
);
|
||||
const rightFootDelta = vec2.subtract(
|
||||
rightFootPosition,
|
||||
rightFootPosition,
|
||||
this.rightFoot.center,
|
||||
);
|
||||
vec2.scale(rightFootDelta, rightFootDelta, 1);
|
||||
this.rightFoot.tryMove(rightFootDelta);
|
||||
if (hitObject instanceof PlanetPhysical) {
|
||||
this.secondsSinceOnSurface = 0;
|
||||
this.currentPlanet = hitObject;
|
||||
}
|
||||
}
|
||||
|
||||
public applyForce(circle: CirclePhysical, force: vec2, timeInSeconds: number) {
|
||||
|
|
|
|||
|
|
@ -1,31 +1,28 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Circle, GameObject, serializesTo, settings } from 'shared';
|
||||
import { Physical } from '../physics/physical';
|
||||
|
||||
import { PhysicalBase } from '../physics/physicals/physical-base';
|
||||
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
||||
import { moveCircle } from '../physics/move-circle';
|
||||
import { moveCircle } from '../physics/functions/move-circle';
|
||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||
import { DynamicPhysical } from '../physics/containers/dynamic-physical';
|
||||
import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
||||
import {
|
||||
ReactsToCollision,
|
||||
reactsToCollision,
|
||||
} from '../physics/physicals/reacts-to-collision';
|
||||
|
||||
@serializesTo(Circle)
|
||||
export class CirclePhysical implements Circle, DynamicPhysical {
|
||||
export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollision {
|
||||
readonly canCollide = true;
|
||||
readonly canMove = true;
|
||||
|
||||
private _isAirborne = true;
|
||||
public velocity = vec2.create();
|
||||
|
||||
public get isAirborne(): boolean {
|
||||
return this._isAirborne;
|
||||
}
|
||||
|
||||
private _boundingBox: BoundingBox;
|
||||
|
||||
constructor(
|
||||
private _center: vec2,
|
||||
private _radius: number,
|
||||
public owner: DynamicPhysical,
|
||||
public owner: GameObject,
|
||||
private readonly container: PhysicalContainer,
|
||||
private restitution = 0,
|
||||
) {
|
||||
|
|
@ -47,7 +44,9 @@ export class CirclePhysical implements Circle, DynamicPhysical {
|
|||
}
|
||||
|
||||
public onCollision(other: GameObject) {
|
||||
this.owner.onCollision(other);
|
||||
if (reactsToCollision(this.owner)) {
|
||||
this.owner.onCollision(other);
|
||||
}
|
||||
}
|
||||
|
||||
public get gameObject(): GameObject {
|
||||
|
|
@ -71,11 +70,11 @@ export class CirclePhysical implements Circle, DynamicPhysical {
|
|||
return vec2.distance(target.center, this.center) - this.radius - target.radius;
|
||||
}
|
||||
|
||||
public areIntersecting(other: Physical): boolean {
|
||||
public areIntersecting(other: PhysicalBase): boolean {
|
||||
return other.distance(this.center) < this.radius;
|
||||
}
|
||||
|
||||
public isInside(other: Physical): boolean {
|
||||
public isInside(other: PhysicalBase): boolean {
|
||||
return other.distance(this.center) < -this.radius;
|
||||
}
|
||||
|
||||
|
|
@ -94,9 +93,9 @@ export class CirclePhysical implements Circle, DynamicPhysical {
|
|||
);
|
||||
}
|
||||
|
||||
public step(deltaTime: number) {}
|
||||
public step(_: number) {}
|
||||
|
||||
public step2(deltaTimeInSeconds: number): boolean {
|
||||
public step2(deltaTimeInSeconds: number): GameObject | undefined {
|
||||
vec2.scale(
|
||||
this.velocity,
|
||||
this.velocity,
|
||||
|
|
@ -108,7 +107,7 @@ export class CirclePhysical implements Circle, DynamicPhysical {
|
|||
const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep);
|
||||
vec2.scale(delta, delta, 1 / stepCount);
|
||||
|
||||
let wasHit = false;
|
||||
let lastHit: GameObject | undefined;
|
||||
|
||||
for (let i = 0; i < stepCount; i++) {
|
||||
const distance = vec2.scale(
|
||||
|
|
@ -120,7 +119,11 @@ export class CirclePhysical implements Circle, DynamicPhysical {
|
|||
const intersecting = this.container.findIntersecting(this.boundingBox);
|
||||
this.radius -= vec2.length(distance);
|
||||
|
||||
const { normal, hitSurface } = moveCircle(this, vec2.clone(distance), intersecting);
|
||||
const { normal, hitSurface, hitObject } = moveCircle(
|
||||
this,
|
||||
vec2.clone(distance),
|
||||
intersecting,
|
||||
);
|
||||
|
||||
if (hitSurface) {
|
||||
vec2.subtract(
|
||||
|
|
@ -133,40 +136,37 @@ export class CirclePhysical implements Circle, DynamicPhysical {
|
|||
),
|
||||
);
|
||||
|
||||
wasHit = true;
|
||||
lastHit = hitObject;
|
||||
}
|
||||
}
|
||||
|
||||
this._isAirborne = !wasHit;
|
||||
return wasHit;
|
||||
return lastHit;
|
||||
}
|
||||
|
||||
public tryMove(delta: vec2) {
|
||||
public tryMove(delta: vec2): GameObject | undefined {
|
||||
const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep);
|
||||
vec2.scale(delta, delta, 1 / stepCount);
|
||||
|
||||
let wasHit = false;
|
||||
let lastHit: GameObject | undefined;
|
||||
|
||||
for (let i = 0; i < stepCount; i++) {
|
||||
this.radius += vec2.length(delta);
|
||||
const intersecting = this.container.findIntersecting(this.boundingBox);
|
||||
this.radius -= vec2.length(delta);
|
||||
|
||||
const { normal, hitSurface } = moveCircle(this, vec2.clone(delta), intersecting);
|
||||
const { tangent, hitSurface, hitObject } = moveCircle(
|
||||
this,
|
||||
vec2.clone(delta),
|
||||
intersecting,
|
||||
);
|
||||
|
||||
if (hitSurface) {
|
||||
vec2.subtract(
|
||||
delta,
|
||||
delta,
|
||||
vec2.scale(normal!, normal!, vec2.dot(normal!, delta)),
|
||||
);
|
||||
|
||||
wasHit = true;
|
||||
delta = vec2.scale(delta, tangent!, vec2.length(delta));
|
||||
lastHit = hitObject;
|
||||
}
|
||||
}
|
||||
|
||||
this._isAirborne = !wasHit;
|
||||
return wasHit;
|
||||
return lastHit;
|
||||
}
|
||||
|
||||
public toArray(): Array<any> {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { vec2, vec3 } from 'gl-matrix';
|
|||
import { LampBase, settings, id, serializesTo } from 'shared';
|
||||
|
||||
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
||||
import { StaticPhysical } from '../physics/containers/static-physical';
|
||||
import { StaticPhysical } from '../physics/physicals/static-physical';
|
||||
|
||||
@serializesTo(LampBase)
|
||||
export class LampPhysical extends LampBase implements StaticPhysical {
|
||||
|
|
@ -32,6 +32,10 @@ export class LampPhysical extends LampBase implements StaticPhysical {
|
|||
return this;
|
||||
}
|
||||
|
||||
public getForce(_: vec2): vec2 {
|
||||
return vec2.create();
|
||||
}
|
||||
|
||||
public distance(target: vec2): number {
|
||||
return vec2.distance(this.center, target);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,31 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { clamp01, id, serializesTo, StoneBase } from 'shared';
|
||||
|
||||
import {
|
||||
clamp,
|
||||
clamp01,
|
||||
id,
|
||||
rotate90Deg,
|
||||
rotateMinus90Deg,
|
||||
serializesTo,
|
||||
settings,
|
||||
PlanetBase,
|
||||
} from 'shared';
|
||||
|
||||
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
||||
import { StaticPhysical } from '../physics/containers/static-physical';
|
||||
import { StaticPhysical } from '../physics/physicals/static-physical';
|
||||
|
||||
@serializesTo(StoneBase)
|
||||
export class StonePhysical extends StoneBase implements StaticPhysical {
|
||||
@serializesTo(PlanetBase)
|
||||
export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
||||
public readonly canCollide = true;
|
||||
public readonly canMove = false;
|
||||
private center: vec2;
|
||||
|
||||
private _boundingBox?: ImmutableBoundingBox;
|
||||
|
||||
constructor(vertices: Array<vec2>) {
|
||||
super(id(), vertices);
|
||||
this.center = vertices.reduce((sum, v) => vec2.add(sum, sum, v), vec2.create());
|
||||
vec2.scale(this.center, this.center, 1 / vertices.length);
|
||||
}
|
||||
|
||||
public distance(target: vec2): number {
|
||||
|
|
@ -72,6 +85,46 @@ export class StonePhysical extends StoneBase implements StaticPhysical {
|
|||
return this._boundingBox;
|
||||
}
|
||||
|
||||
public getForce(position: vec2): vec2 {
|
||||
const height = this.distance(position);
|
||||
|
||||
let closestIndex = 0;
|
||||
this.vertices.forEach((v, i) => {
|
||||
if (
|
||||
vec2.distance(position, v) < vec2.distance(position, this.vertices[closestIndex])
|
||||
) {
|
||||
closestIndex = i;
|
||||
}
|
||||
});
|
||||
|
||||
const afterClosest = this.vertices[(closestIndex + 1) % this.vertices.length];
|
||||
const closest = this.vertices[closestIndex];
|
||||
const beforeClosest = this.vertices[
|
||||
(closestIndex - 1 + this.vertices.length) % this.vertices.length
|
||||
];
|
||||
|
||||
const diff = vec2.subtract(vec2.create(), position, closest);
|
||||
const edge1 = vec2.subtract(vec2.create(), afterClosest, closest);
|
||||
const edge2 = vec2.subtract(vec2.create(), closest, beforeClosest);
|
||||
const normalizedDiff = vec2.normalize(vec2.create(), diff);
|
||||
|
||||
const currentEdge =
|
||||
vec2.dot(vec2.normalize(vec2.create(), rotate90Deg(edge1)), normalizedDiff) >
|
||||
vec2.dot(vec2.normalize(vec2.create(), rotate90Deg(edge2)), normalizedDiff)
|
||||
? edge1
|
||||
: edge2;
|
||||
|
||||
vec2.normalize(currentEdge, currentEdge);
|
||||
const normal = rotateMinus90Deg(currentEdge);
|
||||
|
||||
const scale = clamp(
|
||||
settings.maxGravityQ * ((settings.maxGravityDistance / height) ** 3 - 1),
|
||||
0,
|
||||
settings.maxGravityStrength,
|
||||
);
|
||||
return vec2.scale(normal, normal, scale);
|
||||
}
|
||||
|
||||
public get gameObject(): this {
|
||||
return this;
|
||||
}
|
||||
|
|
@ -2,11 +2,15 @@ import { vec2 } from 'gl-matrix';
|
|||
import { id, settings, serializesTo, ProjectileBase, GameObject } from 'shared';
|
||||
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
||||
import { CirclePhysical } from './circle-physical';
|
||||
import { DynamicPhysical } from '../physics/conatiners/dynamic-physical';
|
||||
import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||
import { PlanetPhysical } from './planet-physical';
|
||||
import { ReactsToCollision } from '../physics/physicals/reacts-to-collision';
|
||||
|
||||
@serializesTo(ProjectileBase)
|
||||
export class ProjectilePhysical extends ProjectileBase implements DynamicPhysical {
|
||||
export class ProjectilePhysical
|
||||
extends ProjectileBase
|
||||
implements DynamicPhysical, ReactsToCollision {
|
||||
public readonly canCollide = true;
|
||||
public readonly canMove = true;
|
||||
private isDestroyed = false;
|
||||
|
|
@ -55,14 +59,16 @@ export class ProjectilePhysical extends ProjectileBase implements DynamicPhysica
|
|||
}
|
||||
}
|
||||
|
||||
public step(deltaTimeInMiliseconds: number) {
|
||||
const deltaTime = deltaTimeInMiliseconds / 1000;
|
||||
public step(deltaTime: number) {
|
||||
const gravity = vec2.create();
|
||||
const intersecting = this.container.findIntersecting(this.boundingBox);
|
||||
intersecting.forEach((i) => {
|
||||
if (i instanceof PlanetPhysical) {
|
||||
vec2.add(gravity, gravity, i.getForce(this.center));
|
||||
}
|
||||
});
|
||||
|
||||
vec2.add(
|
||||
this.velocity,
|
||||
this.velocity,
|
||||
vec2.scale(vec2.create(), settings.gravitationalForce, deltaTime),
|
||||
);
|
||||
vec2.add(this.velocity, this.velocity, vec2.scale(vec2.create(), gravity, deltaTime));
|
||||
|
||||
this.object.velocity = this.velocity;
|
||||
this.object.step2(deltaTime);
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Circle } from 'shared';
|
||||
import { CirclePhysical } from './circle-physical';
|
||||
|
||||
export class Spring {
|
||||
constructor(
|
||||
private a: CirclePhysical | Circle,
|
||||
private b: CirclePhysical | Circle,
|
||||
private distance: number,
|
||||
private strength: number,
|
||||
) {}
|
||||
|
||||
public step(deltaTimeInSeconds: number) {
|
||||
Spring.step(this.a, this.b, this.distance, this.strength, deltaTimeInSeconds);
|
||||
}
|
||||
|
||||
public static step(
|
||||
a: CirclePhysical | Circle,
|
||||
b: CirclePhysical | Circle,
|
||||
distance: number,
|
||||
strength: number,
|
||||
deltaTimeInSeconds: number,
|
||||
) {
|
||||
const length = vec2.dist(a.center, b.center) - distance;
|
||||
|
||||
const abDirection = vec2.subtract(vec2.create(), b.center, a.center);
|
||||
vec2.normalize(abDirection, abDirection);
|
||||
const force = vec2.scale(abDirection, abDirection, strength * length);
|
||||
if (a instanceof CirclePhysical) {
|
||||
a.applyForce(force, deltaTimeInSeconds);
|
||||
}
|
||||
if (b instanceof CirclePhysical) {
|
||||
vec2.scale(force, force, -1);
|
||||
b.applyForce(force, deltaTimeInSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { GameObject } from 'shared';
|
||||
import { Physical } from '../physical';
|
||||
|
||||
export interface DynamicPhysical extends Physical {
|
||||
readonly canMove: true;
|
||||
step(deltaTimeInMilliseconds: number): void;
|
||||
onCollision(other: GameObject): void;
|
||||
}
|
||||
|
|
@ -3,9 +3,9 @@ import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base';
|
|||
import { BoundingBoxList } from './bounding-box-list';
|
||||
import { BoundingBoxTree } from './bounding-box-tree';
|
||||
|
||||
import { Physical } from '../physical';
|
||||
import { StaticPhysical } from './static-physical';
|
||||
import { DynamicPhysical } from './dynamic-physical';
|
||||
import { Physical } from '../physicals/physical';
|
||||
import { StaticPhysical } from '../physicals/static-physical';
|
||||
import { DynamicPhysical } from '../physicals/dynamic-physical';
|
||||
|
||||
export class PhysicalContainer {
|
||||
private isTreeInitialized = false;
|
||||
|
|
@ -34,8 +34,8 @@ export class PhysicalContainer {
|
|||
this.dynamicBoundingBoxes.remove(object);
|
||||
}
|
||||
|
||||
public stepObjects(deltaTimeInMilliseconds: number) {
|
||||
this.dynamicBoundingBoxes.forEach((o) => o.step(deltaTimeInMilliseconds));
|
||||
public stepObjects(deltaTime: number) {
|
||||
this.dynamicBoundingBoxes.forEach((o) => o.step(deltaTime));
|
||||
}
|
||||
|
||||
public findIntersecting(box: BoundingBoxBase): Array<Physical> {
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
import { Physical } from '../physical';
|
||||
import { ImmutableBoundingBox } from '../bounding-boxes/immutable-bounding-box';
|
||||
|
||||
export interface StaticPhysical extends Physical {
|
||||
readonly canMove: false;
|
||||
readonly boundingBox: ImmutableBoundingBox;
|
||||
}
|
||||
24
backend/src/physics/functions/apply-spring-force.ts
Normal file
24
backend/src/physics/functions/apply-spring-force.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Circle } from 'shared';
|
||||
import { CirclePhysical } from '../../objects/circle-physical';
|
||||
|
||||
export const applySpringForce = (
|
||||
a: CirclePhysical | Circle,
|
||||
b: CirclePhysical | Circle,
|
||||
distance: number,
|
||||
strength: number,
|
||||
deltaTimeInSeconds: number,
|
||||
) => {
|
||||
const length = vec2.dist(a.center, b.center) - distance;
|
||||
|
||||
const abDirection = vec2.subtract(vec2.create(), b.center, a.center);
|
||||
vec2.normalize(abDirection, abDirection);
|
||||
const force = vec2.scale(abDirection, abDirection, strength * length);
|
||||
if (a instanceof CirclePhysical) {
|
||||
a.applyForce(force, deltaTimeInSeconds);
|
||||
}
|
||||
if (b instanceof CirclePhysical) {
|
||||
vec2.scale(force, force, -1);
|
||||
b.applyForce(force, deltaTimeInSeconds);
|
||||
}
|
||||
};
|
||||
7
backend/src/physics/functions/evaluate-sdf.ts
Normal file
7
backend/src/physics/functions/evaluate-sdf.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { PhysicalBase } from '../physicals/physical-base';
|
||||
|
||||
export const evaluateSdf = (target: vec2, objects: Array<PhysicalBase>) =>
|
||||
objects
|
||||
.filter((i) => i.canCollide)
|
||||
.reduce((min, i) => (min = Math.min(min, i.distance(target))), 1000);
|
||||
9
backend/src/physics/functions/force-at-position.ts
Normal file
9
backend/src/physics/functions/force-at-position.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Physical } from '../physicals/physical';
|
||||
|
||||
export const forceAtPosition = (position: vec2, objects: Array<Physical>) =>
|
||||
objects.reduce(
|
||||
(sum: vec2, o: Physical) =>
|
||||
!o.canMove ? vec2.add(sum, sum, o.getForce(position)) : sum,
|
||||
vec2.create(),
|
||||
);
|
||||
10
backend/src/physics/functions/get-bounding-box-of-circle.ts
Normal file
10
backend/src/physics/functions/get-bounding-box-of-circle.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Circle } from 'shared';
|
||||
import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base';
|
||||
|
||||
export const getBoundingBoxOfCircle = (circle: Circle): BoundingBoxBase =>
|
||||
new BoundingBoxBase(
|
||||
circle.center.x - circle.radius,
|
||||
circle.center.x + circle.radius,
|
||||
circle.center.y - circle.radius,
|
||||
circle.center.y + circle.radius,
|
||||
);
|
||||
8
backend/src/physics/functions/is-circle-intersecting.ts
Normal file
8
backend/src/physics/functions/is-circle-intersecting.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Circle } from 'shared';
|
||||
import { evaluateSdf } from './evaluate-sdf';
|
||||
import { PhysicalBase } from '../physicals/physical-base';
|
||||
|
||||
export const isCircleIntersecting = (
|
||||
circle: Circle,
|
||||
intersectors: Array<PhysicalBase>,
|
||||
): boolean => evaluateSdf(circle.center, intersectors) < circle.radius;
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Circle, rotate90Deg } from 'shared';
|
||||
import { CirclePhysical } from '../objects/circle-physical';
|
||||
import { DynamicPhysical } from './containers/dynamic-physical';
|
||||
import { Physical } from './physical';
|
||||
import { Circle, GameObject, rotate90Deg } from 'shared';
|
||||
import { CirclePhysical } from '../../objects/circle-physical';
|
||||
import { reactsToCollision } from '../physicals/reacts-to-collision';
|
||||
import { evaluateSdf } from './evaluate-sdf';
|
||||
import { Physical } from '../physicals/physical';
|
||||
|
||||
export const moveCircle = (
|
||||
circle: CirclePhysical,
|
||||
|
|
@ -13,6 +14,7 @@ export const moveCircle = (
|
|||
hitSurface: boolean;
|
||||
normal?: vec2;
|
||||
tangent?: vec2;
|
||||
hitObject?: GameObject;
|
||||
} => {
|
||||
const nextCircle = new Circle(vec2.clone(circle.center), circle.radius);
|
||||
vec2.add(nextCircle.center, nextCircle.center, delta);
|
||||
|
|
@ -21,13 +23,7 @@ export const moveCircle = (
|
|||
(b) => b.gameObject !== circle.gameObject && b.canCollide,
|
||||
);
|
||||
|
||||
const getSdfAtPoint = (point: vec2): number => {
|
||||
return possibleIntersectors
|
||||
.filter((i) => i.canCollide)
|
||||
.reduce((min, i) => (min = Math.min(min, i.distance(point))), 1000);
|
||||
};
|
||||
|
||||
const sdfAtCenter = getSdfAtPoint(nextCircle.center);
|
||||
const sdfAtCenter = evaluateSdf(nextCircle.center, possibleIntersectors);
|
||||
|
||||
if (sdfAtCenter > nextCircle.radius) {
|
||||
circle.center = vec2.add(circle.center, circle.center, delta);
|
||||
|
|
@ -38,21 +34,26 @@ export const moveCircle = (
|
|||
};
|
||||
}
|
||||
|
||||
const intersecting = possibleIntersectors
|
||||
.filter((i) => i.canCollide && i.canMove)
|
||||
.find((i) => i.distance(nextCircle.center) < circle.radius);
|
||||
const intersecting = possibleIntersectors.find(
|
||||
(i) => i.distance(nextCircle.center) <= circle.radius,
|
||||
)!;
|
||||
|
||||
(intersecting?.gameObject as DynamicPhysical)?.onCollision(circle.gameObject);
|
||||
((circle.gameObject as unknown) as DynamicPhysical).onCollision(
|
||||
intersecting?.gameObject,
|
||||
);
|
||||
if (reactsToCollision(intersecting)) {
|
||||
intersecting.onCollision(circle.gameObject);
|
||||
}
|
||||
|
||||
if (reactsToCollision(circle)) {
|
||||
circle.onCollision(intersecting.gameObject);
|
||||
}
|
||||
|
||||
const dx =
|
||||
getSdfAtPoint(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(1, 0))) -
|
||||
sdfAtCenter;
|
||||
evaluateSdf(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0.01, 0)), [
|
||||
intersecting,
|
||||
]) - sdfAtCenter;
|
||||
const dy =
|
||||
getSdfAtPoint(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0, 1))) -
|
||||
sdfAtCenter;
|
||||
evaluateSdf(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0, 0.01)), [
|
||||
intersecting,
|
||||
]) - sdfAtCenter;
|
||||
const normal = vec2.fromValues(dx, dy);
|
||||
vec2.normalize(normal, normal);
|
||||
const rotatedNormal = rotate90Deg(normal);
|
||||
|
|
@ -60,6 +61,7 @@ export const moveCircle = (
|
|||
realDelta: delta,
|
||||
hitSurface: true,
|
||||
normal,
|
||||
hitObject: intersecting?.gameObject,
|
||||
tangent:
|
||||
vec2.dot(rotatedNormal, delta) < 0
|
||||
? vec2.scale(rotatedNormal, rotatedNormal, -1)
|
||||
6
backend/src/physics/physicals/dynamic-physical.ts
Normal file
6
backend/src/physics/physicals/dynamic-physical.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { PhysicalBase } from './physical-base';
|
||||
|
||||
export interface DynamicPhysical extends PhysicalBase {
|
||||
readonly canMove: true;
|
||||
step(deltaTimeInMilliseconds: number): void;
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { GameObject } from 'shared';
|
||||
import { BoundingBoxBase } from './bounding-boxes/bounding-box-base';
|
||||
import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base';
|
||||
|
||||
export interface Physical {
|
||||
export interface PhysicalBase {
|
||||
readonly canCollide: boolean;
|
||||
readonly canMove: boolean;
|
||||
readonly boundingBox: BoundingBoxBase;
|
||||
4
backend/src/physics/physicals/physical.ts
Normal file
4
backend/src/physics/physicals/physical.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { DynamicPhysical } from './dynamic-physical';
|
||||
import { StaticPhysical } from './static-physical';
|
||||
|
||||
export type Physical = StaticPhysical | DynamicPhysical;
|
||||
8
backend/src/physics/physicals/reacts-to-collision.ts
Normal file
8
backend/src/physics/physicals/reacts-to-collision.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { GameObject } from 'shared';
|
||||
|
||||
export interface ReactsToCollision {
|
||||
onCollision(other: GameObject): void;
|
||||
}
|
||||
|
||||
export const reactsToCollision = (a: any): a is ReactsToCollision =>
|
||||
a && 'onCollision' in a;
|
||||
10
backend/src/physics/physicals/static-physical.ts
Normal file
10
backend/src/physics/physicals/static-physical.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { PhysicalBase } from './physical-base';
|
||||
import { ImmutableBoundingBox } from '../bounding-boxes/immutable-bounding-box';
|
||||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export interface StaticPhysical extends PhysicalBase {
|
||||
readonly canMove: false;
|
||||
readonly boundingBox: ImmutableBoundingBox;
|
||||
|
||||
getForce(target: vec2): vec2;
|
||||
}
|
||||
17
backend/tsconfig.json
Normal file
17
backend/tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"target": "es6",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true,
|
||||
"moduleResolution": "node",
|
||||
"module": "commonjs",
|
||||
"composite": true,
|
||||
"lib": ["dom", "es2017"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/*.ts"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue