Make physics more fun
This commit is contained in:
parent
155e360c8e
commit
89fafeafd3
41 changed files with 511 additions and 334 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@ lib
|
||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.firebase
|
.firebase
|
||||||
|
lerna-debug.log
|
||||||
|
|
|
||||||
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();
|
applyArrayPlugins();
|
||||||
|
|
||||||
Random.seed = 42;
|
Random.seed = 44;
|
||||||
|
|
||||||
const objects = new PhysicalContainer();
|
const objects = new PhysicalContainer();
|
||||||
createDungeon(objects);
|
createDungeon(objects);
|
||||||
|
|
@ -94,8 +94,8 @@ const handlePhysics = () => {
|
||||||
console.log(players.map((p) => p.latency));
|
console.log(players.map((p) => p.latency));
|
||||||
}
|
}
|
||||||
|
|
||||||
objects.stepObjects(delta);
|
objects.stepObjects(delta / 1000);
|
||||||
players.forEach((p) => p.step(delta));
|
players.forEach((p) => p.step(delta / 1000));
|
||||||
|
|
||||||
const physicsDelta = deltaTimeCalculator.getDeltaTimeInMilliseconds();
|
const physicsDelta = deltaTimeCalculator.getDeltaTimeInMilliseconds();
|
||||||
deltas.push(physicsDelta);
|
deltas.push(physicsDelta);
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,35 @@
|
||||||
import { vec2, vec3 } from 'gl-matrix';
|
import { vec2, vec3 } from 'gl-matrix';
|
||||||
import { last, Random, settings } from 'shared';
|
import { Random, settings } from 'shared';
|
||||||
import { LampPhysical } from '../objects/lamp-physical';
|
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 { 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) => {
|
export const createDungeon = (objectContainer: PhysicalContainer) => {
|
||||||
const width = 100000;
|
|
||||||
const height = 10000;
|
|
||||||
const objects: Array<Physical> = [];
|
const objects: Array<Physical> = [];
|
||||||
const lights: Array<Physical> = [];
|
const lights: Array<Physical> = [];
|
||||||
|
|
||||||
let previousEnd = vec2.fromValues(-width / 2, 0);
|
const worldSize = vec2.length(
|
||||||
while (previousEnd.x < width / 2) {
|
vec2.fromValues(
|
||||||
const { stone, end } = createFloorElement(previousEnd);
|
settings.worldTopEdge - settings.worldBottomEdge,
|
||||||
objects.push(stone);
|
settings.worldRightEdge - settings.worldLeftEdge,
|
||||||
previousEnd = end;
|
),
|
||||||
}
|
);
|
||||||
const calculateDistanceField = (target: vec2): number =>
|
|
||||||
objects.reduce((min, i) => (min = Math.min(min, i.distance(target))), 1000);
|
for (let i = 0; i < worldSize / 400; i++) {
|
||||||
|
console.log('planet', i);
|
||||||
|
|
||||||
for (let i = 0; i < 400; i++) {
|
|
||||||
let position: vec2;
|
let position: vec2;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
position = vec2.fromValues(
|
position = vec2.fromValues(
|
||||||
Random.getRandomInRange(-width / 2, width / 2),
|
Random.getRandomInRange(settings.worldLeftEdge, settings.worldRightEdge),
|
||||||
Random.getRandomInRange(0, height),
|
Random.getRandomInRange(settings.worldBottomEdge, settings.worldTopEdge),
|
||||||
);
|
);
|
||||||
} while (
|
} while (
|
||||||
calculateDistanceField(position) < 800 ||
|
evaluateSdf(position, objects) < 800 ||
|
||||||
calculateDistanceField(position) > 2000
|
evaluateSdf(position, objects) > 2500
|
||||||
);
|
);
|
||||||
|
|
||||||
objects.push(
|
objects.push(
|
||||||
|
|
@ -38,27 +37,22 @@ export const createDungeon = (objectContainer: PhysicalContainer) => {
|
||||||
position,
|
position,
|
||||||
Random.getRandomInRange(300, 800),
|
Random.getRandomInRange(300, 800),
|
||||||
Random.getRandomInRange(300, 800),
|
Random.getRandomInRange(300, 800),
|
||||||
Random.getRandomInRange(10, 40),
|
Random.getRandomInRange(20, 40),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tryCount = 0;
|
for (let i = 0; i < worldSize / 350; i++) {
|
||||||
lightGeneration: for (let i = 0; i < 300; i++) {
|
console.log('light', i);
|
||||||
console.log(i);
|
|
||||||
let position: vec2;
|
let position: vec2;
|
||||||
do {
|
do {
|
||||||
position = vec2.fromValues(
|
position = vec2.fromValues(
|
||||||
Random.getRandomInRange(-width / 2, width / 2),
|
Random.getRandomInRange(settings.worldLeftEdge, settings.worldRightEdge),
|
||||||
Random.getRandomInRange(-1000, height),
|
Random.getRandomInRange(settings.worldBottomEdge, settings.worldTopEdge),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tryCount++ > 1e4) {
|
|
||||||
break lightGeneration;
|
|
||||||
}
|
|
||||||
} while (
|
} while (
|
||||||
calculateDistanceField(position) < 200 ||
|
evaluateSdf(position, objects) < 200 ||
|
||||||
lights.find((l) => l.distance(position) < 1200)
|
lights.find((l) => l.distance(position) < 1800)
|
||||||
);
|
);
|
||||||
lights.push(
|
lights.push(
|
||||||
new LampPhysical(
|
new LampPhysical(
|
||||||
|
|
@ -84,7 +78,7 @@ const createBlob = (
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
randomness: number,
|
randomness: number,
|
||||||
): StonePhysical => {
|
): PlanetPhysical => {
|
||||||
const vertices = [];
|
const vertices = [];
|
||||||
|
|
||||||
for (let i = 0; i < settings.polygonEdgeCount; i++) {
|
for (let i = 0; i < settings.polygonEdgeCount; i++) {
|
||||||
|
|
@ -99,29 +93,5 @@ const createBlob = (
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return new StonePhysical(vertices);
|
return new PlanetPhysical(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,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,70 @@ import {
|
||||||
clamp,
|
clamp,
|
||||||
last,
|
last,
|
||||||
GameObject,
|
GameObject,
|
||||||
|
Circle,
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
import { DynamicPhysical } from '../physics/conatiners/dynamic-physical';
|
import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
||||||
import { CirclePhysical } from './circle-physical';
|
import { CirclePhysical } from './circle-physical';
|
||||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
import { Spring } from './spring';
|
|
||||||
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
||||||
import { ProjectilePhysical } from './projectile-physical';
|
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)
|
@serializesTo(CharacterBase)
|
||||||
export class CharacterPhysical extends CharacterBase implements DynamicPhysical {
|
export class CharacterPhysical
|
||||||
|
extends CharacterBase
|
||||||
|
implements DynamicPhysical, ReactsToCollision {
|
||||||
public readonly canCollide = true;
|
public readonly canCollide = true;
|
||||||
public readonly canMove = 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 isDestroyed = false;
|
||||||
private jumpEnergyLeft = settings.defaultJumpEnergy;
|
private direction = 0;
|
||||||
|
private currentPlanet?: PlanetPhysical;
|
||||||
|
private lastMovementWasRelative = false;
|
||||||
|
private secondsSinceOnSurface = 1000;
|
||||||
|
|
||||||
public head: CirclePhysical;
|
public head: CirclePhysical;
|
||||||
public leftFoot: CirclePhysical;
|
public leftFoot: CirclePhysical;
|
||||||
|
|
@ -29,37 +79,31 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
public bound: CirclePhysical;
|
public bound: CirclePhysical;
|
||||||
|
|
||||||
private movementActions: Array<MoveActionCommand> = [];
|
private movementActions: Array<MoveActionCommand> = [];
|
||||||
private lastMovementAction: MoveActionCommand = new MoveActionCommand(vec2.create());
|
private lastMovementAction: MoveActionCommand = new MoveActionCommand(
|
||||||
|
vec2.create(),
|
||||||
public handleMovementAction(c: MoveActionCommand) {
|
false,
|
||||||
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);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly colorIndex: number,
|
public readonly colorIndex: number,
|
||||||
private readonly container: PhysicalContainer,
|
private readonly container: PhysicalContainer,
|
||||||
|
startPosition: vec2,
|
||||||
) {
|
) {
|
||||||
super(id(), colorIndex);
|
super(id(), colorIndex);
|
||||||
this.head = new CirclePhysical(
|
this.head = new CirclePhysical(
|
||||||
vec2.clone(CharacterPhysical.headOffset),
|
vec2.add(vec2.create(), startPosition, CharacterPhysical.headOffset),
|
||||||
CharacterPhysical.headRadius,
|
CharacterPhysical.headRadius,
|
||||||
this,
|
this,
|
||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
this.leftFoot = new CirclePhysical(
|
this.leftFoot = new CirclePhysical(
|
||||||
vec2.clone(CharacterPhysical.leftFootOffset),
|
vec2.add(vec2.create(), startPosition, CharacterPhysical.leftFootOffset),
|
||||||
CharacterPhysical.feetRadius,
|
CharacterPhysical.feetRadius,
|
||||||
this,
|
this,
|
||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
this.rightFoot = new CirclePhysical(
|
this.rightFoot = new CirclePhysical(
|
||||||
vec2.clone(CharacterPhysical.rightFootOffset),
|
vec2.add(vec2.create(), startPosition, CharacterPhysical.rightFootOffset),
|
||||||
CharacterPhysical.feetRadius,
|
CharacterPhysical.feetRadius,
|
||||||
this,
|
this,
|
||||||
container,
|
container,
|
||||||
|
|
@ -70,12 +114,16 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
|
|
||||||
this.bound = new CirclePhysical(
|
this.bound = new CirclePhysical(
|
||||||
vec2.create(),
|
vec2.create(),
|
||||||
(CharacterPhysical.headRadius + CharacterPhysical.feetRadius * 2) * 2,
|
CharacterPhysical.boundRadius,
|
||||||
this,
|
this,
|
||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public handleMovementAction(c: MoveActionCommand) {
|
||||||
|
this.movementActions.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
public onCollision(other: GameObject) {
|
public onCollision(other: GameObject) {
|
||||||
if (other instanceof ProjectilePhysical) {
|
if (other instanceof ProjectilePhysical) {
|
||||||
other.destroy();
|
other.destroy();
|
||||||
|
|
@ -110,7 +158,7 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sumAndResetMovementActions(): vec2 {
|
private averageAndResetMovementActions(): vec2 {
|
||||||
let direction: vec2;
|
let direction: vec2;
|
||||||
if (this.movementActions.length === 0) {
|
if (this.movementActions.length === 0) {
|
||||||
direction = vec2.clone(this.lastMovementAction.direction);
|
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);
|
vec2.scale(direction, direction, 1 / this.movementActions.length);
|
||||||
|
|
||||||
|
this.lastMovementWasRelative =
|
||||||
|
this.movementActions.find((a) => a.isCharacterRelative) !== undefined;
|
||||||
|
|
||||||
this.lastMovementAction = last(this.movementActions)!;
|
this.lastMovementAction = last(this.movementActions)!;
|
||||||
this.movementActions = [];
|
this.movementActions = [];
|
||||||
}
|
}
|
||||||
|
|
@ -129,82 +180,104 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
return direction;
|
return direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public step(deltaTimeInMiliseconds: number) {
|
public step(deltaTime: number) {
|
||||||
const deltaTime = deltaTimeInMiliseconds / 1000;
|
if ((this.secondsSinceOnSurface += deltaTime) > 1) {
|
||||||
const direction = this.sumAndResetMovementActions();
|
this.currentPlanet = undefined;
|
||||||
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);
|
|
||||||
|
|
||||||
const xMax = deltaTime * settings.maxAccelerationX;
|
const intersectingWithForcefield = this.container.findIntersecting(
|
||||||
const yMax = this.jumpEnergyLeft > 0 ? settings.maxAccelerationY : 0;
|
getBoundingBoxOfCircle(
|
||||||
const movementForce = vec2.multiply(
|
new Circle(
|
||||||
direction,
|
this.center,
|
||||||
direction,
|
CharacterPhysical.boundRadius + settings.maxGravityDistance,
|
||||||
vec2.fromValues(xMax, yMax),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
const actualGravity = forceAtPosition(this.center, intersectingWithForcefield);
|
||||||
|
|
||||||
Spring.step(
|
const direction = this.averageAndResetMovementActions();
|
||||||
this.leftFoot,
|
const movementForce = vec2.scale(direction, direction, settings.maxAcceleration);
|
||||||
this.rightFoot,
|
|
||||||
vec2.distance(CharacterPhysical.leftFootOffset, CharacterPhysical.rightFootOffset),
|
if (!this.currentPlanet) {
|
||||||
300,
|
this.applyForce(this.leftFoot, actualGravity, deltaTime);
|
||||||
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.leftFoot, movementForce, deltaTime);
|
||||||
this.applyForce(this.rightFoot, movementForce, deltaTime);
|
this.applyForce(this.rightFoot, movementForce, deltaTime);
|
||||||
|
|
||||||
if (feetAirborne) {
|
this.stepBodyPart(this.leftFoot, deltaTime);
|
||||||
this.applyForce(this.head, settings.gravitationalForce, 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);
|
private stepBodyPart(part: CirclePhysical, deltaTime: number) {
|
||||||
this.leftFoot.step2(deltaTime);
|
const hitObject = part.step2(deltaTime);
|
||||||
this.rightFoot.step2(deltaTime);
|
|
||||||
|
|
||||||
let sumBody = vec2.add(vec2.create(), this.head.center, this.leftFoot.center);
|
if (hitObject instanceof PlanetPhysical) {
|
||||||
vec2.add(sumBody, sumBody, this.rightFoot.center);
|
this.secondsSinceOnSurface = 0;
|
||||||
vec2.scale(sumBody, sumBody, 1 / 3);
|
this.currentPlanet = hitObject;
|
||||||
|
}
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public applyForce(circle: CirclePhysical, force: vec2, timeInSeconds: number) {
|
public applyForce(circle: CirclePhysical, force: vec2, timeInSeconds: number) {
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,28 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
import { Circle, GameObject, serializesTo, settings } from 'shared';
|
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 { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||||
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
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 { 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)
|
@serializesTo(Circle)
|
||||||
export class CirclePhysical implements Circle, DynamicPhysical {
|
export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollision {
|
||||||
readonly canCollide = true;
|
readonly canCollide = true;
|
||||||
readonly canMove = true;
|
readonly canMove = true;
|
||||||
|
|
||||||
private _isAirborne = true;
|
|
||||||
public velocity = vec2.create();
|
public velocity = vec2.create();
|
||||||
|
|
||||||
public get isAirborne(): boolean {
|
|
||||||
return this._isAirborne;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _boundingBox: BoundingBox;
|
private _boundingBox: BoundingBox;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _center: vec2,
|
private _center: vec2,
|
||||||
private _radius: number,
|
private _radius: number,
|
||||||
public owner: DynamicPhysical,
|
public owner: GameObject,
|
||||||
private readonly container: PhysicalContainer,
|
private readonly container: PhysicalContainer,
|
||||||
private restitution = 0,
|
private restitution = 0,
|
||||||
) {
|
) {
|
||||||
|
|
@ -47,7 +44,9 @@ export class CirclePhysical implements Circle, DynamicPhysical {
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCollision(other: GameObject) {
|
public onCollision(other: GameObject) {
|
||||||
this.owner.onCollision(other);
|
if (reactsToCollision(this.owner)) {
|
||||||
|
this.owner.onCollision(other);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get gameObject(): GameObject {
|
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;
|
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;
|
return other.distance(this.center) < this.radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isInside(other: Physical): boolean {
|
public isInside(other: PhysicalBase): boolean {
|
||||||
return other.distance(this.center) < -this.radius;
|
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(
|
vec2.scale(
|
||||||
this.velocity,
|
this.velocity,
|
||||||
this.velocity,
|
this.velocity,
|
||||||
|
|
@ -108,7 +107,7 @@ export class CirclePhysical implements Circle, DynamicPhysical {
|
||||||
const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep);
|
const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep);
|
||||||
vec2.scale(delta, delta, 1 / stepCount);
|
vec2.scale(delta, delta, 1 / stepCount);
|
||||||
|
|
||||||
let wasHit = false;
|
let lastHit: GameObject | undefined;
|
||||||
|
|
||||||
for (let i = 0; i < stepCount; i++) {
|
for (let i = 0; i < stepCount; i++) {
|
||||||
const distance = vec2.scale(
|
const distance = vec2.scale(
|
||||||
|
|
@ -120,7 +119,11 @@ export class CirclePhysical implements Circle, DynamicPhysical {
|
||||||
const intersecting = this.container.findIntersecting(this.boundingBox);
|
const intersecting = this.container.findIntersecting(this.boundingBox);
|
||||||
this.radius -= vec2.length(distance);
|
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) {
|
if (hitSurface) {
|
||||||
vec2.subtract(
|
vec2.subtract(
|
||||||
|
|
@ -133,40 +136,37 @@ export class CirclePhysical implements Circle, DynamicPhysical {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
wasHit = true;
|
lastHit = hitObject;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._isAirborne = !wasHit;
|
return lastHit;
|
||||||
return wasHit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public tryMove(delta: vec2) {
|
public tryMove(delta: vec2): GameObject | undefined {
|
||||||
const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep);
|
const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep);
|
||||||
vec2.scale(delta, delta, 1 / stepCount);
|
vec2.scale(delta, delta, 1 / stepCount);
|
||||||
|
|
||||||
let wasHit = false;
|
let lastHit: GameObject | undefined;
|
||||||
|
|
||||||
for (let i = 0; i < stepCount; i++) {
|
for (let i = 0; i < stepCount; i++) {
|
||||||
this.radius += vec2.length(delta);
|
this.radius += vec2.length(delta);
|
||||||
const intersecting = this.container.findIntersecting(this.boundingBox);
|
const intersecting = this.container.findIntersecting(this.boundingBox);
|
||||||
this.radius -= vec2.length(delta);
|
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) {
|
if (hitSurface) {
|
||||||
vec2.subtract(
|
delta = vec2.scale(delta, tangent!, vec2.length(delta));
|
||||||
delta,
|
lastHit = hitObject;
|
||||||
delta,
|
|
||||||
vec2.scale(normal!, normal!, vec2.dot(normal!, delta)),
|
|
||||||
);
|
|
||||||
|
|
||||||
wasHit = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._isAirborne = !wasHit;
|
return lastHit;
|
||||||
return wasHit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
public toArray(): Array<any> {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { vec2, vec3 } from 'gl-matrix';
|
||||||
import { LampBase, settings, id, serializesTo } from 'shared';
|
import { LampBase, settings, id, serializesTo } from 'shared';
|
||||||
|
|
||||||
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
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)
|
@serializesTo(LampBase)
|
||||||
export class LampPhysical extends LampBase implements StaticPhysical {
|
export class LampPhysical extends LampBase implements StaticPhysical {
|
||||||
|
|
@ -32,6 +32,10 @@ export class LampPhysical extends LampBase implements StaticPhysical {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getForce(_: vec2): vec2 {
|
||||||
|
return vec2.create();
|
||||||
|
}
|
||||||
|
|
||||||
public distance(target: vec2): number {
|
public distance(target: vec2): number {
|
||||||
return vec2.distance(this.center, target);
|
return vec2.distance(this.center, target);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,31 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
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 { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
||||||
import { StaticPhysical } from '../physics/containers/static-physical';
|
import { StaticPhysical } from '../physics/physicals/static-physical';
|
||||||
|
|
||||||
@serializesTo(StoneBase)
|
@serializesTo(PlanetBase)
|
||||||
export class StonePhysical extends StoneBase implements StaticPhysical {
|
export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
||||||
public readonly canCollide = true;
|
public readonly canCollide = true;
|
||||||
public readonly canMove = false;
|
public readonly canMove = false;
|
||||||
|
private center: vec2;
|
||||||
|
|
||||||
private _boundingBox?: ImmutableBoundingBox;
|
private _boundingBox?: ImmutableBoundingBox;
|
||||||
|
|
||||||
constructor(vertices: Array<vec2>) {
|
constructor(vertices: Array<vec2>) {
|
||||||
super(id(), vertices);
|
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 {
|
public distance(target: vec2): number {
|
||||||
|
|
@ -72,6 +85,46 @@ export class StonePhysical extends StoneBase implements StaticPhysical {
|
||||||
return this._boundingBox;
|
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 {
|
public get gameObject(): this {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
@ -2,11 +2,15 @@ import { vec2 } from 'gl-matrix';
|
||||||
import { id, settings, serializesTo, ProjectileBase, GameObject } from 'shared';
|
import { id, settings, serializesTo, ProjectileBase, GameObject } from 'shared';
|
||||||
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
||||||
import { CirclePhysical } from './circle-physical';
|
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 { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
|
import { PlanetPhysical } from './planet-physical';
|
||||||
|
import { ReactsToCollision } from '../physics/physicals/reacts-to-collision';
|
||||||
|
|
||||||
@serializesTo(ProjectileBase)
|
@serializesTo(ProjectileBase)
|
||||||
export class ProjectilePhysical extends ProjectileBase implements DynamicPhysical {
|
export class ProjectilePhysical
|
||||||
|
extends ProjectileBase
|
||||||
|
implements DynamicPhysical, ReactsToCollision {
|
||||||
public readonly canCollide = true;
|
public readonly canCollide = true;
|
||||||
public readonly canMove = true;
|
public readonly canMove = true;
|
||||||
private isDestroyed = false;
|
private isDestroyed = false;
|
||||||
|
|
@ -55,14 +59,16 @@ export class ProjectilePhysical extends ProjectileBase implements DynamicPhysica
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public step(deltaTimeInMiliseconds: number) {
|
public step(deltaTime: number) {
|
||||||
const deltaTime = deltaTimeInMiliseconds / 1000;
|
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(
|
vec2.add(this.velocity, this.velocity, vec2.scale(vec2.create(), gravity, deltaTime));
|
||||||
this.velocity,
|
|
||||||
this.velocity,
|
|
||||||
vec2.scale(vec2.create(), settings.gravitationalForce, deltaTime),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.object.velocity = this.velocity;
|
this.object.velocity = this.velocity;
|
||||||
this.object.step2(deltaTime);
|
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 { BoundingBoxList } from './bounding-box-list';
|
||||||
import { BoundingBoxTree } from './bounding-box-tree';
|
import { BoundingBoxTree } from './bounding-box-tree';
|
||||||
|
|
||||||
import { Physical } from '../physical';
|
import { Physical } from '../physicals/physical';
|
||||||
import { StaticPhysical } from './static-physical';
|
import { StaticPhysical } from '../physicals/static-physical';
|
||||||
import { DynamicPhysical } from './dynamic-physical';
|
import { DynamicPhysical } from '../physicals/dynamic-physical';
|
||||||
|
|
||||||
export class PhysicalContainer {
|
export class PhysicalContainer {
|
||||||
private isTreeInitialized = false;
|
private isTreeInitialized = false;
|
||||||
|
|
@ -34,8 +34,8 @@ export class PhysicalContainer {
|
||||||
this.dynamicBoundingBoxes.remove(object);
|
this.dynamicBoundingBoxes.remove(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
public stepObjects(deltaTimeInMilliseconds: number) {
|
public stepObjects(deltaTime: number) {
|
||||||
this.dynamicBoundingBoxes.forEach((o) => o.step(deltaTimeInMilliseconds));
|
this.dynamicBoundingBoxes.forEach((o) => o.step(deltaTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
public findIntersecting(box: BoundingBoxBase): Array<Physical> {
|
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 { vec2 } from 'gl-matrix';
|
||||||
import { Circle, rotate90Deg } from 'shared';
|
import { Circle, GameObject, rotate90Deg } from 'shared';
|
||||||
import { CirclePhysical } from '../objects/circle-physical';
|
import { CirclePhysical } from '../../objects/circle-physical';
|
||||||
import { DynamicPhysical } from './containers/dynamic-physical';
|
import { reactsToCollision } from '../physicals/reacts-to-collision';
|
||||||
import { Physical } from './physical';
|
import { evaluateSdf } from './evaluate-sdf';
|
||||||
|
import { Physical } from '../physicals/physical';
|
||||||
|
|
||||||
export const moveCircle = (
|
export const moveCircle = (
|
||||||
circle: CirclePhysical,
|
circle: CirclePhysical,
|
||||||
|
|
@ -13,6 +14,7 @@ export const moveCircle = (
|
||||||
hitSurface: boolean;
|
hitSurface: boolean;
|
||||||
normal?: vec2;
|
normal?: vec2;
|
||||||
tangent?: vec2;
|
tangent?: vec2;
|
||||||
|
hitObject?: GameObject;
|
||||||
} => {
|
} => {
|
||||||
const nextCircle = new Circle(vec2.clone(circle.center), circle.radius);
|
const nextCircle = new Circle(vec2.clone(circle.center), circle.radius);
|
||||||
vec2.add(nextCircle.center, nextCircle.center, delta);
|
vec2.add(nextCircle.center, nextCircle.center, delta);
|
||||||
|
|
@ -21,13 +23,7 @@ export const moveCircle = (
|
||||||
(b) => b.gameObject !== circle.gameObject && b.canCollide,
|
(b) => b.gameObject !== circle.gameObject && b.canCollide,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getSdfAtPoint = (point: vec2): number => {
|
const sdfAtCenter = evaluateSdf(nextCircle.center, possibleIntersectors);
|
||||||
return possibleIntersectors
|
|
||||||
.filter((i) => i.canCollide)
|
|
||||||
.reduce((min, i) => (min = Math.min(min, i.distance(point))), 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sdfAtCenter = getSdfAtPoint(nextCircle.center);
|
|
||||||
|
|
||||||
if (sdfAtCenter > nextCircle.radius) {
|
if (sdfAtCenter > nextCircle.radius) {
|
||||||
circle.center = vec2.add(circle.center, circle.center, delta);
|
circle.center = vec2.add(circle.center, circle.center, delta);
|
||||||
|
|
@ -38,21 +34,26 @@ export const moveCircle = (
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const intersecting = possibleIntersectors
|
const intersecting = possibleIntersectors.find(
|
||||||
.filter((i) => i.canCollide && i.canMove)
|
(i) => i.distance(nextCircle.center) <= circle.radius,
|
||||||
.find((i) => i.distance(nextCircle.center) < circle.radius);
|
)!;
|
||||||
|
|
||||||
(intersecting?.gameObject as DynamicPhysical)?.onCollision(circle.gameObject);
|
if (reactsToCollision(intersecting)) {
|
||||||
((circle.gameObject as unknown) as DynamicPhysical).onCollision(
|
intersecting.onCollision(circle.gameObject);
|
||||||
intersecting?.gameObject,
|
}
|
||||||
);
|
|
||||||
|
if (reactsToCollision(circle)) {
|
||||||
|
circle.onCollision(intersecting.gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
const dx =
|
const dx =
|
||||||
getSdfAtPoint(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(1, 0))) -
|
evaluateSdf(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0.01, 0)), [
|
||||||
sdfAtCenter;
|
intersecting,
|
||||||
|
]) - sdfAtCenter;
|
||||||
const dy =
|
const dy =
|
||||||
getSdfAtPoint(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0, 1))) -
|
evaluateSdf(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0, 0.01)), [
|
||||||
sdfAtCenter;
|
intersecting,
|
||||||
|
]) - sdfAtCenter;
|
||||||
const normal = vec2.fromValues(dx, dy);
|
const normal = vec2.fromValues(dx, dy);
|
||||||
vec2.normalize(normal, normal);
|
vec2.normalize(normal, normal);
|
||||||
const rotatedNormal = rotate90Deg(normal);
|
const rotatedNormal = rotate90Deg(normal);
|
||||||
|
|
@ -60,6 +61,7 @@ export const moveCircle = (
|
||||||
realDelta: delta,
|
realDelta: delta,
|
||||||
hitSurface: true,
|
hitSurface: true,
|
||||||
normal,
|
normal,
|
||||||
|
hitObject: intersecting?.gameObject,
|
||||||
tangent:
|
tangent:
|
||||||
vec2.dot(rotatedNormal, delta) < 0
|
vec2.dot(rotatedNormal, delta) < 0
|
||||||
? vec2.scale(rotatedNormal, rotatedNormal, -1)
|
? 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 { vec2 } from 'gl-matrix';
|
||||||
import { GameObject } from 'shared';
|
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 canCollide: boolean;
|
||||||
readonly canMove: boolean;
|
readonly canMove: boolean;
|
||||||
readonly boundingBox: BoundingBoxBase;
|
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"]
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import { glMatrix } from 'gl-matrix';
|
import { glMatrix } from 'gl-matrix';
|
||||||
import { CharacterBase, LampBase, overrideDeserialization, StoneBase } from 'shared';
|
import { CharacterBase, LampBase, overrideDeserialization, PlanetBase } from 'shared';
|
||||||
import { ProjectileBase } from 'shared/src/objects/types/projectile-base';
|
import { ProjectileBase } from 'shared/src/objects/types/projectile-base';
|
||||||
import { Game } from './scripts/game';
|
import { Game } from './scripts/game';
|
||||||
import { CharacterView } from './scripts/objects/character-view';
|
import { CharacterView } from './scripts/objects/character-view';
|
||||||
import { LampView } from './scripts/objects/lamp-view';
|
import { LampView } from './scripts/objects/lamp-view';
|
||||||
import { ProjectileView } from './scripts/objects/projectile-view';
|
import { ProjectileView } from './scripts/objects/projectile-view';
|
||||||
import { StoneView } from './scripts/objects/stone-view';
|
import { PlanetView } from './scripts/objects/planet-view';
|
||||||
import './styles/main.scss';
|
import './styles/main.scss';
|
||||||
|
|
||||||
glMatrix.setMatrixArrayType(Array);
|
glMatrix.setMatrixArrayType(Array);
|
||||||
|
|
||||||
overrideDeserialization(CharacterBase, CharacterView);
|
overrideDeserialization(CharacterBase, CharacterView);
|
||||||
overrideDeserialization(StoneBase, StoneView);
|
overrideDeserialization(PlanetBase, PlanetView);
|
||||||
overrideDeserialization(LampBase, LampView);
|
overrideDeserialization(LampBase, LampView);
|
||||||
overrideDeserialization(ProjectileBase, ProjectileView);
|
overrideDeserialization(ProjectileBase, ProjectileView);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export class KeyboardListener extends CommandGenerator {
|
||||||
if (vec2.squaredLength(movement) > 0) {
|
if (vec2.squaredLength(movement) > 0) {
|
||||||
vec2.normalize(movement, movement);
|
vec2.normalize(movement, movement);
|
||||||
}
|
}
|
||||||
this.sendCommandToSubcribers(new MoveActionCommand(movement));
|
this.sendCommandToSubcribers(new MoveActionCommand(movement, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalize(key: string): string {
|
private normalize(key: string): string {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export class TouchListener extends CommandGenerator {
|
||||||
|
|
||||||
if (vec2.squaredLength(movement) > 0) {
|
if (vec2.squaredLength(movement) > 0) {
|
||||||
vec2.normalize(movement, movement);
|
vec2.normalize(movement, movement);
|
||||||
this.sendCommandToSubcribers(new MoveActionCommand(movement));
|
this.sendCommandToSubcribers(new MoveActionCommand(movement, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.previousPosition = position;
|
this.previousPosition = position;
|
||||||
|
|
@ -46,7 +46,7 @@ export class TouchListener extends CommandGenerator {
|
||||||
|
|
||||||
target.addEventListener('touchend', (event: TouchEvent) => {
|
target.addEventListener('touchend', (event: TouchEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.sendCommandToSubcribers(new MoveActionCommand(vec2.create()));
|
this.sendCommandToSubcribers(new MoveActionCommand(vec2.create(), false));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
import {
|
import {
|
||||||
Circle,
|
|
||||||
CircleLight,
|
CircleLight,
|
||||||
compile,
|
compile,
|
||||||
FilteringOptions,
|
FilteringOptions,
|
||||||
|
|
@ -28,6 +27,7 @@ import { Configuration } from './config/configuration';
|
||||||
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
|
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
|
||||||
import { GameObjectContainer } from './objects/game-object-container';
|
import { GameObjectContainer } from './objects/game-object-container';
|
||||||
import { BlobShape } from './shapes/blob-shape';
|
import { BlobShape } from './shapes/blob-shape';
|
||||||
|
import { Circle } from './shapes/circle';
|
||||||
import { Polygon } from './shapes/polygon';
|
import { Polygon } from './shapes/polygon';
|
||||||
|
|
||||||
export class Game {
|
export class Game {
|
||||||
|
|
@ -74,14 +74,14 @@ export class Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setupRenderer(): Promise<void> {
|
private async setupRenderer(): Promise<void> {
|
||||||
const noiseTexture = await renderNoise([256, 256], 2, 1 / 10);
|
const noiseTexture = await renderNoise([256, 256], 2, 1);
|
||||||
|
|
||||||
this.renderer = await compile(
|
this.renderer = await compile(
|
||||||
this.canvas,
|
this.canvas,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
...Polygon.descriptor,
|
...Polygon.descriptor,
|
||||||
shaderCombinationSteps: [0, 2, 6, 16, 32],
|
shaderCombinationSteps: [0, 2, 6, 16],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...BlobShape.descriptor,
|
...BlobShape.descriptor,
|
||||||
|
|
@ -89,7 +89,7 @@ export class Game {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...Circle.descriptor,
|
...Circle.descriptor,
|
||||||
shaderCombinationSteps: [0, 2, 16, 32],
|
shaderCombinationSteps: [0, 2, 16],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...CircleLight.descriptor,
|
...CircleLight.descriptor,
|
||||||
|
|
@ -115,7 +115,7 @@ export class Game {
|
||||||
rgb(1, 1, 1),
|
rgb(1, 1, 1),
|
||||||
...settings.playerColors,
|
...settings.playerColors,
|
||||||
],
|
],
|
||||||
enableHighDpiRendering: false,
|
enableHighDpiRendering: true,
|
||||||
lightCutoffDistance: settings.lightCutoffDistance,
|
lightCutoffDistance: settings.lightCutoffDistance,
|
||||||
textures: {
|
textures: {
|
||||||
noiseTexture: {
|
noiseTexture: {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export class Camera extends GameObject implements ViewObject {
|
||||||
renderer.setViewArea(viewArea.topLeft, viewArea.size);
|
renderer.setViewArea(viewArea.topLeft, viewArea.size);
|
||||||
|
|
||||||
renderer.setRuntimeSettings({
|
renderer.setRuntimeSettings({
|
||||||
backgroundColor: mixRgb(
|
ambientLight: mixRgb(
|
||||||
settings.backgroundGradient[0],
|
settings.backgroundGradient[0],
|
||||||
settings.backgroundGradient[1],
|
settings.backgroundGradient[1],
|
||||||
(this.center.x - settings.worldLeftEdge) /
|
(this.center.x - settings.worldLeftEdge) /
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
import { Drawable, Renderer } from 'sdf-2d';
|
import { Drawable, Renderer } from 'sdf-2d';
|
||||||
import { CommandExecutors, Id, Random, StoneBase } from 'shared';
|
import { CommandExecutors, Id, Random, PlanetBase } from 'shared';
|
||||||
import { RenderCommand } from '../commands/types/render';
|
import { RenderCommand } from '../commands/types/render';
|
||||||
import { Polygon } from '../shapes/polygon';
|
import { Polygon } from '../shapes/polygon';
|
||||||
import { ViewObject } from './view-object';
|
import { ViewObject } from './view-object';
|
||||||
|
|
||||||
export class StoneView extends StoneBase implements ViewObject {
|
export class PlanetView extends PlanetBase implements ViewObject {
|
||||||
private shape: Drawable;
|
private shape: Drawable;
|
||||||
|
|
||||||
protected commandExecutors: CommandExecutors = {
|
protected commandExecutors: CommandExecutors = {
|
||||||
|
|
@ -14,11 +14,12 @@ export class StoneView extends StoneBase implements ViewObject {
|
||||||
|
|
||||||
constructor(id: Id, vertices: Array<vec2>) {
|
constructor(id: Id, vertices: Array<vec2>) {
|
||||||
super(id, vertices);
|
super(id, vertices);
|
||||||
this.shape = new Polygon(vertices, Random.getRandom());
|
this.shape = new Polygon(vertices);
|
||||||
|
(this.shape as any).randomOffset = Random.getRandom();
|
||||||
}
|
}
|
||||||
|
|
||||||
public step(deltaTimeInMilliseconds: number): void {
|
public step(deltaTimeInMilliseconds: number): void {
|
||||||
this.shape.randomOffset += deltaTimeInMilliseconds / 5000;
|
(this.shape as any).randomOffset += deltaTimeInMilliseconds / 4000;
|
||||||
}
|
}
|
||||||
|
|
||||||
public draw(renderer: Renderer): void {
|
public draw(renderer: Renderer): void {
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
import { Circle, CircleLight, Renderer } from 'sdf-2d';
|
import { CircleLight, Renderer } from 'sdf-2d';
|
||||||
import { Id, ProjectileBase, rgb } from 'shared';
|
import { Id, ProjectileBase, rgb } from 'shared';
|
||||||
import { ViewObject } from './view-object';
|
import { ViewObject } from './view-object';
|
||||||
|
import { Circle } from '../shapes/circle';
|
||||||
|
|
||||||
export class ProjectileView extends ProjectileBase implements ViewObject {
|
export class ProjectileView extends ProjectileBase implements ViewObject {
|
||||||
private circle: Circle;
|
private circle: InstanceType<typeof Circle>;
|
||||||
private light: CircleLight;
|
private light: CircleLight;
|
||||||
|
|
||||||
constructor(id: Id, center: vec2, radius: number) {
|
constructor(id: Id, center: vec2, radius: number) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export class BlobShape extends Drawable {
|
||||||
uniform vec2 rightFootCenters[BLOB_COUNT];
|
uniform vec2 rightFootCenters[BLOB_COUNT];
|
||||||
uniform float headRadii[BLOB_COUNT];
|
uniform float headRadii[BLOB_COUNT];
|
||||||
uniform float footRadii[BLOB_COUNT];
|
uniform float footRadii[BLOB_COUNT];
|
||||||
uniform float blobColors[BLOB_COUNT];
|
uniform int blobColors[BLOB_COUNT];
|
||||||
|
|
||||||
float blobSmoothMin(float a, float b)
|
float blobSmoothMin(float a, float b)
|
||||||
{
|
{
|
||||||
|
|
@ -24,7 +24,7 @@ export class BlobShape extends Drawable {
|
||||||
return distance(target, circleCenter) - radius;
|
return distance(target, circleCenter) - radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
float blobMinDistance(vec2 target, out float colorIndex) {
|
float blobMinDistance(vec2 target, out vec4 color) {
|
||||||
float minDistance = 1000.0;
|
float minDistance = 1000.0;
|
||||||
|
|
||||||
for (int i = 0; i < BLOB_COUNT; i++) {
|
for (int i = 0; i < BLOB_COUNT; i++) {
|
||||||
|
|
@ -40,30 +40,24 @@ export class BlobShape extends Drawable {
|
||||||
vec2 leftEyeOffset = vec2(-headRadii[i] * 0.35, headRadii[i] * 0.2);
|
vec2 leftEyeOffset = vec2(-headRadii[i] * 0.35, headRadii[i] * 0.2);
|
||||||
vec2 rightEyeOffset = vec2(headRadii[i] * 0.35, headRadii[i] * 0.2);
|
vec2 rightEyeOffset = vec2(headRadii[i] * 0.35, headRadii[i] * 0.2);
|
||||||
|
|
||||||
res = max(
|
float eyeDistance = min(
|
||||||
res,
|
circleDistance(headCenters[i] + rightEyeOffset, headRadii[i] * 0.25, target),
|
||||||
-circleDistance(headCenters[i] + leftEyeOffset, headRadii[i] * 0.25, target)
|
circleDistance(headCenters[i] + leftEyeOffset, headRadii[i] * 0.25, target)
|
||||||
);
|
);
|
||||||
|
|
||||||
res = max(
|
eyeDistance = max(
|
||||||
res,
|
eyeDistance,
|
||||||
-circleDistance(headCenters[i] + rightEyeOffset, headRadii[i] * 0.25, target)
|
-circleDistance(headCenters[i] + leftEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.2, target)
|
||||||
);
|
|
||||||
|
|
||||||
res = min(
|
|
||||||
res,
|
|
||||||
circleDistance(headCenters[i] + leftEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.25, target)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
res = min(
|
eyeDistance = max(
|
||||||
res,
|
eyeDistance,
|
||||||
circleDistance(headCenters[i] + rightEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.25, target)
|
-circleDistance(headCenters[i] + rightEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.2, target)
|
||||||
);
|
);
|
||||||
|
|
||||||
minDistance = min(minDistance, res);
|
if (res < minDistance) {
|
||||||
|
minDistance = res;
|
||||||
if (res < 0.0) {
|
color = eyeDistance < 0.0 ? vec4(1.0) : readFromPalette(blobColors[i]);
|
||||||
colorIndex = blobColors[i];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
3
frontend/src/scripts/shapes/circle.ts
Normal file
3
frontend/src/scripts/shapes/circle.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { CircleFactory } from 'sdf-2d';
|
||||||
|
|
||||||
|
export const Circle = CircleFactory(2);
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
"gl-matrix": "^3.3.0",
|
"gl-matrix": "^3.3.0",
|
||||||
"lerna": "^3.22.1",
|
"lerna": "^3.22.1",
|
||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
"sdf-2d": "^0.4.1",
|
"sdf-2d": "^0.5.2",
|
||||||
"socket.io-client": "^2.3.1",
|
"socket.io-client": "^2.3.1",
|
||||||
"typescript": "^4.0.3",
|
"typescript": "^4.0.3",
|
||||||
"uuid": "^8.2.0"
|
"uuid": "^8.2.0"
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@ import { Command } from '../command';
|
||||||
|
|
||||||
@serializable
|
@serializable
|
||||||
export class MoveActionCommand extends Command {
|
export class MoveActionCommand extends Command {
|
||||||
public constructor(public readonly direction: vec2) {
|
public constructor(
|
||||||
|
public readonly direction: vec2,
|
||||||
|
public readonly isCharacterRelative: boolean,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
public toArray(): Array<any> {
|
||||||
return [this.direction];
|
return [this.direction, this.isCharacterRelative];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
shared/src/helper/rotate-minus-90-deg.ts
Normal file
3
shared/src/helper/rotate-minus-90-deg.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
|
export const rotateMinus90Deg = (vec: vec2): vec2 => vec2.fromValues(vec.y, -vec.x);
|
||||||
|
|
@ -26,6 +26,7 @@ export * from './helper/mix';
|
||||||
export * from './helper/random';
|
export * from './helper/random';
|
||||||
export * from './helper/id';
|
export * from './helper/id';
|
||||||
export * from './helper/rotate-90-deg';
|
export * from './helper/rotate-90-deg';
|
||||||
|
export * from './helper/rotate-minus-90-deg';
|
||||||
export * from './objects/game-object';
|
export * from './objects/game-object';
|
||||||
export * from './transport/serialization/deserialize';
|
export * from './transport/serialization/deserialize';
|
||||||
export * from './transport/serialization/serialize';
|
export * from './transport/serialization/serialize';
|
||||||
|
|
@ -34,7 +35,7 @@ export * from './transport/serialization/serializable';
|
||||||
export * from './transport/serialization/override-deserialization';
|
export * from './transport/serialization/override-deserialization';
|
||||||
export * from './objects/types/character-base';
|
export * from './objects/types/character-base';
|
||||||
export * from './objects/types/lamp-base';
|
export * from './objects/types/lamp-base';
|
||||||
export * from './objects/types/stone-base';
|
export * from './objects/types/planet-base';
|
||||||
export * from './objects/types/projectile-base';
|
export * from './objects/types/projectile-base';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
export * from './transport/transport-events';
|
export * from './transport/transport-events';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { serializable } from '../../transport/serialization/serializable';
|
||||||
import { GameObject } from '../game-object';
|
import { GameObject } from '../game-object';
|
||||||
|
|
||||||
@serializable
|
@serializable
|
||||||
export class StoneBase extends GameObject {
|
export class PlanetBase extends GameObject {
|
||||||
constructor(id: Id, public readonly vertices: Array<vec2>) {
|
constructor(id: Id, public readonly vertices: Array<vec2>) {
|
||||||
super(id);
|
super(id);
|
||||||
}
|
}
|
||||||
|
|
@ -1,21 +1,25 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
import { rgb255 } from './helper/rgb255';
|
import { rgb255 } from './helper/rgb255';
|
||||||
|
|
||||||
export const settings = {
|
export const settings = {
|
||||||
lightCutoffDistance: 600,
|
lightCutoffDistance: 600,
|
||||||
physicsMaxStep: 5,
|
physicsMaxStep: 2,
|
||||||
gravitationalForce: vec2.fromValues(0, -3000),
|
maxVelocityX: 1000,
|
||||||
maxVelocityX: 2000,
|
maxVelocityY: 1000,
|
||||||
maxVelocityY: 3650,
|
polygonEdgeCount: 7,
|
||||||
polygonEdgeCount: 8,
|
maxGravityDistance: 700,
|
||||||
projectileSpeed: 2000,
|
maxGravityQ: 180,
|
||||||
|
maxGravityStrength: 10000,
|
||||||
|
maxAcceleration: 10000,
|
||||||
|
projectileSpeed: 4000,
|
||||||
projectileMaxBounceCount: 1,
|
projectileMaxBounceCount: 1,
|
||||||
projectileStartOffset: 150,
|
projectileStartOffset: 250,
|
||||||
projectileCreationInterval: 500,
|
projectileCreationInterval: 0.1,
|
||||||
playerColorIndexOffset: 3,
|
playerColorIndexOffset: 3,
|
||||||
worldLeftEdge: -5000,
|
worldTopEdge: 10000,
|
||||||
worldRightEdge: 5000,
|
worldRightEdge: 10000,
|
||||||
backgroundGradient: [rgb255(255, 117, 129), rgb255(136, 117, 255)],
|
worldLeftEdge: -10000,
|
||||||
|
worldBottomEdge: -10000,
|
||||||
|
backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)],
|
||||||
playerColors: [
|
playerColors: [
|
||||||
rgb255(107, 48, 188),
|
rgb255(107, 48, 188),
|
||||||
rgb255(56, 254, 220),
|
rgb255(56, 254, 220),
|
||||||
|
|
@ -46,11 +50,8 @@ export const settings = {
|
||||||
rgb255(114, 73, 30),
|
rgb255(114, 73, 30),
|
||||||
rgb255(75, 75, 75),
|
rgb255(75, 75, 75),
|
||||||
],
|
],
|
||||||
maxAccelerationX: 2000000,
|
|
||||||
maxAccelerationY: 20500,
|
|
||||||
targetPhysicsDeltaTimeInMilliseconds: 20,
|
targetPhysicsDeltaTimeInMilliseconds: 20,
|
||||||
minPhysicsSleepTime: 4,
|
minPhysicsSleepTime: 4,
|
||||||
velocityAttenuation: 0.1,
|
velocityAttenuation: 0.25,
|
||||||
inViewAreaSize: 1920 * 1080 * 3,
|
inViewAreaSize: 1920 * 1080 * 3,
|
||||||
defaultJumpEnergy: 0.25,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,6 @@
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"lib": ["dom", "es2017"]
|
"lib": ["dom", "es2017"]
|
||||||
}
|
},
|
||||||
|
"include": ["**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue