diff --git a/backend/.dockerignore b/.dockerignore similarity index 60% rename from backend/.dockerignore rename to .dockerignore index a9c8231..e10427c 100644 --- a/backend/.dockerignore +++ b/.dockerignore @@ -1,3 +1,3 @@ .dockerignore Dockerfile -node_modules +**/node_modules diff --git a/Dockerfile b/Dockerfile index ae21f1e..e4e26b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,15 @@ -FROM node:14.13.0-alpine3.10 as base +FROM node:14.13.0-alpine3.10 as build COPY . . - RUN npm install && npm run initialize && npm run build + FROM node:14.13.0-alpine3.10 COPY backend/package.json . - RUN npm install --production -COPY --from=base backend/dist/main.js main.js +COPY --from=build backend/dist/main.js main.js EXPOSE 3000 - CMD [ "node", "main.js" ] diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index 3ea763a..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM node:14.13.0-alpine3.10 - -COPY package.json . - -RUN npm install --production - -COPY dist/main.js main.js - -EXPOSE 3000 - -CMD [ "node", "main.js" ] diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml deleted file mode 100644 index c44fd30..0000000 --- a/backend/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: '3.8' - -services: - decla-red-server: - init: true - image: schmelczera/decla-red-server - networks: - - network - deploy: - resources: - limits: - cpus: '1.0' - memory: 256M - reservations: - cpus: '0.25' - memory: 256M - restart_policy: - condition: on-failure - window: 30s - -networks: - network: diff --git a/backend/src/main.ts b/backend/src/main.ts index be08247..a8c860a 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -7,7 +7,6 @@ import { Random, TransportEvents, deserialize, - StepCommand, settings, } from 'shared'; import './index.html'; @@ -25,9 +24,6 @@ Random.seed = 42; const objects = new PhysicalContainer(); createDungeon(objects); -createDungeon(objects); -createDungeon(objects); -createDungeon(objects); objects.initialize(); @@ -88,8 +84,7 @@ let deltas: Array = []; const handlePhysics = () => { const delta = deltaTimeCalculator.getNextDeltaTimeInMilliseconds(); deltas.push(delta); - const step = new StepCommand(delta); - if (deltas.length > 100) { + if (deltas.length > 1000) { deltas.sort((a, b) => a - b); console.log(`Median physics time: ${deltas[50].toFixed(2)} ms`); console.log( @@ -99,17 +94,8 @@ const handlePhysics = () => { console.log(players.map((p) => p.latency)); } - if (deltas.length > 100) { - deltas.sort((a, b) => a - b); - console.log(`Median physics time: ${deltas[50].toFixed(2)} ms`); - console.log( - `Memory used: ${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)} MB`, - ); - deltas = []; - } - - objects.sendCommand(step); - players.forEach((p) => p.sendCommand(step)); + objects.stepObjects(delta); + players.forEach((p) => p.sendObjects()); const physicsDelta = deltaTimeCalculator.getDeltaTimeInMilliseconds(); deltas.push(physicsDelta); diff --git a/backend/src/map/create-dungeon.ts b/backend/src/map/create-dungeon.ts index f654b71..1c6b952 100644 --- a/backend/src/map/create-dungeon.ts +++ b/backend/src/map/create-dungeon.ts @@ -1,45 +1,127 @@ import { vec2, vec3 } from 'gl-matrix'; -import { Random } from 'shared'; +import { last, Random, settings } from 'shared'; import { LampPhysical } from '../objects/lamp-physical'; -import { TunnelPhysical } from '../objects/tunnel-physical'; +import { StonePhysical } from '../objects/stone-physical'; import { PhysicalContainer } from '../physics/containers/physical-container'; +import { Physical } from '../physics/physical'; -export const createDungeon = (objects: PhysicalContainer) => { - let previousRadius = 350; - let previousEnd = vec2.create(); +export const createDungeon = (objectContainer: PhysicalContainer) => { + const width = 100000; + const height = 10000; + const objects: Array = []; + const lights: Array = []; - let tunnelsCountSinceLastLight = 0; + 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); - for (let i = 0; i < 50000; i += 500) { - const deltaHeight = (Random.getRandom() - 0.5) * 500; - const height = previousEnd.y + deltaHeight; - const currentEnd = vec2.fromValues(i, height); - const currentToRadius = Random.getRandom() * 300 + 150; + for (let i = 0; i < 400; i++) { + let position: vec2; - const tunnel = new TunnelPhysical( - previousEnd, - currentEnd, - previousRadius, - currentToRadius, + do { + position = vec2.fromValues( + Random.getRandomInRange(-width / 2, width / 2), + Random.getRandomInRange(0, height), + ); + } while ( + calculateDistanceField(position) < 600 || + calculateDistanceField(position) > 2000 ); - objects.addObject(tunnel); - - if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.7) { - objects.addObject( - new LampPhysical( - currentEnd, - vec3.normalize( - vec3.create(), - vec3.fromValues(Random.getRandom(), 0, Random.getRandom()), - ), - 0.5, - ), - ); - tunnelsCountSinceLastLight = 0; - } - - previousEnd = currentEnd; - previousRadius = currentToRadius; + objects.push( + createBlob( + position, + Random.getRandomInRange(200, 2000), + Random.getRandomInRange(100, 500), + Random.getRandomInRange(10, 40), + ), + ); } + + let tryCount = 0; + lightGeneration: for (let i = 0; i < 300; i++) { + console.log(i); + let position: vec2; + do { + position = vec2.fromValues( + Random.getRandomInRange(-width / 2, width / 2), + Random.getRandomInRange(-1000, height), + ); + + if (tryCount++ > 1e4) { + break lightGeneration; + } + } while ( + calculateDistanceField(position) < 200 || + lights.find((l) => l.distance(position) < 1200) + ); + lights.push( + new LampPhysical( + position, + vec3.normalize( + vec3.create(), + vec3.fromValues( + Random.getRandomInRange(0.5, 1), + 0, + Random.getRandomInRange(0.5, 1), + ), + ), + Random.getRandomInRange(0.75, 1.5), + ), + ); + } + + [...objects, ...lights].forEach((o) => objectContainer.addObject(o)); +}; + +const createBlob = ( + center: vec2, + width: number, + height: number, + randomness: number, +): StonePhysical => { + const vertices = []; + + for (let i = 0; i < settings.polygonEdgeCount; i++) { + vertices.push( + vec2.fromValues( + center.x + + (width / 2) * Math.cos((i / settings.polygonEdgeCount) * -Math.PI * 2) + + Random.getRandomInRange(-randomness, randomness), + center.y + + (height / 2) * Math.sin((i / settings.polygonEdgeCount) * -Math.PI * 2) + + Random.getRandomInRange(-randomness, randomness), + ), + ); + } + return new StonePhysical(vertices); +}; + +const createFloorElement = ( + start: vec2, +): { + stone: StonePhysical; + end: vec2; +} => { + const vertices: Array = [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, + }; }; diff --git a/backend/src/objects/character-physical.ts b/backend/src/objects/character-physical.ts index 93eecd0..d1023db 100644 --- a/backend/src/objects/character-physical.ts +++ b/backend/src/objects/character-physical.ts @@ -2,25 +2,22 @@ import { vec2 } from 'gl-matrix'; import { id, CharacterBase, - StepCommand, settings, - CommandExecutors, MoveActionCommand, serializesTo, clamp, last, - Circle, } from 'shared'; +import { DynamicPhysical } from '../physics/conatiners/dynamic-physical'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; import { CirclePhysical } from './circle-physical'; -import { Physical } from '../physics/physical'; + import { PhysicalContainer } from '../physics/containers/physical-container'; import { Spring } from './spring'; @serializesTo(CharacterBase) -export class CharacterPhysical extends CharacterBase implements Physical { +export class CharacterPhysical extends CharacterBase implements DynamicPhysical { public readonly canCollide = true; - public readonly isInverted = false; public readonly canMove = true; private jumpEnergyLeft = settings.defaultJumpEnergy; @@ -32,17 +29,19 @@ export class CharacterPhysical extends CharacterBase implements Physical { private movementActions: Array = []; private lastMovementAction: MoveActionCommand = new MoveActionCommand(vec2.create()); - protected commandExecutors: CommandExecutors = { - [StepCommand.type]: this.step.bind(this), - [MoveActionCommand.type]: (c: MoveActionCommand) => this.movementActions.push(c), - }; + public handleMovementAction(c: MoveActionCommand) { + this.movementActions.push(c); + } private static readonly headOffset = vec2.fromValues(0, 40); private static readonly leftFootOffset = vec2.fromValues(-20, -35); private static readonly rightFootOffset = vec2.fromValues(20, -35); - constructor(private readonly container: PhysicalContainer) { - super(id()); + constructor( + public readonly colorIndex: number, + private readonly container: PhysicalContainer, + ) { + super(id(), colorIndex); this.head = new CirclePhysical( vec2.clone(CharacterPhysical.headOffset), 50, @@ -76,7 +75,7 @@ export class CharacterPhysical extends CharacterBase implements Physical { return this._boundingBox; } - public get gameObject(): CharacterPhysical { + public get gameObject(): this { return this; } @@ -84,6 +83,10 @@ export class CharacterPhysical extends CharacterBase implements Physical { return this.head.center; } + public get velocity(): vec2 { + return this.head.velocity; + } + public distance(target: vec2): number { return ( Math.min( @@ -113,11 +116,11 @@ export class CharacterPhysical extends CharacterBase implements Physical { return direction; } - public step(c: StepCommand) { - const deltaTime = c.deltaTimeInMiliseconds / 1000; - + public step(deltaTimeInMiliseconds: number) { + const deltaTime = deltaTimeInMiliseconds / 1000; const direction = this.sumAndResetMovementActions(); - const isAirborne = this.leftFoot.isAirborne && this.rightFoot.isAirborne; + 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); @@ -129,52 +132,80 @@ export class CharacterPhysical extends CharacterBase implements Physical { vec2.fromValues(xMax, yMax), ); - const 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); - - Spring.step(new Circle(headPosition, 0), this.head, 0, 30, deltaTime); - - const footDistance = vec2.distance( - CharacterPhysical.headOffset, - CharacterPhysical.leftFootOffset, - ); - - Spring.step( - new Circle(this.head.center, this.head.radius), - this.leftFoot, - footDistance, - 25, - deltaTime, - ); - Spring.step( - new Circle(this.head.center, this.head.radius), - this.rightFoot, - footDistance, - 25, - deltaTime, - ); Spring.step( this.leftFoot, this.rightFoot, vec2.distance(CharacterPhysical.leftFootOffset, CharacterPhysical.rightFootOffset), - 100, + 300, deltaTime, ); - this.head.applyForce(movementForce, deltaTime); - this.leftFoot.applyForce(movementForce, deltaTime); - this.rightFoot.applyForce(movementForce, deltaTime); + this.applyForce(this.head, movementForce, deltaTime); + this.applyForce(this.leftFoot, movementForce, deltaTime); + this.applyForce(this.rightFoot, movementForce, deltaTime); - this.head.applyForce(settings.gravitationalForce, deltaTime); - this.leftFoot.applyForce(settings.gravitationalForce, deltaTime); - this.rightFoot.applyForce(settings.gravitationalForce, deltaTime); + if (feetAirborne) { + this.applyForce(this.head, settings.gravitationalForce, deltaTime); + } + this.applyForce(this.leftFoot, settings.gravitationalForce, deltaTime); + this.applyForce(this.rightFoot, settings.gravitationalForce, deltaTime); - this.head.step(deltaTime); - this.leftFoot.step(deltaTime); - this.rightFoot.step(deltaTime); + this.head.step2(deltaTime); + this.leftFoot.step2(deltaTime); + this.rightFoot.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); + } + + public applyForce(circle: CirclePhysical, force: vec2, timeInSeconds: number) { + vec2.add( + circle.velocity, + circle.velocity, + vec2.scale(vec2.create(), force, timeInSeconds), + ); + + vec2.set( + circle.velocity, + clamp(circle.velocity.x, -settings.maxVelocityX, settings.maxVelocityX), + clamp(circle.velocity.y, -settings.maxVelocityY, settings.maxVelocityY), + ); } public destroy() { diff --git a/backend/src/objects/circle-physical.ts b/backend/src/objects/circle-physical.ts index a4872ad..d858630 100644 --- a/backend/src/objects/circle-physical.ts +++ b/backend/src/objects/circle-physical.ts @@ -1,20 +1,20 @@ import { vec2 } from 'gl-matrix'; -import { Circle, clamp, GameObject, serializesTo, settings } from 'shared'; +import { Circle, GameObject, serializesTo, settings } from 'shared'; import { Physical } from '../physics/physical'; import { BoundingBox } from '../physics/bounding-boxes/bounding-box'; import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base'; import { moveCircle } from '../physics/move-circle'; import { PhysicalContainer } from '../physics/containers/physical-container'; +import { DynamicPhysical } from '../physics/conatiners/dynamic-physical'; @serializesTo(Circle) -export class CirclePhysical implements Circle, Physical { - readonly isInverted = false; +export class CirclePhysical implements Circle, DynamicPhysical { readonly canCollide = true; readonly canMove = true; private _isAirborne = true; - private velocity = vec2.create(); + public velocity = vec2.create(); public get isAirborne(): boolean { return this._isAirborne; @@ -27,6 +27,7 @@ export class CirclePhysical implements Circle, Physical { private _radius: number, public owner: GameObject, private readonly container: PhysicalContainer, + private restitution = 0, ) { this._boundingBox = new BoundingBox(); this.recalculateBoundingBox(); @@ -74,19 +75,6 @@ export class CirclePhysical implements Circle, Physical { return other.distance(this.center) < -this.radius; } - public getPerimeterPoints(count: number): Array { - const result: Array = []; - for (let i = 0; i < count; i++) { - result.push( - vec2.fromValues( - Math.cos((2 * Math.PI * i) / count) * this.radius + this.center.x, - Math.sin((2 * Math.PI * i) / count) * this.radius + this.center.y, - ), - ); - } - return result; - } - private recalculateBoundingBox() { this._boundingBox.xMin = this.center.x - this._radius; this._boundingBox.xMax = this.center.x + this._radius; @@ -100,48 +88,75 @@ export class CirclePhysical implements Circle, Physical { this.velocity, vec2.scale(vec2.create(), force, timeInSeconds), ); - - vec2.set( - this.velocity, - clamp(this.velocity.x, -settings.maxVelocityX, settings.maxVelocityX), - clamp(this.velocity.y, -settings.maxVelocityY, settings.maxVelocityY), - ); } - public resetVelocity() { - this.velocity = vec2.create(); - } + public step(deltaTime: number) {} - public step(deltaTimeInSeconds: number): boolean { + public step2(deltaTimeInSeconds: number): boolean { vec2.scale( this.velocity, this.velocity, Math.pow(settings.velocityAttenuation, deltaTimeInSeconds), ); - const distance = vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds); + const delta = vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds); - const distanceLength = vec2.length(distance); - const stepCount = Math.ceil(distanceLength / settings.physicsMaxStep); - vec2.scale(distance, distance, 1 / stepCount); + const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep); + vec2.scale(delta, delta, 1 / stepCount); let wasHit = false; for (let i = 0; i < stepCount; i++) { - const { tangent, hitSurface } = moveCircle( - this, - vec2.clone(distance), - this.container.findIntersecting(this.boundingBox), + const distance = vec2.scale( + vec2.create(), + this.velocity, + deltaTimeInSeconds / stepCount, ); + this.radius += vec2.length(distance); + const intersecting = this.container.findIntersecting(this.boundingBox); + this.radius -= vec2.length(distance); + + const { normal, hitSurface } = moveCircle(this, vec2.clone(distance), intersecting); if (hitSurface) { - vec2.scale(this.velocity, tangent!, vec2.dot(tangent!, this.velocity)); - if ( - vec2.length(this.velocity) < - settings.frictionMinVelocity * deltaTimeInSeconds - ) { - this.velocity = vec2.create(); - } + vec2.subtract( + this.velocity, + this.velocity, + vec2.scale( + normal!, + normal!, + (1 + this.restitution) * vec2.dot(normal!, this.velocity), + ), + ); + + wasHit = true; + } + } + + this._isAirborne = !wasHit; + return wasHit; + } + + public tryMove(delta: vec2) { + const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep); + vec2.scale(delta, delta, 1 / stepCount); + + let wasHit = false; + + 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); + + if (hitSurface) { + vec2.subtract( + delta, + delta, + vec2.scale(normal!, normal!, vec2.dot(normal!, delta)), + ); + wasHit = true; } } diff --git a/backend/src/objects/lamp-physical.ts b/backend/src/objects/lamp-physical.ts index f93e9d9..2ca2dca 100644 --- a/backend/src/objects/lamp-physical.ts +++ b/backend/src/objects/lamp-physical.ts @@ -2,13 +2,11 @@ import { vec2, vec3 } from 'gl-matrix'; import { LampBase, settings, id, serializesTo } from 'shared'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; - -import { Physical } from '../physics/physical'; +import { StaticPhysical } from '../physics/containers/static-physical'; @serializesTo(LampBase) -export class LampPhysical extends LampBase implements Physical { +export class LampPhysical extends LampBase implements StaticPhysical { public readonly canCollide = false; - public readonly isInverted = false; public readonly canMove = false; constructor(center: vec2, color: vec3, lightness: number) { @@ -30,12 +28,11 @@ export class LampPhysical extends LampBase implements Physical { return this._boundingBox; } - public get gameObject(): LampPhysical { + public get gameObject(): this { return this; } - // todo - public distance(_: vec2): number { - return 0; + public distance(target: vec2): number { + return vec2.distance(this.center, target); } } diff --git a/backend/src/objects/projectile-physical.ts b/backend/src/objects/projectile-physical.ts new file mode 100644 index 0000000..a2f68a2 --- /dev/null +++ b/backend/src/objects/projectile-physical.ts @@ -0,0 +1,67 @@ +import { vec2 } from 'gl-matrix'; +import { + id, + settings, + serializesTo, + ProjectileBase, + GameObject, + rotate90Deg, +} from 'shared'; +import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; +import { CirclePhysical } from './circle-physical'; +import { DynamicPhysical } from '../physics/conatiners/dynamic-physical'; +import { PhysicalContainer } from '../physics/containers/physical-container'; + +@serializesTo(ProjectileBase) +export class ProjectilePhysical extends ProjectileBase implements DynamicPhysical { + public readonly canCollide = true; + public readonly canMove = true; + + public object: CirclePhysical; + + constructor( + center: vec2, + radius: number, + private velocity: vec2, + readonly container: PhysicalContainer, + ) { + super(id(), center, radius); + this.object = new CirclePhysical(center, radius, this, container, 0.9); + } + + private _boundingBox?: ImmutableBoundingBox; + + public get boundingBox(): ImmutableBoundingBox { + if (!this._boundingBox) { + this._boundingBox = (this.object as CirclePhysical).boundingBox; + } + + return this._boundingBox; + } + + public get gameObject(): this { + return this; + } + + public distance(target: vec2): number { + return this.object.distance(target); + } + + public onCollision(normal: vec2, other: GameObject) { + const tangent = rotate90Deg(normal); + vec2.scale(this.velocity, tangent, vec2.dot(tangent, this.velocity)); + } + + public step(deltaTimeInMiliseconds: number) { + const deltaTime = deltaTimeInMiliseconds / 1000; + + vec2.add( + this.velocity, + this.velocity, + vec2.scale(vec2.create(), settings.gravitationalForce, deltaTime), + ); + + this.object.velocity = this.velocity; + this.object.step2(deltaTime); + } +} diff --git a/backend/src/objects/stone-physical.ts b/backend/src/objects/stone-physical.ts new file mode 100644 index 0000000..743c3ac --- /dev/null +++ b/backend/src/objects/stone-physical.ts @@ -0,0 +1,78 @@ +import { vec2 } from 'gl-matrix'; +import { clamp01, id, serializesTo, StoneBase } from 'shared'; + +import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; +import { StaticPhysical } from '../physics/containers/static-physical'; + +@serializesTo(StoneBase) +export class StonePhysical extends StoneBase implements StaticPhysical { + public readonly canCollide = true; + public readonly canMove = false; + + private _boundingBox?: ImmutableBoundingBox; + + constructor(vertices: Array) { + super(id(), vertices); + } + + public distance(target: vec2): number { + const startEnd = this.vertices[0]; + let vb = startEnd; + + let d = vec2.squaredDistance(target, vb); + let sign = 1; + + for (let i = 1; i <= this.vertices.length; i++) { + const va = vb; + vb = i === this.vertices.length ? startEnd : this.vertices[i]; + const targetFromDelta = vec2.subtract(vec2.create(), target, va); + const toFromDelta = vec2.subtract(vec2.create(), vb, va); + const h = clamp01( + vec2.dot(targetFromDelta, toFromDelta) / vec2.squaredLength(toFromDelta), + ); + + const ds = vec2.fromValues( + vec2.dist(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)), + toFromDelta.x * targetFromDelta.y - toFromDelta.y * targetFromDelta.x, + ); + + if ( + (target.y >= va.y && target.y < vb.y && ds.y > 0) || + (target.y < va.y && target.y >= vb.y && ds.y <= 0) + ) { + sign *= -1; + } + + d = Math.min(d, ds.x); + } + + return sign * d; + } + + public get boundingBox(): ImmutableBoundingBox { + if (!this._boundingBox) { + const { xMin, xMax, yMin, yMax } = this.vertices.reduce( + (extremities, vertex) => ({ + xMin: Math.min(extremities.xMin, vertex.x), + xMax: Math.max(extremities.xMax, vertex.x), + yMin: Math.min(extremities.yMin, vertex.y), + yMax: Math.max(extremities.yMax, vertex.y), + }), + { + xMin: Infinity, + xMax: -Infinity, + yMin: Infinity, + yMax: -Infinity, + }, + ); + + this._boundingBox = new ImmutableBoundingBox(xMin, xMax, yMin, yMax); + } + + return this._boundingBox; + } + + public get gameObject(): this { + return this; + } +} diff --git a/backend/src/objects/tunnel-physical.ts b/backend/src/objects/tunnel-physical.ts deleted file mode 100644 index 54042f7..0000000 --- a/backend/src/objects/tunnel-physical.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { clamp01, mix, TunnelBase, id, serializesTo } from 'shared'; - -import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; -import { StaticPhysical } from '../physics/containers/static-physical-object'; - -@serializesTo(TunnelBase) -export class TunnelPhysical extends TunnelBase implements StaticPhysical { - public readonly canCollide = true; - public readonly isInverted = true; - public readonly canMove = false; - - private _boundingBox?: ImmutableBoundingBox; - - constructor(from: vec2, to: vec2, fromRadius: number, toRadius: number) { - super(id(), from, to, fromRadius, toRadius); - } - - public distance(target: vec2): number { - const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); - const targetFromDelta = vec2.subtract(vec2.create(), target, this.from); - - const h = clamp01( - vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta), - ); - - return ( - vec2.distance(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)) - - mix(this.fromRadius, this.toRadius, h) - ); - } - - public get boundingBox(): ImmutableBoundingBox { - if (!this._boundingBox) { - const xMin = Math.min(this.from.x - this.fromRadius, this.to.x - this.toRadius); - const yMin = Math.min(this.from.y - this.fromRadius, this.to.y - this.toRadius); - const xMax = Math.max(this.from.x + this.fromRadius, this.to.x + this.toRadius); - const yMax = Math.max(this.from.y + this.fromRadius, this.to.y + this.toRadius); - this._boundingBox = new ImmutableBoundingBox(xMin, xMax, yMin, yMax); - } - - return this._boundingBox; - } - - public get gameObject(): TunnelPhysical { - return this; - } -} diff --git a/backend/src/physics/bounding-boxes/bounding-box-base.ts b/backend/src/physics/bounding-boxes/bounding-box-base.ts index 8bb1c1b..753bbb0 100644 --- a/backend/src/physics/bounding-boxes/bounding-box-base.ts +++ b/backend/src/physics/bounding-boxes/bounding-box-base.ts @@ -8,6 +8,8 @@ export class BoundingBoxBase { protected _yMax: number = 0, ) {} + [key: number]: number | undefined; + public get 0(): number { return this._xMin; } diff --git a/backend/src/physics/containers/bounding-box-list.ts b/backend/src/physics/containers/bounding-box-list.ts index df48c07..1a9f963 100644 --- a/backend/src/physics/containers/bounding-box-list.ts +++ b/backend/src/physics/containers/bounding-box-list.ts @@ -1,21 +1,25 @@ -import { Physical } from '../physical'; +import { DynamicPhysical } from '../dynamic-physical'; import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base'; export class BoundingBoxList { - constructor(private objects: Array = []) {} + constructor(private objects: Array = []) {} - public insert(object: Physical) { + public insert(object: DynamicPhysical) { this.objects.push(object); } - public remove(object: Physical) { + public remove(object: DynamicPhysical) { this.objects.splice( this.objects.findIndex((i) => i === object), 1, ); } - public findIntersecting(boundingBox: BoundingBoxBase): Array { + public forEach(func: (object: DynamicPhysical) => unknown) { + this.objects.forEach(func); + } + + public findIntersecting(boundingBox: BoundingBoxBase): Array { return this.objects.filter((b) => b.boundingBox.intersects(boundingBox)); } } diff --git a/backend/src/physics/containers/bounding-box-tree.ts b/backend/src/physics/containers/bounding-box-tree.ts index 3821033..00b1371 100644 --- a/backend/src/physics/containers/bounding-box-tree.ts +++ b/backend/src/physics/containers/bounding-box-tree.ts @@ -1,15 +1,15 @@ import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base'; -import { StaticPhysical } from './static-physical-object'; +import { StaticPhysical } from './static-physical'; // source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js class Node { - public left?: Node = null; - public right?: Node = null; - constructor(public object: StaticPhysical, public parent: Node) {} + public left: Node | null = null; + public right: Node | null = null; + constructor(public object: StaticPhysical, public parent: Node | null) {} } export class BoundingBoxTree { - root?: Node; + root?: Node | null; constructor(objects: Array = []) { this.build(objects); @@ -22,8 +22,8 @@ export class BoundingBoxTree { private buildRecursive( objects: Array, depth: number, - parent: Node, - ): Node { + parent: Node | null, + ): Node | null { if (objects.length === 0) { return null; } @@ -34,7 +34,7 @@ export class BoundingBoxTree { const dimension = depth % 4; - objects.sort((a, b) => a.boundingBox[dimension] - b.boundingBox[dimension]); + objects.sort((a, b) => a.boundingBox[dimension]! - b.boundingBox[dimension]!); const median = Math.floor(objects.length / 2); @@ -54,7 +54,9 @@ export class BoundingBoxTree { const node = new Node(object, insertPosition); const dimension = depth % 4; - if (object.boundingBox[dimension] < insertPosition.object.boundingBox[dimension]) { + if ( + object.boundingBox[dimension]! < insertPosition.object.boundingBox[dimension]! + ) { insertPosition.left = node; } else { insertPosition.right = node; @@ -63,14 +65,14 @@ export class BoundingBoxTree { } public findIntersecting(boundingBox: BoundingBoxBase): Array { - const maybeResults = this.findMaybeIntersecting(boundingBox, this.root, 0); + const maybeResults = this.findMaybeIntersecting(boundingBox, this.root!, 0); const results = maybeResults.filter((b) => b.boundingBox.intersects(boundingBox)); return results; } private findMaybeIntersecting( boundingBox: BoundingBoxBase, - node: Node, + node: Node | null, depth: number, ): Array { if (node === null) { @@ -102,17 +104,17 @@ export class BoundingBoxTree { private findParent( object: StaticPhysical, - node: Node, + node: Node | null | undefined, depth: number, - parent: Node, - ): [Node, number] { - if (node === null) { + parent: Node | null, + ): [Node | null, number] { + if (!node) { return [parent, depth - 1]; } const dimension = depth % 4; - if (object.boundingBox[dimension] < node.object.boundingBox[dimension]) { + if (object.boundingBox[dimension]! < node.object.boundingBox[dimension]!) { return this.findParent(object, node.left, depth + 1, node); } diff --git a/backend/src/physics/containers/dynamic-physical.ts b/backend/src/physics/containers/dynamic-physical.ts new file mode 100644 index 0000000..326b56e --- /dev/null +++ b/backend/src/physics/containers/dynamic-physical.ts @@ -0,0 +1,5 @@ +import { Physical } from '../physical'; + +export interface DynamicPhysical extends Physical { + step(deltaTimeInMilliseconds: number): void; +} diff --git a/backend/src/physics/containers/physical-container.ts b/backend/src/physics/containers/physical-container.ts index ab34fda..4d1e6f4 100644 --- a/backend/src/physics/containers/physical-container.ts +++ b/backend/src/physics/containers/physical-container.ts @@ -1,11 +1,11 @@ -import { GameObject, Id } from 'shared'; import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base'; import { BoundingBoxList } from './bounding-box-list'; import { BoundingBoxTree } from './bounding-box-tree'; -import { Command } from 'shared'; + import { Physical } from '../physical'; -import { StaticPhysical } from './static-physical-object'; +import { StaticPhysical } from './static-physical'; +import { DynamicPhysical } from './dynamic-physical'; export class PhysicalContainer { private isTreeInitialized = false; @@ -13,27 +13,12 @@ export class PhysicalContainer { private staticBoundingBoxes = new BoundingBoxTree(); private dynamicBoundingBoxes = new BoundingBoxList(); - protected objects: Map = new Map(); - private objectsGroupedByAbilities: Map> = new Map(); - public initialize() { this.staticBoundingBoxes.build(this.staticBoundingBoxesWaitList); this.isTreeInitialized = true; } - public addObject(object: Physical) { - this.objects.set(object.gameObject.id, object.gameObject); - - for (const command of this.objectsGroupedByAbilities.keys()) { - if (object.gameObject.reactsToCommand(command)) { - this.objectsGroupedByAbilities.get(command).push(object.gameObject); - } - } - - this.addPhysical(object); - } - - public addPhysical(physical: Physical) { + public addObject(physical: Physical) { if (physical.canMove) { this.dynamicBoundingBoxes.insert(physical); } else { @@ -45,39 +30,12 @@ export class PhysicalContainer { } } - public removeObject(object: Physical) { - this.objects.delete(object.gameObject.id); - - for (const command of this.objectsGroupedByAbilities.keys()) { - if (object.gameObject.reactsToCommand(command)) { - const array = this.objectsGroupedByAbilities.get(command); - array.splice( - array.findIndex((i) => i.id == object.gameObject.id), - 1, - ); - } - } - + public removeObject(object: DynamicPhysical) { this.dynamicBoundingBoxes.remove(object); } - public sendCommand(e: Command) { - if (!this.objectsGroupedByAbilities.has(e.type)) { - this.createGroupForCommand(e.type); - } - - this.objectsGroupedByAbilities.get(e.type).forEach((o, _) => o.sendCommand(e)); - } - - private createGroupForCommand(commandType: string) { - const objectsReactingToCommand = []; - this.objects.forEach((o, _) => { - if (o.reactsToCommand(commandType)) { - objectsReactingToCommand.push(o); - } - }); - - this.objectsGroupedByAbilities.set(commandType, objectsReactingToCommand); + public stepObjects(deltaTimeInMilliseconds: number) { + this.dynamicBoundingBoxes.forEach((o) => o.step(deltaTimeInMilliseconds)); } public findIntersecting(box: BoundingBoxBase): Array { diff --git a/backend/src/physics/containers/static-physical-object.ts b/backend/src/physics/containers/static-physical.ts similarity index 100% rename from backend/src/physics/containers/static-physical-object.ts rename to backend/src/physics/containers/static-physical.ts diff --git a/backend/src/physics/move-circle.ts b/backend/src/physics/move-circle.ts index 1c30524..4ff6e8b 100644 --- a/backend/src/physics/move-circle.ts +++ b/backend/src/physics/move-circle.ts @@ -1,5 +1,5 @@ import { vec2 } from 'gl-matrix'; -import { rotate90Deg, settings } from 'shared'; +import { Circle, rotate90Deg } from 'shared'; import { CirclePhysical } from '../objects/circle-physical'; import { Physical } from './physical'; @@ -13,71 +13,48 @@ export const moveCircle = ( normal?: vec2; tangent?: vec2; } => { - circle.center = vec2.add(circle.center, circle.center, delta); + const nextCircle = new Circle(vec2.clone(circle.center), circle.radius); + vec2.add(nextCircle.center, nextCircle.center, delta); - const intersecting = possibleIntersectors.filter( - (b) => - b.gameObject !== circle.gameObject && circle.areIntersecting(b) && b.canCollide, + possibleIntersectors = possibleIntersectors.filter( + (b) => b.gameObject !== circle.gameObject && b.canCollide, ); - if (intersecting.length === 0) { + 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); + + if (sdfAtCenter > nextCircle.radius) { + circle.center = vec2.add(circle.center, circle.center, delta); + return { realDelta: delta, hitSurface: false, }; } - const points = circle.getPerimeterPoints(settings.hitDetectionCirclePointCount); + const dx = + getSdfAtPoint(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(1, 0))) - + sdfAtCenter; + const dy = + getSdfAtPoint(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0, 1))) - + sdfAtCenter; + const normal = vec2.fromValues(dx, dy); - const distancesOfPoints = points - .map((point) => ({ - point, - closest: intersecting - .map((i) => ({ - inverted: i.isInverted, - distance: i.distance(point), - })) - .sort((a, b) => a.distance - b.distance)[0], - })) - .filter((i) => i.closest); + vec2.normalize(normal, normal); - const distancesOfIntersectingPoints = distancesOfPoints.filter( - (d) => - (d.closest.distance > 0 && d.closest.inverted) || - (d.closest.distance < 0 && !d.closest.inverted), - ); - - if (distancesOfIntersectingPoints.length === 0) { - return { - realDelta: delta, - hitSurface: false, - }; - } - - const deltas = distancesOfIntersectingPoints.map((pointDistance) => { - vec2.subtract(pointDistance.point, circle.center, pointDistance.point); - vec2.normalize(pointDistance.point, pointDistance.point); - vec2.scale( - pointDistance.point, - pointDistance.point, - (pointDistance.closest.inverted ? 1 : -1) * pointDistance.closest.distance, - ); - return pointDistance.point; - }); - - const approxNormal = deltas.reduce( - (sum, current) => vec2.add(sum, sum, current), - vec2.create(), - ); - vec2.scale(approxNormal, approxNormal, 1 / deltas.length); - - circle.center = vec2.add(circle.center, circle.center, approxNormal); - - vec2.normalize(approxNormal, approxNormal); + const rotatedNormal = rotate90Deg(normal); return { realDelta: delta, hitSurface: true, - normal: approxNormal, - tangent: rotate90Deg(approxNormal), + normal, + tangent: + vec2.dot(rotatedNormal, delta) < 0 + ? vec2.scale(rotatedNormal, rotatedNormal, -1) + : rotatedNormal, }; }; diff --git a/backend/src/physics/physical.ts b/backend/src/physics/physical.ts index 0948fd3..962d699 100644 --- a/backend/src/physics/physical.ts +++ b/backend/src/physics/physical.ts @@ -3,7 +3,6 @@ import { GameObject } from 'shared'; import { BoundingBoxBase } from './bounding-boxes/bounding-box-base'; export interface Physical { - readonly isInverted: boolean; readonly canCollide: boolean; readonly canMove: boolean; readonly boundingBox: BoundingBoxBase; diff --git a/backend/src/players/player-color-service.ts b/backend/src/players/player-color-service.ts new file mode 100644 index 0000000..e23d49d --- /dev/null +++ b/backend/src/players/player-color-service.ts @@ -0,0 +1,15 @@ +import { settings } from 'shared'; + +const colorUsage: Array = new Array(settings.playerColors.length).fill(false); + +export const requestColor = (): number => { + const index = colorUsage.findIndex((a) => !a); + if (index >= 0) { + colorUsage[index] = true; + } + return index + settings.playerColorIndexOffset; +}; + +export const freeColor = (index: number) => { + colorUsage[index - settings.playerColorIndexOffset] = false; +}; diff --git a/backend/src/players/player.ts b/backend/src/players/player.ts index 757a004..6a86504 100644 --- a/backend/src/players/player.ts +++ b/backend/src/players/player.ts @@ -1,3 +1,4 @@ +import { vec2 } from 'gl-matrix'; import { CommandExecutors, CommandReceiver, @@ -8,16 +9,19 @@ import { serialize, TransportEvents, UpdateObjectsCommand, - StepCommand, SetAspectRatioActionCommand, calculateViewArea, + SecondaryActionCommand, + settings, } from 'shared'; import { getTimeInMilliseconds } from '../helper/get-time-in-milliseconds'; import { CharacterPhysical } from '../objects/character-physical'; +import { ProjectilePhysical } from '../objects/projectile-physical'; import { BoundingBox } from '../physics/bounding-boxes/bounding-box'; import { PhysicalContainer } from '../physics/containers/physical-container'; import { Physical } from '../physics/physical'; +import { requestColor, freeColor } from './player-color-service'; export class Player extends CommandReceiver { private character: CharacterPhysical; @@ -42,10 +46,24 @@ export class Player extends CommandReceiver { } protected commandExecutors: CommandExecutors = { - [StepCommand.type]: this.sendObjects.bind(this), [SetAspectRatioActionCommand.type]: (v: SetAspectRatioActionCommand) => (this.aspectRatio = v.aspectRatio), - [MoveActionCommand.type]: (c: MoveActionCommand) => this.character.sendCommand(c), + [MoveActionCommand.type]: (c: MoveActionCommand) => + this.character.handleMovementAction(c), + [SecondaryActionCommand.type]: (c: SecondaryActionCommand) => { + const start = vec2.clone(this.character.center); + const direction = vec2.subtract(vec2.create(), c.position, start); + vec2.normalize(direction, direction); + vec2.add( + start, + start, + vec2.scale(vec2.create(), direction, settings.projectileStartOffset), + ); + const velocity = vec2.scale(direction, direction, settings.projectileSpeed); + vec2.add(velocity, velocity, this.character.velocity); + const projectile = new ProjectilePhysical(start, 20, velocity, this.objects); + this.objects.addObject(projectile); + }, }; constructor( @@ -53,7 +71,8 @@ export class Player extends CommandReceiver { private readonly socket: SocketIO.Socket, ) { super(); - this.character = new CharacterPhysical(objects); + const colorIndex = requestColor(); + this.character = new CharacterPhysical(colorIndex, objects); this.objectsPreviouslyInViewArea.push(this.character); this.objectsInViewArea.push(this.character); @@ -70,7 +89,6 @@ export class Player extends CommandReceiver { ); this.measureLatency(); - this.sendObjects(); } @@ -89,6 +107,7 @@ export class Player extends CommandReceiver { const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter( (o) => !this.objectsInViewArea.includes(o), ); + this.objectsPreviouslyInViewArea = this.objectsInViewArea; if (noLongerIntersecting.length > 0) { @@ -113,7 +132,7 @@ export class Player extends CommandReceiver { ); } - this.socket.emit( + this.socket.volatile.emit( TransportEvents.ServerToPlayer, serialize( new UpdateObjectsCommand([ @@ -127,6 +146,7 @@ export class Player extends CommandReceiver { public destroy() { this.isActive = false; + freeColor(this.character.colorIndex); this.character.destroy(); this.objects.removeObject(this.character); } diff --git a/frontend/src/index.html b/frontend/src/index.html index c63c46c..d488574 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -1,21 +1,24 @@ + + + + - - - - + + - - + decla.red + - decla.red - - - - -
- - - - \ No newline at end of file + + + +
+ + + diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 27e9fd5..315221d 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,16 +1,19 @@ import { glMatrix } from 'gl-matrix'; -import { CharacterBase, LampBase, overrideDeserialization, TunnelBase } from 'shared'; +import { CharacterBase, LampBase, overrideDeserialization, StoneBase } from 'shared'; +import { ProjectileBase } from 'shared/src/objects/types/projectile-base'; import { Game } from './scripts/game'; import { CharacterView } from './scripts/objects/character-view'; import { LampView } from './scripts/objects/lamp-view'; -import { TunnelView } from './scripts/objects/tunnel-view'; +import { ProjectileView } from './scripts/objects/projectile-view'; +import { StoneView } from './scripts/objects/stone-view'; import './styles/main.scss'; glMatrix.setMatrixArrayType(Array); overrideDeserialization(CharacterBase, CharacterView); -overrideDeserialization(TunnelBase, TunnelView); +overrideDeserialization(StoneBase, StoneView); overrideDeserialization(LampBase, LampView); +overrideDeserialization(ProjectileBase, ProjectileView); const main = async () => { try { diff --git a/frontend/src/scripts/commands/generators/mouse-listener.ts b/frontend/src/scripts/commands/generators/mouse-listener.ts index ca0cdd7..b8cb30a 100644 --- a/frontend/src/scripts/commands/generators/mouse-listener.ts +++ b/frontend/src/scripts/commands/generators/mouse-listener.ts @@ -5,9 +5,10 @@ import { SecondaryActionCommand, TernaryActionCommand, } from 'shared'; +import { Game } from '../../game'; export class MouseListener extends CommandGenerator { - constructor(target: HTMLElement) { + constructor(target: HTMLElement, private readonly game: Game) { super(); target.addEventListener('mousemove', (event: MouseEvent) => { @@ -31,6 +32,8 @@ export class MouseListener extends CommandGenerator { } private positionFromEvent(event: MouseEvent): vec2 { - return vec2.fromValues(event.clientX, event.clientY); + return this.game.displayToWorldCoordinates( + vec2.fromValues(event.clientX, event.clientY), + ); } } diff --git a/frontend/src/scripts/commands/generators/touch-listener.ts b/frontend/src/scripts/commands/generators/touch-listener.ts index ce073bf..371cf6b 100644 --- a/frontend/src/scripts/commands/generators/touch-listener.ts +++ b/frontend/src/scripts/commands/generators/touch-listener.ts @@ -6,11 +6,12 @@ import { TernaryActionCommand, MoveActionCommand, } from 'shared'; +import { Game } from '../../game'; export class TouchListener extends CommandGenerator { private previousPosition = vec2.create(); - constructor(target: HTMLElement) { + constructor(target: HTMLElement, private readonly game: Game) { super(); target.addEventListener('touchstart', (event: TouchEvent) => { @@ -53,10 +54,12 @@ export class TouchListener extends CommandGenerator { const touches = Array.prototype.slice.call(event.touches); const center = touches.reduce( (center: vec2, touch: Touch) => - vec2.add(center, center, vec2.fromValues(-touch.clientX, touch.clientY)), + vec2.add(center, center, vec2.fromValues(-touch.clientX, -touch.clientY)), vec2.create(), ); - return vec2.scale(center, center, 1 / event.touches.length); + return this.game.displayToWorldCoordinates( + vec2.scale(center, center, 1 / event.touches.length), + ); } } diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index acfba8c..3f183ab 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -5,7 +5,6 @@ import { compile, FilteringOptions, Flashlight, - InvertedTunnel, Renderer, renderNoise, WrapOptions, @@ -13,25 +12,23 @@ import { import { broadcastCommands, deserialize, - prettyPrint, serialize, settings, - StepCommand, TransportEvents, + SetAspectRatioActionCommand, + rgb, } from 'shared'; -import { SetAspectRatioActionCommand } from 'shared/src/main'; import io from 'socket.io-client'; import { KeyboardListener } from './commands/generators/keyboard-listener'; import { MouseListener } from './commands/generators/mouse-listener'; import { TouchListener } from './commands/generators/touch-listener'; import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket'; -import { RenderCommand } from './commands/types/render'; + import { Configuration } from './config/configuration'; import { DeltaTimeCalculator } from './helper/delta-time-calculator'; -import { rgb } from './helper/rgb'; - import { GameObjectContainer } from './objects/game-object-container'; import { BlobShape } from './shapes/blob-shape'; +import { Polygon } from './shapes/polygon'; export class Game { public readonly gameObjects = new GameObjectContainer(this); @@ -46,7 +43,7 @@ export class Game { private async setupCommunication(): Promise { await Configuration.initialize(); - this.socket = io(Configuration.servers[0], { + this.socket = io(Configuration.servers[1], { reconnectionDelayMax: 10000, transports: ['websocket'], }); @@ -69,22 +66,22 @@ export class Game { broadcastCommands( [ new KeyboardListener(document.body), - new MouseListener(this.canvas), - new TouchListener(this.canvas), + new MouseListener(this.canvas, this), + new TouchListener(this.canvas, this), ], [this.gameObjects, new CommandReceiverSocket(this.socket)], ); } private async setupRenderer(): Promise { - const noiseTexture = await renderNoise([512, 1], 50, 1 / 10); + const noiseTexture = await renderNoise([64, 1], 20, 1 / 10); this.renderer = await compile( this.canvas, [ { - ...InvertedTunnel.descriptor, - shaderCombinationSteps: [0, 2, 6, 16], + ...Polygon.descriptor, + shaderCombinationSteps: [0, 2, 6, 16, 32], }, { ...BlobShape.descriptor, @@ -92,7 +89,7 @@ export class Game { }, { ...Circle.descriptor, - shaderCombinationSteps: [0, 2, 16], + shaderCombinationSteps: [0, 2, 16, 32], }, { ...CircleLight.descriptor, @@ -111,16 +108,12 @@ export class Game { ); this.renderer.setRuntimeSettings({ - isWorldInverted: true, - ambientLight: rgb(0.35, 0.1, 0.45), + ambientLight: rgb(0.45, 0.4, 0.45), colorPalette: [ - rgb(0.4, 1, 0.6), - rgb(1, 1, 0), - rgb(0.3, 1, 1), - rgb(0.3, 1, 1), - rgb(0.3, 1, 1), - rgb(0.3, 1, 1), + rgb(1, 1, 1), + rgb(0.4, 0.4, 0.4), rgb(0.3, 1, 1), + ...settings.playerColors, ], enableHighDpiRendering: false, lightCutoffDistance: settings.lightCutoffDistance, @@ -155,11 +148,11 @@ export class Game { private gameLoop(time: DOMHighResTimeStamp) { const deltaTime = this.deltaTimeCalculator.getNextDeltaTimeInMilliseconds(time); - this.gameObjects.sendCommand(new StepCommand(deltaTime)); - this.gameObjects.sendCommand(new RenderCommand(this.renderer)); + this.gameObjects.stepObjects(deltaTime); + this.gameObjects.drawObjects(this.renderer); this.renderer.renderDrawables(); - this.overlay.innerText = prettyPrint(this.renderer.insights); + // this.overlay.innerText = prettyPrint(this.renderer.insights); requestAnimationFrame(this.gameLoop.bind(this)); } } diff --git a/frontend/src/scripts/objects/camera.ts b/frontend/src/scripts/objects/camera.ts index 46a1989..25f88fb 100644 --- a/frontend/src/scripts/objects/camera.ts +++ b/frontend/src/scripts/objects/camera.ts @@ -1,29 +1,38 @@ import { vec2 } from 'gl-matrix'; -import { calculateViewArea, CommandExecutors, GameObject } from 'shared'; -import { RenderCommand } from '../commands/types/render'; -import { Game } from '../game'; +import { Renderer } from 'sdf-2d'; +import { calculateViewArea, GameObject, mixRgb, settings } from 'shared'; -export class Camera extends GameObject { +import { Game } from '../game'; +import { ViewObject } from './view-object'; + +export class Camera extends GameObject implements ViewObject { public center: vec2 = vec2.create(); private aspectRatio?: number; - protected commandExecutors: CommandExecutors = { - [RenderCommand.type]: this.draw.bind(this), - }; - constructor(private game: Game) { super(null); } - private draw(c: RenderCommand) { - const canvasAspectRatio = c.renderer.canvasSize.x / c.renderer.canvasSize.y; + public step(deltaTimeInMilliseconds: number): void {} + + public draw(renderer: Renderer) { + const canvasAspectRatio = renderer.canvasSize.x / renderer.canvasSize.y; if (canvasAspectRatio !== this.aspectRatio) { this.aspectRatio = canvasAspectRatio; this.game.aspectRatioChanged(canvasAspectRatio); } const viewArea = calculateViewArea(this.center, canvasAspectRatio); - c.renderer.setViewArea(viewArea.topLeft, viewArea.size); + renderer.setViewArea(viewArea.topLeft, viewArea.size); + + renderer.setRuntimeSettings({ + backgroundColor: mixRgb( + settings.backgroundGradient[0], + settings.backgroundGradient[1], + (this.center.x - settings.worldLeftEdge) / + (Math.abs(settings.worldLeftEdge) + Math.abs(settings.worldRightEdge)), + ), + }); } } diff --git a/frontend/src/scripts/objects/character-view.ts b/frontend/src/scripts/objects/character-view.ts index 8cb63b6..8f7560e 100644 --- a/frontend/src/scripts/objects/character-view.ts +++ b/frontend/src/scripts/objects/character-view.ts @@ -1,19 +1,32 @@ import { vec2 } from 'gl-matrix'; -import { CharacterBase, CommandExecutors } from 'shared'; -import { RenderCommand } from '../commands/types/render'; +import { Renderer } from 'sdf-2d'; +import { CharacterBase, Circle, Id } from 'shared'; + import { BlobShape } from '../shapes/blob-shape'; +import { ViewObject } from './view-object'; -export class CharacterView extends CharacterBase { - private shape = new BlobShape(); +export class CharacterView extends CharacterBase implements ViewObject { + private shape: BlobShape; - protected commandExecutors: CommandExecutors = { - [RenderCommand.type]: (c: RenderCommand) => { - this.shape.setCircles([this.head, this.leftFoot, this.rightFoot]); - c.renderer.addDrawable(this.shape); - }, - }; + constructor( + id: Id, + colorIndex: number, + head?: Circle, + leftFoot?: Circle, + rightFoot?: Circle, + ) { + super(id, colorIndex, head, leftFoot, rightFoot); + this.shape = new BlobShape(colorIndex); + } public get position(): vec2 { - return this.head.center; + return this.head!.center; + } + + public step(deltaTimeInMilliseconds: number): void {} + + public draw(renderer: Renderer): void { + this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]); + renderer.addDrawable(this.shape); } } diff --git a/frontend/src/scripts/objects/game-object-container.ts b/frontend/src/scripts/objects/game-object-container.ts index 053295f..4fcfcbf 100644 --- a/frontend/src/scripts/objects/game-object-container.ts +++ b/frontend/src/scripts/objects/game-object-container.ts @@ -1,23 +1,22 @@ +import { Renderer } from 'sdf-2d'; import { - Command, CommandExecutors, CommandReceiver, CreateObjectsCommand, CreatePlayerCommand, DeleteObjectsCommand, - GameObject, Id, - StepCommand, UpdateObjectsCommand, } from 'shared'; import { Game } from '../game'; import { Camera } from './camera'; import { CharacterView } from './character-view'; +import { ViewObject } from './view-object'; export class GameObjectContainer extends CommandReceiver { - protected objects: Map = new Map(); - public player: CharacterView; - public camera: Camera; + protected objects: Map = new Map(); + public player!: CharacterView; + public camera!: Camera; protected commandExecutors: CommandExecutors = { [CreatePlayerCommand.type]: (c: CreatePlayerCommand) => { @@ -27,14 +26,8 @@ export class GameObjectContainer extends CommandReceiver { this.addObject(this.camera); }, - [StepCommand.type]: (_: StepCommand) => { - if (this.player) { - this.camera.center = this.player.position; - } - }, - [CreateObjectsCommand.type]: (c: CreateObjectsCommand) => - c.objects.forEach((o) => this.addObject(o)), + c.objects.forEach((o) => this.addObject(o as ViewObject)), [DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) => c.ids.forEach((id: Id) => this.objects.delete(id)), @@ -42,7 +35,7 @@ export class GameObjectContainer extends CommandReceiver { [UpdateObjectsCommand.type]: (c: UpdateObjectsCommand) => { c.objects.forEach((o) => { this.objects.delete(o.id); - this.addObject(o); + this.addObject(o as ViewObject); if (o.id === this.player.id) { this.player = o as CharacterView; } @@ -54,15 +47,19 @@ export class GameObjectContainer extends CommandReceiver { super(); } - protected defaultCommandExecutor(c: Command) { - this.objects.forEach((o) => o.sendCommand(c)); + public stepObjects(delta: number) { + if (this.player) { + this.camera.center = this.player.position; + } + + this.objects.forEach((o) => o.step(delta)); } - public sendCommandToSingleObject(id: Id, e: Command) { - this.objects.get(id)!.sendCommand(e); + public drawObjects(renderer: Renderer) { + this.objects.forEach((o) => o.draw(renderer)); } - private addObject(object: GameObject) { + private addObject(object: ViewObject) { this.objects.set(object.id, object); } } diff --git a/frontend/src/scripts/objects/lamp-view.ts b/frontend/src/scripts/objects/lamp-view.ts index a2b1045..ece2595 100644 --- a/frontend/src/scripts/objects/lamp-view.ts +++ b/frontend/src/scripts/objects/lamp-view.ts @@ -1,17 +1,24 @@ import { vec2, vec3 } from 'gl-matrix'; -import { CircleLight } from 'sdf-2d'; +import { CircleLight, Renderer } from 'sdf-2d'; import { CommandExecutors, Id, LampBase } from 'shared'; import { RenderCommand } from '../commands/types/render'; +import { ViewObject } from './view-object'; -export class LampView extends LampBase { +export class LampView extends LampBase implements ViewObject { private light: CircleLight; protected commandExecutors: CommandExecutors = { [RenderCommand.type]: (c: RenderCommand) => c.renderer.addDrawable(this.light), - } as any; + }; constructor(id: Id, center: vec2, color: vec3, lightness: number) { super(id, center, color, lightness); this.light = new CircleLight(center, color, lightness); } + + public step(deltaTimeInMilliseconds: number): void {} + + public draw(renderer: Renderer): void { + renderer.addDrawable(this.light); + } } diff --git a/frontend/src/scripts/objects/projectile-view.ts b/frontend/src/scripts/objects/projectile-view.ts new file mode 100644 index 0000000..bcb6ef6 --- /dev/null +++ b/frontend/src/scripts/objects/projectile-view.ts @@ -0,0 +1,19 @@ +import { vec2 } from 'gl-matrix'; +import { Circle, Renderer } from 'sdf-2d'; +import { Id, ProjectileBase } from 'shared'; +import { ViewObject } from './view-object'; + +export class ProjectileView extends ProjectileBase implements ViewObject { + private circle: Circle; + + constructor(id: Id, center: vec2, radius: number) { + super(id, center, radius); + this.circle = new Circle(center, radius); + } + + public step(deltaTimeInMilliseconds: number): void {} + + public draw(renderer: Renderer): void { + renderer.addDrawable(this.circle); + } +} diff --git a/frontend/src/scripts/objects/stone-view.ts b/frontend/src/scripts/objects/stone-view.ts new file mode 100644 index 0000000..7595e0b --- /dev/null +++ b/frontend/src/scripts/objects/stone-view.ts @@ -0,0 +1,25 @@ +import { vec2 } from 'gl-matrix'; +import { Drawable, Renderer } from 'sdf-2d'; +import { CommandExecutors, Id, StoneBase } from 'shared'; +import { RenderCommand } from '../commands/types/render'; +import { Polygon } from '../shapes/polygon'; +import { ViewObject } from './view-object'; + +export class StoneView extends StoneBase implements ViewObject { + private shape: Drawable; + + protected commandExecutors: CommandExecutors = { + [RenderCommand.type]: (c: RenderCommand) => c.renderer.addDrawable(this.shape), + }; + + constructor(id: Id, vertices: Array) { + super(id, vertices); + this.shape = new Polygon(vertices); + } + + public step(deltaTimeInMilliseconds: number): void {} + + public draw(renderer: Renderer): void { + renderer.addDrawable(this.shape); + } +} diff --git a/frontend/src/scripts/objects/tunnel-view.ts b/frontend/src/scripts/objects/tunnel-view.ts deleted file mode 100644 index ecd6638..0000000 --- a/frontend/src/scripts/objects/tunnel-view.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { InvertedTunnel } from 'sdf-2d'; -import { CommandExecutors, Id, TunnelBase } from 'shared'; -import { RenderCommand } from '../commands/types/render'; - -export class TunnelView extends TunnelBase { - private shape: InvertedTunnel; - - protected commandExecutors: CommandExecutors = { - [RenderCommand.type]: (c: RenderCommand) => c.renderer.addDrawable(this.shape), - } as any; - - constructor(id: Id, from: vec2, to: vec2, fromRadius: number, toRadius: number) { - super(id, from, to, fromRadius, toRadius); - this.shape = new InvertedTunnel(from, to, fromRadius, toRadius); - } -} diff --git a/frontend/src/scripts/objects/view-object.ts b/frontend/src/scripts/objects/view-object.ts new file mode 100644 index 0000000..d3ae529 --- /dev/null +++ b/frontend/src/scripts/objects/view-object.ts @@ -0,0 +1,7 @@ +import { Renderer } from 'sdf-2d'; +import { GameObject } from 'shared'; + +export interface ViewObject extends GameObject { + step(deltaTimeInMilliseconds: number): void; + draw(renderer: Renderer): void; +} diff --git a/frontend/src/scripts/shapes/blob-shape.ts b/frontend/src/scripts/shapes/blob-shape.ts index fb5a3fa..0c44f0b 100644 --- a/frontend/src/scripts/shapes/blob-shape.ts +++ b/frontend/src/scripts/shapes/blob-shape.ts @@ -11,6 +11,7 @@ export class BlobShape extends Drawable { uniform vec2 rightFootCenters[BLOB_COUNT]; uniform float headRadii[BLOB_COUNT]; uniform float footRadii[BLOB_COUNT]; + uniform float blobColors[BLOB_COUNT]; float blobSmoothMin(float a, float b) { @@ -25,7 +26,6 @@ export class BlobShape extends Drawable { float blobMinDistance(vec2 target, out float colorIndex) { float minDistance = 1000.0; - colorIndex = 3.0; for (int i = 0; i < BLOB_COUNT; i++) { float headDistance = circleDistance(headCenters[i], headRadii[i], target); @@ -61,6 +61,10 @@ export class BlobShape extends Drawable { ); minDistance = min(minDistance, res); + + if (res < 0.0) { + colorIndex = blobColors[i]; + } } return minDistance; @@ -74,17 +78,18 @@ export class BlobShape extends Drawable { rightFootCenter: 'rightFootCenters', leftFootCenter: 'leftFootCenters', headCenter: 'headCenters', + color: 'blobColors', }, uniformCountMacroName: 'BLOB_COUNT', - shaderCombinationSteps: [0, 1, 10], - empty: new BlobShape(), + shaderCombinationSteps: [], + empty: new BlobShape(0), }; - protected head: Circle; - protected leftFoot: Circle; - protected rightFoot: Circle; + protected head!: Circle; + protected leftFoot!: Circle; + protected rightFoot!: Circle; - public constructor() { + public constructor(private readonly color: number) { super(); const circle = new Circle(vec2.create(), 200); @@ -120,6 +125,7 @@ export class BlobShape extends Drawable { ), headRadius: this.head.radius * transform1d, footRadius: this.leftFoot.radius * transform1d, + color: this.color, }; } } diff --git a/frontend/src/scripts/shapes/polygon.ts b/frontend/src/scripts/shapes/polygon.ts new file mode 100644 index 0000000..5ab0052 --- /dev/null +++ b/frontend/src/scripts/shapes/polygon.ts @@ -0,0 +1,4 @@ +import { PolygonFactory } from 'sdf-2d'; +import { settings } from 'shared'; + +export const Polygon = PolygonFactory(settings.polygonEdgeCount); diff --git a/package.json b/package.json index d90168f..71fa50c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "gl-matrix": "^3.3.0", "lerna": "^3.22.1", "prettier": "^2.0.5", - "sdf-2d": "^0.4.0", + "sdf-2d": "^0.4.1", "socket.io-client": "^2.3.1", "typescript": "^4.0.3", "uuid": "^8.2.0" diff --git a/shared/src/commands/types/step.ts b/shared/src/commands/types/step.ts deleted file mode 100644 index fb0181c..0000000 --- a/shared/src/commands/types/step.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Command } from '../command'; - -export class StepCommand extends Command { - public constructor(public readonly deltaTimeInMiliseconds: number) { - super(); - } -} diff --git a/shared/src/helper/mix-rgb.ts b/shared/src/helper/mix-rgb.ts new file mode 100644 index 0000000..17195af --- /dev/null +++ b/shared/src/helper/mix-rgb.ts @@ -0,0 +1,12 @@ +import { vec3 } from 'gl-matrix'; +import { clamp01 } from './clamp'; +import { mix } from './mix'; + +export const mixRgb = (a: vec3, b: vec3, q: number): vec3 => { + const clampedQ = clamp01(q); + return vec3.fromValues( + mix(a[0], b[0], clampedQ), + mix(a[1], b[1], clampedQ), + mix(a[2], b[2], clampedQ), + ); +}; diff --git a/shared/src/helper/random.ts b/shared/src/helper/random.ts index 10e93bb..4f800bc 100644 --- a/shared/src/helper/random.ts +++ b/shared/src/helper/random.ts @@ -9,6 +9,19 @@ export abstract class Random { Random._seed = value; } + public static choose(values: Array): T | undefined { + const to = values.length; + if (to === 0) { + return undefined; + } + + return values[Math.floor(this.getRandomInRange(0, to))]; + } + + public static getRandomInRange(from: number, to: number): number { + return from + this.getRandom() * (to - from); + } + public static getRandom(): number { let t = (Random._seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); diff --git a/frontend/src/scripts/helper/rgb.ts b/shared/src/helper/rgb.ts similarity index 100% rename from frontend/src/scripts/helper/rgb.ts rename to shared/src/helper/rgb.ts diff --git a/frontend/src/scripts/helper/rgb255.ts b/shared/src/helper/rgb255.ts similarity index 100% rename from frontend/src/scripts/helper/rgb255.ts rename to shared/src/helper/rgb255.ts diff --git a/shared/src/main.ts b/shared/src/main.ts index bf9a7bb..f21f2c7 100644 --- a/shared/src/main.ts +++ b/shared/src/main.ts @@ -3,10 +3,8 @@ export * from './commands/types/create-objects'; export * from './commands/types/create-player'; export * from './commands/types/delete-objects'; export * from './commands/types/update-objects'; -export * from './commands/types/step'; export * from './commands/types/ternary-action'; export * from './commands/types/move-action'; -export * from './commands/types/set-aspect-ratio-action'; export * from './commands/types/primary-action'; export * from './commands/types/secondary-action'; export * from './commands/command-receiver'; @@ -16,6 +14,9 @@ export * from './commands/broadcast-commands'; export * from './commands/types/set-aspect-ratio-action'; export * from './helper/array'; export * from './helper/last'; +export * from './helper/rgb'; +export * from './helper/rgb255'; +export * from './helper/mix-rgb'; export * from './helper/clamp'; export * from './helper/calculate-view-area'; export * from './helper/pretty-print'; @@ -33,7 +34,8 @@ export * from './transport/serialization/serializable'; export * from './transport/serialization/override-deserialization'; export * from './objects/types/character-base'; export * from './objects/types/lamp-base'; -export * from './objects/types/tunnel-base'; +export * from './objects/types/stone-base'; +export * from './objects/types/projectile-base'; export * from './settings'; export * from './transport/transport-events'; export * from './transport/identity'; diff --git a/shared/src/objects/game-object.ts b/shared/src/objects/game-object.ts index 6192edd..22d9e6f 100644 --- a/shared/src/objects/game-object.ts +++ b/shared/src/objects/game-object.ts @@ -1,8 +1,5 @@ -import { CommandReceiver } from '../commands/command-receiver'; import { Id } from '../transport/identity'; -export abstract class GameObject extends CommandReceiver { - constructor(public readonly id: Id) { - super(); - } +export abstract class GameObject { + constructor(public readonly id: Id) {} } diff --git a/shared/src/objects/types/character-base.ts b/shared/src/objects/types/character-base.ts index a0e6f84..57c9e99 100644 --- a/shared/src/objects/types/character-base.ts +++ b/shared/src/objects/types/character-base.ts @@ -7,6 +7,7 @@ import { GameObject } from '../game-object'; export class CharacterBase extends GameObject { constructor( id: Id, + public colorIndex: number, public head?: Circle, public leftFoot?: Circle, public rightFoot?: Circle, @@ -15,7 +16,7 @@ export class CharacterBase extends GameObject { } public toArray(): Array { - const { id, head, leftFoot, rightFoot } = this as any; - return [id, head, leftFoot, rightFoot]; + const { id, colorIndex, head, leftFoot, rightFoot } = this; + return [id, colorIndex, head, leftFoot, rightFoot]; } } diff --git a/shared/src/objects/types/lamp-base.ts b/shared/src/objects/types/lamp-base.ts index cf594a2..3977fce 100644 --- a/shared/src/objects/types/lamp-base.ts +++ b/shared/src/objects/types/lamp-base.ts @@ -9,7 +9,7 @@ export class LampBase extends GameObject { } public toArray(): Array { - const { id, center, color, lightness } = this as any; + const { id, center, color, lightness } = this; return [id, center, color, lightness]; } } diff --git a/shared/src/objects/types/projectile-base.ts b/shared/src/objects/types/projectile-base.ts new file mode 100644 index 0000000..90ef4e2 --- /dev/null +++ b/shared/src/objects/types/projectile-base.ts @@ -0,0 +1,15 @@ +import { vec2 } from 'gl-matrix'; +import { Id } from '../../transport/identity'; +import { serializable } from '../../transport/serialization/serializable'; +import { GameObject } from '../game-object'; + +@serializable +export class ProjectileBase extends GameObject { + constructor(id: Id, public center: vec2, public radius: number) { + super(id); + } + + public toArray(): Array { + return [this.id, this.center, this.radius]; + } +} diff --git a/shared/src/objects/types/stone-base.ts b/shared/src/objects/types/stone-base.ts new file mode 100644 index 0000000..8736673 --- /dev/null +++ b/shared/src/objects/types/stone-base.ts @@ -0,0 +1,15 @@ +import { vec2 } from 'gl-matrix'; +import { Id } from '../../transport/identity'; +import { serializable } from '../../transport/serialization/serializable'; +import { GameObject } from '../game-object'; + +@serializable +export class StoneBase extends GameObject { + constructor(id: Id, public readonly vertices: Array) { + super(id); + } + + public toArray(): Array { + return [this.id, this.vertices]; + } +} diff --git a/shared/src/objects/types/tunnel-base.ts b/shared/src/objects/types/tunnel-base.ts deleted file mode 100644 index 58dea4d..0000000 --- a/shared/src/objects/types/tunnel-base.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Id } from '../../transport/identity'; -import { serializable } from '../../transport/serialization/serializable'; -import { GameObject } from '../game-object'; - -@serializable -export class TunnelBase extends GameObject { - constructor( - id: Id, - public readonly from: vec2, - public readonly to: vec2, - public readonly fromRadius: number, - public readonly toRadius: number, - ) { - super(id); - } - - public toArray(): Array { - const { id, from, to, fromRadius, toRadius } = this as any; - return [id, from, to, fromRadius, toRadius]; - } -} diff --git a/shared/src/settings.ts b/shared/src/settings.ts index 3c9935b..4777e84 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -1,19 +1,54 @@ import { vec2 } from 'gl-matrix'; +import { rgb255 } from './helper/rgb255'; export const settings = { lightCutoffDistance: 600, - hitDetectionCirclePointCount: 32, - hitDetectionMaxOverlap: 0.01, physicsMaxStep: 5, - gravitationalForce: vec2.fromValues(0, -200), - maxVelocityX: 500, - maxVelocityY: 750, - maxAccelerationX: 16000, - maxAccelerationY: 5500, - targetPhysicsDeltaTimeInMilliseconds: 15, + gravitationalForce: vec2.fromValues(0, -3000), + maxVelocityX: 4800, + maxVelocityY: 3650, + polygonEdgeCount: 8, + projectileSpeed: 2000, + projectileStartOffset: 150, + playerColorIndexOffset: 3, + worldLeftEdge: -5000, + worldRightEdge: 5000, + backgroundGradient: [rgb255(255, 117, 129), rgb255(136, 117, 255)], + playerColors: [ + rgb255(107, 48, 188), + rgb255(56, 254, 220), + rgb255(197, 17, 17), + rgb255(24, 24, 24), + rgb255(245, 245, 88), + rgb255(80, 239, 58), + rgb255(18, 127, 45), + rgb255(240, 125, 13), + rgb255(214, 227, 241), + rgb255(0, 161, 161), + rgb255(250, 161, 151), + rgb255(235, 84, 185), + rgb255(114, 73, 30), + rgb255(75, 75, 75), + rgb255(107, 48, 188), + rgb255(56, 254, 220), + rgb255(197, 17, 17), + rgb255(24, 24, 24), + rgb255(245, 245, 88), + rgb255(80, 239, 58), + rgb255(18, 127, 45), + rgb255(240, 125, 13), + rgb255(214, 227, 241), + rgb255(0, 161, 161), + rgb255(250, 161, 151), + rgb255(235, 84, 185), + rgb255(114, 73, 30), + rgb255(75, 75, 75), + ], + maxAccelerationX: 200000, + maxAccelerationY: 20500, + targetPhysicsDeltaTimeInMilliseconds: 20, minPhysicsSleepTime: 4, - velocityAttenuation: 0.5, - frictionMinVelocity: 400, + velocityAttenuation: 0.33, inViewAreaSize: 1920 * 1080 * 3, - defaultJumpEnergy: 0.75, + defaultJumpEnergy: 0.25, };