clean up
This commit is contained in:
parent
aa02d0ab6f
commit
b9274a42c7
35 changed files with 480 additions and 457 deletions
|
|
@ -48,9 +48,6 @@ export class GameServer extends CommandReceiver {
|
||||||
private isInEndGame = false;
|
private isInEndGame = false;
|
||||||
private timeScaling = 1;
|
private timeScaling = 1;
|
||||||
|
|
||||||
private serverName: string;
|
|
||||||
private playerLimit: number;
|
|
||||||
|
|
||||||
private initialize() {
|
private initialize() {
|
||||||
const previousPlayers = this.players;
|
const previousPlayers = this.players;
|
||||||
this.releaseJoinedSockets();
|
this.releaseJoinedSockets();
|
||||||
|
|
@ -85,9 +82,6 @@ export class GameServer extends CommandReceiver {
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.serverName = options.name;
|
|
||||||
this.playerLimit = options.playerLimit;
|
|
||||||
|
|
||||||
this.initialize();
|
this.initialize();
|
||||||
|
|
||||||
io.on('connection', (socket: Socket) => {
|
io.on('connection', (socket: Socket) => {
|
||||||
|
|
@ -274,14 +268,9 @@ export class GameServer extends CommandReceiver {
|
||||||
this.isInEndGame = true;
|
this.isInEndGame = true;
|
||||||
const endTitleLength = 6;
|
const endTitleLength = 6;
|
||||||
this.players.endGame(winningTeam);
|
this.players.endGame(winningTeam);
|
||||||
this.players.queueCommandForEachClient(
|
this.players.queueCommandForEachClient(new GameEndCommand());
|
||||||
new GameEndCommand(winningTeam, endTitleLength, true),
|
// Rebuild the world for the next round once the end card has been shown.
|
||||||
);
|
setTimeout(() => this.initialize(), endTitleLength * 1000 * 1.1);
|
||||||
setTimeout(() => this.destroy(), endTitleLength * 1000 * 1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private destroy() {
|
|
||||||
this.initialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private timeSinceLastPointUpdate = 0;
|
private timeSinceLastPointUpdate = 0;
|
||||||
|
|
@ -406,9 +395,9 @@ export class GameServer extends CommandReceiver {
|
||||||
|
|
||||||
public get serverInfo(): ServerInformation {
|
public get serverInfo(): ServerInformation {
|
||||||
return {
|
return {
|
||||||
serverName: this.serverName,
|
serverName: this.options.name,
|
||||||
playerCount: this.players.count,
|
playerCount: this.players.count,
|
||||||
playerLimit: this.playerLimit,
|
playerLimit: this.options.playerLimit,
|
||||||
gameStatePercent: this.gameProgress,
|
gameStatePercent: this.gameProgress,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,12 @@ import {
|
||||||
CharacterMovementSnapshot,
|
CharacterMovementSnapshot,
|
||||||
mix,
|
mix,
|
||||||
clamp01,
|
clamp01,
|
||||||
|
strengthToCharge,
|
||||||
stepCharacterMovement,
|
stepCharacterMovement,
|
||||||
applyLeapImpulse,
|
applyLeapImpulse,
|
||||||
decayMomentum,
|
decayMomentum,
|
||||||
CharacterMovementState,
|
tickPlanetDetachment,
|
||||||
|
characterCenter,
|
||||||
CharacterWorld,
|
CharacterWorld,
|
||||||
GroundSurface,
|
GroundSurface,
|
||||||
headRadius,
|
headRadius,
|
||||||
|
|
@ -40,6 +42,14 @@ import { StepCommand } from '../commands/step';
|
||||||
import { ReactToCollisionCommand } from '../commands/react-to-collision';
|
import { ReactToCollisionCommand } from '../commands/react-to-collision';
|
||||||
import { GeneratePointsCommand } from '../commands/generate-points';
|
import { GeneratePointsCommand } from '../commands/generate-points';
|
||||||
|
|
||||||
|
// The three body circles, as a value — used to diff a tick's movement into the
|
||||||
|
// rate of change the client interpolates on.
|
||||||
|
interface BodyPose {
|
||||||
|
head: Circle;
|
||||||
|
leftFoot: Circle;
|
||||||
|
rightFoot: Circle;
|
||||||
|
}
|
||||||
|
|
||||||
@serializesTo(CharacterBase)
|
@serializesTo(CharacterBase)
|
||||||
export class CharacterPhysical extends CharacterBase implements DynamicPhysical {
|
export class CharacterPhysical extends CharacterBase implements DynamicPhysical {
|
||||||
public readonly canCollide = true;
|
public readonly canCollide = true;
|
||||||
|
|
@ -47,13 +57,6 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
|
|
||||||
private projectileStrength = settings.playerMaxStrength;
|
private projectileStrength = settings.playerMaxStrength;
|
||||||
|
|
||||||
// Body geometry (head/foot radii, posture offsets, bound radius) is defined
|
|
||||||
// once in the shared movement module and imported here, so the authoritative
|
|
||||||
// body and the client's predicted body are bit-identical by construction
|
|
||||||
// instead of by a hand-synced "copied verbatim" duplicate. Re-exposed as a
|
|
||||||
// static only because external callers reference CharacterPhysical.boundRadius.
|
|
||||||
public static readonly boundRadius = boundRadius;
|
|
||||||
|
|
||||||
private timeSinceDying = 0;
|
private timeSinceDying = 0;
|
||||||
private isDestroyed = false;
|
private isDestroyed = false;
|
||||||
private timeSinceBorn = 0;
|
private timeSinceBorn = 0;
|
||||||
|
|
@ -66,20 +69,20 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
|
|
||||||
private killStreak = 0;
|
private killStreak = 0;
|
||||||
|
|
||||||
private direction = 0;
|
// CharacterMovementState, held directly rather than mirrored through an
|
||||||
private currentPlanet?: PlanetPhysical;
|
// adapter: the shared simulation reads and writes these in place, so the
|
||||||
private secondsSinceOnSurface = settings.planetDetachmentSeconds;
|
// character simply IS the state it is stepped as.
|
||||||
|
public direction = 0;
|
||||||
|
public currentPlanet: GroundSurface | undefined;
|
||||||
|
public secondsSinceOnSurface = settings.planetDetachmentSeconds;
|
||||||
|
public readonly bodyVelocity = vec2.create();
|
||||||
|
|
||||||
private bodyVelocity = vec2.create();
|
|
||||||
private timeSinceLastLeap = settings.leapCooldownSeconds;
|
private timeSinceLastLeap = settings.leapCooldownSeconds;
|
||||||
|
|
||||||
public head: CirclePhysical;
|
public head: CirclePhysical;
|
||||||
public leftFoot: CirclePhysical;
|
public leftFoot: CirclePhysical;
|
||||||
public rightFoot: CirclePhysical;
|
public rightFoot: CirclePhysical;
|
||||||
|
|
||||||
private movementState!: CharacterMovementState;
|
|
||||||
private movementWorld!: CharacterWorld;
|
|
||||||
|
|
||||||
private movementActions: Array<MoveActionCommand> = [];
|
private movementActions: Array<MoveActionCommand> = [];
|
||||||
private lastMovementAction: MoveActionCommand = new MoveActionCommand(vec2.create());
|
private lastMovementAction: MoveActionCommand = new MoveActionCommand(vec2.create());
|
||||||
|
|
||||||
|
|
@ -122,47 +125,15 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
container.addObject(this.head);
|
container.addObject(this.head);
|
||||||
container.addObject(this.leftFoot);
|
container.addObject(this.leftFoot);
|
||||||
container.addObject(this.rightFoot);
|
container.addObject(this.rightFoot);
|
||||||
|
|
||||||
this.initMovementBridge();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private initMovementBridge() {
|
// The container, presented as the collision/gravity world the shared movement
|
||||||
// The movementState object-literal getters/setters below can't use `this`
|
// queries.
|
||||||
// (it would bind to the literal), so alias the character instance.
|
private readonly movementWorld: CharacterWorld = {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// Same set and order forceAtPosition used: planets in the force field, in
|
||||||
const self = this;
|
// container-traversal order (so the f64 gravity sum is unchanged).
|
||||||
this.movementState = {
|
|
||||||
head: this.head,
|
|
||||||
leftFoot: this.leftFoot,
|
|
||||||
rightFoot: this.rightFoot,
|
|
||||||
get direction() {
|
|
||||||
return self.direction;
|
|
||||||
},
|
|
||||||
set direction(value: number) {
|
|
||||||
self.direction = value;
|
|
||||||
},
|
|
||||||
get currentPlanet() {
|
|
||||||
return self.currentPlanet;
|
|
||||||
},
|
|
||||||
set currentPlanet(value: GroundSurface | undefined) {
|
|
||||||
// On the server every ground is a PlanetPhysical (the world only ever
|
|
||||||
// hands back planets), so this narrowing is safe.
|
|
||||||
self.currentPlanet = value as PlanetPhysical | undefined;
|
|
||||||
},
|
|
||||||
get secondsSinceOnSurface() {
|
|
||||||
return self.secondsSinceOnSurface;
|
|
||||||
},
|
|
||||||
set secondsSinceOnSurface(value: number) {
|
|
||||||
self.secondsSinceOnSurface = value;
|
|
||||||
},
|
|
||||||
bodyVelocity: this.bodyVelocity,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.movementWorld = {
|
|
||||||
// Same set and order forceAtPosition used: planets in the force field,
|
|
||||||
// in container-traversal order (so the f64 gravity sum is unchanged).
|
|
||||||
groundsNear: (center, radius) =>
|
groundsNear: (center, radius) =>
|
||||||
self.container
|
this.container
|
||||||
.findIntersecting(getBoundingBoxOfCircle(new Circle(center, radius)))
|
.findIntersecting(getBoundingBoxOfCircle(new Circle(center, radius)))
|
||||||
.filter((o): o is PlanetPhysical => o instanceof PlanetPhysical),
|
.filter((o): o is PlanetPhysical => o instanceof PlanetPhysical),
|
||||||
stepBody: (body, deltaTimeInSeconds) => {
|
stepBody: (body, deltaTimeInSeconds) => {
|
||||||
|
|
@ -170,7 +141,6 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
return hitObject instanceof PlanetPhysical ? hitObject : undefined;
|
return hitObject instanceof PlanetPhysical ? hitObject : undefined;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
private hasFiredSinceSpawn = false;
|
private hasFiredSinceSpawn = false;
|
||||||
private get isSpawnProtected(): boolean {
|
private get isSpawnProtected(): boolean {
|
||||||
|
|
@ -206,8 +176,10 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
this.movementActions.push(c);
|
this.movementActions.push(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The world only ever hands back planets, so narrowing the shared
|
||||||
|
// GroundSurface is safe here.
|
||||||
public get groundPlanet(): PlanetPhysical | undefined {
|
public get groundPlanet(): PlanetPhysical | undefined {
|
||||||
return this.currentPlanet;
|
return this.currentPlanet as PlanetPhysical | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The continuous movement state streamed to the owning client, so its
|
// The continuous movement state streamed to the owning client, so its
|
||||||
|
|
@ -219,7 +191,7 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
vec2.clone(this.bodyVelocity),
|
vec2.clone(this.bodyVelocity),
|
||||||
vec2.clone(this.leftFoot.lastNormal),
|
vec2.clone(this.leftFoot.lastNormal),
|
||||||
vec2.clone(this.rightFoot.lastNormal),
|
vec2.clone(this.rightFoot.lastNormal),
|
||||||
this.currentPlanet?.id ?? null,
|
this.groundPlanet?.id ?? null,
|
||||||
this.secondsSinceOnSurface,
|
this.secondsSinceOnSurface,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -311,10 +283,7 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
const strength = Math.min(desiredStrength, this.projectileStrength);
|
const strength = Math.min(desiredStrength, this.projectileStrength);
|
||||||
this.projectileStrength -= strength;
|
this.projectileStrength -= strength;
|
||||||
|
|
||||||
const c = clamp01(
|
const c = strengthToCharge(strength);
|
||||||
(strength - settings.chargeShotStrengthMin) /
|
|
||||||
(settings.chargeShotStrengthMax - settings.chargeShotStrengthMin),
|
|
||||||
);
|
|
||||||
|
|
||||||
const radius = mix(settings.chargeShotRadiusMin, settings.chargeShotRadiusMax, c);
|
const radius = mix(settings.chargeShotRadiusMin, settings.chargeShotRadiusMax, c);
|
||||||
const speed = mix(settings.chargeShotSpeedMin, settings.chargeShotSpeedMax, c);
|
const speed = mix(settings.chargeShotSpeedMin, settings.chargeShotSpeedMax, c);
|
||||||
|
|
@ -367,12 +336,12 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
|
|
||||||
// Same impulse the client predicts with (shared), so a leap launches
|
// Same impulse the client predicts with (shared), so a leap launches
|
||||||
// identically on both sides.
|
// identically on both sides.
|
||||||
applyLeapImpulse(this.movementState, this.lastMovementAction.direction);
|
applyLeapImpulse(this, this.lastMovementAction.direction);
|
||||||
this.remoteCall('onLeap');
|
this.remoteCall('onLeap');
|
||||||
}
|
}
|
||||||
|
|
||||||
public get boundingBox(): BoundingBoxBase {
|
public get boundingBox(): BoundingBoxBase {
|
||||||
return getBoundingBoxOfCircle(new Circle(this.center, CharacterPhysical.boundRadius));
|
return getBoundingBoxOfCircle(new Circle(this.center, boundRadius));
|
||||||
}
|
}
|
||||||
|
|
||||||
public get gameObject(): this {
|
public get gameObject(): this {
|
||||||
|
|
@ -380,9 +349,7 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
}
|
}
|
||||||
|
|
||||||
public get center(): vec2 {
|
public get center(): vec2 {
|
||||||
const bodyCenter = vec2.add(vec2.create(), this.head.center, this.leftFoot.center);
|
return characterCenter(this.head, this.leftFoot, this.rightFoot);
|
||||||
vec2.add(bodyCenter, bodyCenter, this.rightFoot.center);
|
|
||||||
return vec2.scale(bodyCenter, bodyCenter, 1 / 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public distance(target: vec2): number {
|
public distance(target: vec2): number {
|
||||||
|
|
@ -434,53 +401,42 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setPropertyUpdates(
|
// Rate of change of one body part over the tick, which the client's
|
||||||
oldHead: Circle,
|
// interpolator coasts on when a snapshot is late. `previous` is consumed as
|
||||||
oldLeftFoot: Circle,
|
// scratch so this allocates nothing beyond the Circle it returns.
|
||||||
oldRightFoot: Circle,
|
private static rateOfChange(previous: Circle, current: Circle, deltaTime: number) {
|
||||||
deltaTime: number,
|
return new Circle(
|
||||||
) {
|
|
||||||
this.headVelocity = new Circle(
|
|
||||||
vec2.scale(
|
vec2.scale(
|
||||||
oldHead.center,
|
previous.center,
|
||||||
vec2.subtract(oldHead.center, this.head.center, oldHead.center),
|
vec2.subtract(previous.center, current.center, previous.center),
|
||||||
1 / deltaTime,
|
1 / deltaTime,
|
||||||
),
|
),
|
||||||
(this.head.radius - oldHead.radius) / deltaTime,
|
(current.radius - previous.radius) / deltaTime,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.leftFootVelocity = new Circle(
|
private setPropertyUpdates(previous: BodyPose, deltaTime: number) {
|
||||||
vec2.scale(
|
const rate = CharacterPhysical.rateOfChange;
|
||||||
oldLeftFoot.center,
|
this.headVelocity = rate(previous.head, this.head, deltaTime);
|
||||||
vec2.subtract(oldLeftFoot.center, this.leftFoot.center, oldLeftFoot.center),
|
this.leftFootVelocity = rate(previous.leftFoot, this.leftFoot, deltaTime);
|
||||||
1 / deltaTime,
|
this.rightFootVelocity = rate(previous.rightFoot, this.rightFoot, deltaTime);
|
||||||
),
|
}
|
||||||
(this.leftFoot.radius - oldLeftFoot.radius) / deltaTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.rightFootVelocity = new Circle(
|
private get pose(): BodyPose {
|
||||||
vec2.scale(
|
const snapshot = (circle: Circle) =>
|
||||||
oldRightFoot.center,
|
new Circle(vec2.clone(circle.center), circle.radius);
|
||||||
vec2.subtract(oldRightFoot.center, this.rightFoot.center, oldRightFoot.center),
|
return {
|
||||||
1 / deltaTime,
|
head: snapshot(this.head),
|
||||||
),
|
leftFoot: snapshot(this.leftFoot),
|
||||||
(this.rightFoot.radius - oldRightFoot.radius) / deltaTime,
|
rightFoot: snapshot(this.rightFoot),
|
||||||
);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private step({ deltaTimeInSeconds, game }: StepCommand) {
|
private step({ deltaTimeInSeconds, game }: StepCommand) {
|
||||||
this.getPoints(game);
|
this.getPoints(game);
|
||||||
this.timeAlive += deltaTimeInSeconds;
|
this.timeAlive += deltaTimeInSeconds;
|
||||||
this.timeSinceLastLeap += deltaTimeInSeconds;
|
this.timeSinceLastLeap += deltaTimeInSeconds;
|
||||||
const oldHead = new Circle(vec2.clone(this.head.center), this.head.radius);
|
const previousPose = this.pose;
|
||||||
const oldLeftFoot = new Circle(
|
|
||||||
vec2.clone(this.leftFoot.center),
|
|
||||||
this.leftFoot.radius,
|
|
||||||
);
|
|
||||||
const oldRightFoot = new Circle(
|
|
||||||
vec2.clone(this.rightFoot.center),
|
|
||||||
this.rightFoot.radius,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.isDestroyed) {
|
if (this.isDestroyed) {
|
||||||
if ((this.timeSinceDying += deltaTimeInSeconds) > settings.spawnDespawnTime) {
|
if ((this.timeSinceDying += deltaTimeInSeconds) > settings.spawnDespawnTime) {
|
||||||
|
|
@ -489,7 +445,7 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
this.freeFallCorpse(deltaTimeInSeconds);
|
this.freeFallCorpse(deltaTimeInSeconds);
|
||||||
this.animateScaling(1 - this.timeSinceDying / settings.spawnDespawnTime);
|
this.animateScaling(1 - this.timeSinceDying / settings.spawnDespawnTime);
|
||||||
}
|
}
|
||||||
this.setPropertyUpdates(oldHead, oldLeftFoot, oldRightFoot, deltaTimeInSeconds);
|
this.setPropertyUpdates(previousPose, deltaTimeInSeconds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,16 +456,11 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
} else {
|
} else {
|
||||||
this.animateScaling(this.timeSinceBorn / settings.spawnDespawnTime);
|
this.animateScaling(this.timeSinceBorn / settings.spawnDespawnTime);
|
||||||
}
|
}
|
||||||
this.setPropertyUpdates(oldHead, oldLeftFoot, oldRightFoot, deltaTimeInSeconds);
|
this.setPropertyUpdates(previousPose, deltaTimeInSeconds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
tickPlanetDetachment(this, deltaTimeInSeconds);
|
||||||
(this.secondsSinceOnSurface += deltaTimeInSeconds) >
|
|
||||||
settings.planetDetachmentSeconds
|
|
||||||
) {
|
|
||||||
this.currentPlanet = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timeSinceLastShot += deltaTimeInSeconds;
|
this.timeSinceLastShot += deltaTimeInSeconds;
|
||||||
|
|
||||||
|
|
@ -523,7 +474,7 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
|
|
||||||
// The planet tallies who is standing on it and resolves capture itself, so
|
// The planet tallies who is standing on it and resolves capture itself, so
|
||||||
// a contested rock can freeze instead of two squads silently cancelling.
|
// a contested rock can freeze instead of two squads silently cancelling.
|
||||||
this.currentPlanet?.registerPresence(this);
|
this.groundPlanet?.registerPresence(this);
|
||||||
|
|
||||||
// The whole walking model — gravity gather, movement force, on/off-planet
|
// The whole walking model — gravity gather, movement force, on/off-planet
|
||||||
// branch, posture springs, body-momentum, and stepping the three parts —
|
// branch, posture springs, body-momentum, and stepping the three parts —
|
||||||
|
|
@ -531,23 +482,15 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
||||||
// drift. Server-only concerns (scoring, health, shooting, spawn/death,
|
// drift. Server-only concerns (scoring, health, shooting, spawn/death,
|
||||||
// ownership) stay here around it.
|
// ownership) stay here around it.
|
||||||
const direction = this.averageAndResetMovementActions();
|
const direction = this.averageAndResetMovementActions();
|
||||||
stepCharacterMovement(
|
stepCharacterMovement(this, this.movementWorld, direction, deltaTimeInSeconds);
|
||||||
this.movementState,
|
|
||||||
this.movementWorld,
|
|
||||||
direction,
|
|
||||||
deltaTimeInSeconds,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setPropertyUpdates(oldHead, oldLeftFoot, oldRightFoot, deltaTimeInSeconds);
|
this.setPropertyUpdates(previousPose, deltaTimeInSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private freeFallCorpse(deltaTime: number) {
|
private freeFallCorpse(deltaTime: number) {
|
||||||
const intersecting = this.container.findIntersecting(
|
const intersecting = this.container.findIntersecting(
|
||||||
getBoundingBoxOfCircle(
|
getBoundingBoxOfCircle(
|
||||||
new Circle(
|
new Circle(this.center, boundRadius + settings.maxGravityDistance),
|
||||||
this.center,
|
|
||||||
CharacterPhysical.boundRadius + settings.maxGravityDistance,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
let grounded = false;
|
let grounded = false;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
import {
|
import {
|
||||||
|
applyForce,
|
||||||
Circle,
|
Circle,
|
||||||
CommandExecutors,
|
CommandExecutors,
|
||||||
CommandReceiver,
|
CommandReceiver,
|
||||||
|
|
@ -12,6 +13,7 @@ import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
||||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
||||||
import { Physical } from '../physics/physicals/physical';
|
import { Physical } from '../physics/physicals/physical';
|
||||||
|
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
||||||
import { ReactToCollisionCommand } from '../commands/react-to-collision';
|
import { ReactToCollisionCommand } from '../commands/react-to-collision';
|
||||||
|
|
||||||
@serializesTo(Circle)
|
@serializesTo(Circle)
|
||||||
|
|
@ -84,11 +86,7 @@ export class CirclePhysical extends CommandReceiver implements Circle, DynamicPh
|
||||||
}
|
}
|
||||||
|
|
||||||
public applyForce(force: vec2, timeInSeconds: number) {
|
public applyForce(force: vec2, timeInSeconds: number) {
|
||||||
vec2.add(
|
applyForce(this, force, timeInSeconds);
|
||||||
this.velocity,
|
|
||||||
this.velocity,
|
|
||||||
vec2.scale(vec2.create(), force, timeInSeconds),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position-resolution for one tick. Delegates to the shared
|
// Position-resolution for one tick. Delegates to the shared
|
||||||
|
|
@ -133,10 +131,11 @@ export class CirclePhysical extends CommandReceiver implements Circle, DynamicPh
|
||||||
const sweep = vec2.length(
|
const sweep = vec2.length(
|
||||||
vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds),
|
vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds),
|
||||||
);
|
);
|
||||||
this.radius += sweep;
|
// Grow the query, not the object: inflating and deflating this.radius ran
|
||||||
const intersecting = this.container.findIntersecting(this.boundingBox);
|
// the setter (and so re-registered the body's extent) twice mid-tick.
|
||||||
this.radius -= sweep;
|
return this.container.findIntersecting(
|
||||||
return intersecting;
|
getBoundingBoxOfCircle(new Circle(this.center, this.radius + sweep)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
public toArray(): Array<any> {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { vec2, vec3 } from 'gl-matrix';
|
import { vec2, vec3 } from 'gl-matrix';
|
||||||
import { LampBase, settings, id, serializesTo } from 'shared';
|
import { Circle, 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 { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
||||||
import { StaticPhysical } from '../physics/physicals/static-physical';
|
import { StaticPhysical } from '../physics/physicals/static-physical';
|
||||||
|
|
||||||
@serializesTo(LampBase)
|
@serializesTo(LampBase)
|
||||||
|
|
@ -17,11 +18,8 @@ export class LampPhysical extends LampBase implements StaticPhysical {
|
||||||
|
|
||||||
public get boundingBox(): ImmutableBoundingBox {
|
public get boundingBox(): ImmutableBoundingBox {
|
||||||
if (!this._boundingBox) {
|
if (!this._boundingBox) {
|
||||||
this._boundingBox = new ImmutableBoundingBox(
|
this._boundingBox = getBoundingBoxOfCircle(
|
||||||
this.center.x - settings.lightCutoffDistance,
|
new Circle(this.center, settings.lightCutoffDistance),
|
||||||
this.center.x + settings.lightCutoffDistance,
|
|
||||||
this.center.y - settings.lightCutoffDistance,
|
|
||||||
this.center.y + settings.lightCutoffDistance,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
clamp,
|
Circle,
|
||||||
clamp01,
|
clamp01,
|
||||||
id,
|
id,
|
||||||
mix,
|
mix,
|
||||||
|
|
@ -12,6 +12,8 @@ import {
|
||||||
CharacterTeam,
|
CharacterTeam,
|
||||||
PropertyUpdatesForObject,
|
PropertyUpdatesForObject,
|
||||||
UpdatePropertyCommand,
|
UpdatePropertyCommand,
|
||||||
|
planetDistance,
|
||||||
|
planetGravity,
|
||||||
CommandExecutors,
|
CommandExecutors,
|
||||||
CommandReceiver,
|
CommandReceiver,
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
|
|
@ -20,6 +22,7 @@ import { AnnounceCommand } from '../commands/announce';
|
||||||
import { StepCommand } from '../commands/step';
|
import { StepCommand } from '../commands/step';
|
||||||
|
|
||||||
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
||||||
|
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
||||||
import { StaticPhysical } from '../physics/physicals/static-physical';
|
import { StaticPhysical } from '../physics/physicals/static-physical';
|
||||||
import { LampPhysical } from './lamp-physical';
|
import { LampPhysical } from './lamp-physical';
|
||||||
import type { CharacterPhysical } from './character-physical';
|
import type { CharacterPhysical } from './character-physical';
|
||||||
|
|
@ -85,59 +88,26 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
||||||
}
|
}
|
||||||
|
|
||||||
public distance(target: vec2): number {
|
public distance(target: vec2): number {
|
||||||
// Evaluate the SDF in the planet's own rotating frame so this collision
|
// The one shared planet outline — the same function the client predictor
|
||||||
// outline turns in lockstep with the rendered planet (see planet-shape.ts).
|
// collides against, which is what lets prediction reconcile. Only the
|
||||||
const local = this.toLocalFrame(target);
|
// cos/sin memo lives here, because distance() is called many times per tick
|
||||||
|
// by the raymarcher and the angle rarely changes between those calls.
|
||||||
const startEnd = this.vertices[0];
|
this.syncRotationTrigonometry();
|
||||||
let vb = startEnd;
|
return planetDistance(
|
||||||
|
target,
|
||||||
let d = vec2.dist(local, vb);
|
this.vertices,
|
||||||
let sign = 1;
|
this.center,
|
||||||
|
this.cosRotation,
|
||||||
for (let i = 1; i <= this.vertices.length; i++) {
|
this.sinRotation,
|
||||||
const va = vb;
|
|
||||||
vb = i === this.vertices.length ? startEnd : this.vertices[i];
|
|
||||||
const targetFromDelta = vec2.subtract(vec2.create(), local, 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 (
|
|
||||||
(local.y >= va.y && local.y < vb.y && ds.y > 0) ||
|
|
||||||
(local.y < va.y && local.y >= vb.y && ds.y <= 0)
|
|
||||||
) {
|
|
||||||
sign *= -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
d = Math.min(d, ds.x);
|
private syncRotationTrigonometry() {
|
||||||
}
|
|
||||||
|
|
||||||
return sign * d;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rotate a world point by -rotation about the centre, matching the shader's
|
|
||||||
// `localTarget = center + R(rotation) * (target - center)` transform exactly.
|
|
||||||
private toLocalFrame(target: vec2): vec2 {
|
|
||||||
if (this.rotation !== this.cachedRotation) {
|
if (this.rotation !== this.cachedRotation) {
|
||||||
this.cachedRotation = this.rotation;
|
this.cachedRotation = this.rotation;
|
||||||
this.cosRotation = Math.cos(this.rotation);
|
this.cosRotation = Math.cos(this.rotation);
|
||||||
this.sinRotation = Math.sin(this.rotation);
|
this.sinRotation = Math.sin(this.rotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dx = target.x - this.center.x;
|
|
||||||
const dy = target.y - this.center.y;
|
|
||||||
|
|
||||||
return vec2.fromValues(
|
|
||||||
this.center.x + this.cosRotation * dx - this.sinRotation * dy,
|
|
||||||
this.center.y + this.sinRotation * dx + this.cosRotation * dy,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signed angular velocity in rad/s, exposed so a character standing on the
|
// Signed angular velocity in rad/s, exposed so a character standing on the
|
||||||
|
|
@ -146,14 +116,6 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
||||||
return this.rotationSpeed;
|
return this.rotationSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get team(): CharacterTeam {
|
|
||||||
return Math.abs(this.ownership - 0.5) < settings.planetControlThreshold
|
|
||||||
? CharacterTeam.neutral
|
|
||||||
: this.ownership < 0.5
|
|
||||||
? CharacterTeam.blue
|
|
||||||
: CharacterTeam.red;
|
|
||||||
}
|
|
||||||
|
|
||||||
private timeSinceLastPointGeneration = 0;
|
private timeSinceLastPointGeneration = 0;
|
||||||
private getPoints(game: CommandReceiver) {
|
private getPoints(game: CommandReceiver) {
|
||||||
if (this.timeSinceLastPointGeneration > settings.planetPointGenerationInterval) {
|
if (this.timeSinceLastPointGeneration > settings.planetPointGenerationInterval) {
|
||||||
|
|
@ -317,32 +279,22 @@ export class PlanetPhysical extends PlanetBase implements StaticPhysical {
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
this._boundingBox = new ImmutableBoundingBox(
|
this._boundingBox = getBoundingBoxOfCircle(
|
||||||
this.center.x - maxVertexDistance,
|
new Circle(this.center, maxVertexDistance),
|
||||||
this.center.x + maxVertexDistance,
|
|
||||||
this.center.y - maxVertexDistance,
|
|
||||||
this.center.y + maxVertexDistance,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._boundingBox;
|
return this._boundingBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getForce(position: vec2): vec2 {
|
// GroundSurface gravity, shared with the client predictor.
|
||||||
const diff = vec2.subtract(vec2.create(), this.center, position);
|
public gravityAt(position: vec2): vec2 {
|
||||||
const dist = Math.max(settings.minGravityDistance, vec2.length(diff) - this.radius);
|
return planetGravity(this.center, this.radius, position);
|
||||||
vec2.normalize(diff, diff);
|
|
||||||
const scale = clamp(
|
|
||||||
settings.maxGravityQ * ((settings.maxGravityDistance / dist) ** 1.5 - 1),
|
|
||||||
0,
|
|
||||||
settings.maxGravityStrength,
|
|
||||||
);
|
|
||||||
return vec2.scale(diff, diff, scale);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroundSurface alias the shared movement simulation calls for gravity.
|
// forceAtPosition's name for the same thing.
|
||||||
public gravityAt(position: vec2): vec2 {
|
public getForce(position: vec2): vec2 {
|
||||||
return this.getForce(position);
|
return this.gravityAt(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get gameObject(): this {
|
public get gameObject(): this {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
Circle,
|
Circle,
|
||||||
marchCircle,
|
marchCircle,
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
||||||
import { CirclePhysical } from './circle-physical';
|
import { CirclePhysical } from './circle-physical';
|
||||||
import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
||||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
|
|
@ -28,7 +28,6 @@ export class ProjectilePhysical extends ProjectileBase implements DynamicPhysica
|
||||||
|
|
||||||
private isDestroyed = false;
|
private isDestroyed = false;
|
||||||
private bounceCount = 0;
|
private bounceCount = 0;
|
||||||
private _boundingBox?: ImmutableBoundingBox;
|
|
||||||
|
|
||||||
public object: CirclePhysical;
|
public object: CirclePhysical;
|
||||||
|
|
||||||
|
|
@ -107,12 +106,11 @@ export class ProjectilePhysical extends ProjectileBase implements DynamicPhysica
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get boundingBox(): ImmutableBoundingBox {
|
// The circle keeps its own box in sync as it moves, so this is a live view of
|
||||||
if (!this._boundingBox) {
|
// it, not a snapshot — caching it saved nothing and mistyped a mutable box as
|
||||||
this._boundingBox = (this.object as CirclePhysical).boundingBox;
|
// immutable.
|
||||||
}
|
public get boundingBox(): BoundingBoxBase {
|
||||||
|
return this.object.boundingBox;
|
||||||
return this._boundingBox;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get gameObject(): this {
|
public get gameObject(): this {
|
||||||
|
|
@ -163,22 +161,18 @@ export class ProjectilePhysical extends ProjectileBase implements DynamicPhysica
|
||||||
// This single broadphase is reused for the step below: the gravity radius
|
// This single broadphase is reused for the step below: the gravity radius
|
||||||
// (maxGravityDistance) dwarfs one tick's travel, so the set is a superset of
|
// (maxGravityDistance) dwarfs one tick's travel, so the set is a superset of
|
||||||
// the swept-collision box and the step needn't query the container again.
|
// the swept-collision box and the step needn't query the container again.
|
||||||
const intersecting = settings.projectileGravityEnabled
|
const intersecting = this.container.findIntersecting(
|
||||||
? this.container.findIntersecting(
|
|
||||||
getBoundingBoxOfCircle(
|
getBoundingBoxOfCircle(
|
||||||
new Circle(this.center, this.object.radius + settings.maxGravityDistance),
|
new Circle(this.center, this.object.radius + settings.maxGravityDistance),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (intersecting) {
|
|
||||||
vec2.scaleAndAdd(
|
vec2.scaleAndAdd(
|
||||||
this.velocity,
|
this.velocity,
|
||||||
this.velocity,
|
this.velocity,
|
||||||
forceAtPosition(this.center, intersecting),
|
forceAtPosition(this.center, intersecting),
|
||||||
settings.projectileGravityScale * deltaTimeInSeconds,
|
settings.projectileGravityScale * deltaTimeInSeconds,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
vec2.copy(this.object.velocity, this.velocity);
|
vec2.copy(this.object.velocity, this.velocity);
|
||||||
const { velocity } = this.object.stepManually(deltaTimeInSeconds, intersecting);
|
const { velocity } = this.object.stepManually(deltaTimeInSeconds, intersecting);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
|
|
||||||
// axis-aligned
|
// axis-aligned
|
||||||
export abstract class BoundingBoxBase {
|
export abstract class BoundingBoxBase {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -43,14 +41,6 @@ export abstract class BoundingBoxBase {
|
||||||
return this._yMax;
|
return this._yMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get topLeft(): vec2 {
|
|
||||||
return vec2.fromValues(this._xMin, this._yMax);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get size(): vec2 {
|
|
||||||
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
|
|
||||||
}
|
|
||||||
|
|
||||||
public intersects(other: BoundingBoxBase): boolean {
|
public intersects(other: BoundingBoxBase): boolean {
|
||||||
return (
|
return (
|
||||||
this._xMin < other._xMax &&
|
this._xMin < other._xMax &&
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
import { BoundingBoxBase } from './bounding-box-base';
|
import { BoundingBoxBase } from './bounding-box-base';
|
||||||
import { ImmutableBoundingBox } from './immutable-bounding-box';
|
|
||||||
|
|
||||||
export class BoundingBox extends BoundingBoxBase {
|
export class BoundingBox extends BoundingBoxBase {
|
||||||
public get xMin(): number {
|
public get xMin(): number {
|
||||||
|
|
@ -52,8 +51,4 @@ export class BoundingBox extends BoundingBoxBase {
|
||||||
public get size(): vec2 {
|
public get size(): vec2 {
|
||||||
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
|
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public cloneAsImmutable(): ImmutableBoundingBox {
|
|
||||||
return new ImmutableBoundingBox(this.xMin, this.xMax, this.yMin, this.yMax);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Circle } from 'shared';
|
import { Circle } from 'shared';
|
||||||
import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base';
|
|
||||||
import { ImmutableBoundingBox } from '../bounding-boxes/immutable-bounding-box';
|
import { ImmutableBoundingBox } from '../bounding-boxes/immutable-bounding-box';
|
||||||
|
|
||||||
export const getBoundingBoxOfCircle = (circle: Circle): BoundingBoxBase =>
|
export const getBoundingBoxOfCircle = (circle: Circle): ImmutableBoundingBox =>
|
||||||
new ImmutableBoundingBox(
|
new ImmutableBoundingBox(
|
||||||
circle.center.x - circle.radius,
|
circle.center.x - circle.radius,
|
||||||
circle.center.x + circle.radius,
|
circle.center.x + circle.radius,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
MoveActionCommand,
|
MoveActionCommand,
|
||||||
CharacterTeam,
|
CharacterTeam,
|
||||||
Id,
|
Id,
|
||||||
|
rotate90Deg,
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
import { PlayerContainer } from './player-container';
|
import { PlayerContainer } from './player-container';
|
||||||
|
|
@ -100,23 +101,9 @@ export class NPC extends PlayerBase {
|
||||||
this.step(0);
|
this.step(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private timeUntilRespawn = 0;
|
|
||||||
public step(deltaTimeInSeconds: number) {
|
public step(deltaTimeInSeconds: number) {
|
||||||
if (this.character) {
|
const character = this.stepLifecycle(deltaTimeInSeconds);
|
||||||
this.center = this.character.center;
|
if (!character) {
|
||||||
|
|
||||||
if (!this.character.isAlive) {
|
|
||||||
this.sumDeaths++;
|
|
||||||
this.sumKills = this.character.killCount;
|
|
||||||
this.character = null;
|
|
||||||
this.timeUntilRespawn = settings.playerDiedTimeout;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
|
|
||||||
this.createCharacter();
|
|
||||||
this.center = this.character!.center;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,16 +130,16 @@ export class NPC extends PlayerBase {
|
||||||
this.dodgeCommitRemaining -= deltaTimeInSeconds;
|
this.dodgeCommitRemaining -= deltaTimeInSeconds;
|
||||||
this.dodgeCooldownRemaining -= deltaTimeInSeconds;
|
this.dodgeCooldownRemaining -= deltaTimeInSeconds;
|
||||||
const movement = this.decideMovement(this.nearObjects);
|
const movement = this.decideMovement(this.nearObjects);
|
||||||
this.character.handleMovementAction(new MoveActionCommand(movement));
|
character.handleMovementAction(new MoveActionCommand(movement));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.isComingBack &&
|
!this.isComingBack &&
|
||||||
this.character.groundPlanet &&
|
character.groundPlanet &&
|
||||||
vec2.length(movement) > 0 &&
|
vec2.length(movement) > 0 &&
|
||||||
Random.getRandom() <
|
Random.getRandom() <
|
||||||
npcTuning.leapChancePerSecond * this.aggression * deltaTimeInSeconds
|
npcTuning.leapChancePerSecond * this.aggression * deltaTimeInSeconds
|
||||||
) {
|
) {
|
||||||
this.character.leap();
|
character.leap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -178,18 +165,15 @@ export class NPC extends PlayerBase {
|
||||||
const nearObjects = this.observe(npcTuning.planScanRadius);
|
const nearObjects = this.observe(npcTuning.planScanRadius);
|
||||||
const enemies = this.enemiesByDistance(nearObjects);
|
const enemies = this.enemiesByDistance(nearObjects);
|
||||||
|
|
||||||
if (enemies.length > 0) {
|
|
||||||
const nearest = enemies[0];
|
const nearest = enemies[0];
|
||||||
|
if (nearest) {
|
||||||
const fleeRange =
|
const fleeRange =
|
||||||
npcTuning.fleeBaseRange * (npcTuning.fleeAggressionFalloff - this.aggression);
|
npcTuning.fleeBaseRange * (npcTuning.fleeAggressionFalloff - this.aggression);
|
||||||
if (nearest.distance < fleeRange) {
|
if (nearest.distance < fleeRange) {
|
||||||
vec2.subtract(this.direction, this.center, nearest.character.center);
|
vec2.subtract(this.direction, this.center, nearest.character.center);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (enemies.length > 0) {
|
|
||||||
const nearest = enemies[0];
|
|
||||||
const chaseRange =
|
const chaseRange =
|
||||||
npcTuning.chaseBaseRange + npcTuning.chaseAggressionRange * this.aggression;
|
npcTuning.chaseBaseRange + npcTuning.chaseAggressionRange * this.aggression;
|
||||||
if (nearest.distance < chaseRange) {
|
if (nearest.distance < chaseRange) {
|
||||||
|
|
@ -275,7 +259,7 @@ export class NPC extends PlayerBase {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const perpendicular = vec2.fromValues(-threat.direction.y, threat.direction.x);
|
const perpendicular = rotate90Deg(threat.direction);
|
||||||
const toMe = vec2.subtract(vec2.create(), this.center, threat.center);
|
const toMe = vec2.subtract(vec2.create(), this.center, threat.center);
|
||||||
if (vec2.dot(perpendicular, toMe) < 0) {
|
if (vec2.dot(perpendicular, toMe) < 0) {
|
||||||
vec2.negate(perpendicular, perpendicular);
|
vec2.negate(perpendicular, perpendicular);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
Random,
|
Random,
|
||||||
settings,
|
settings,
|
||||||
sanitizeName,
|
sanitizeName,
|
||||||
|
boundRadius,
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
||||||
|
|
@ -23,6 +24,7 @@ export abstract class PlayerBase extends CommandReceiver {
|
||||||
|
|
||||||
protected sumKills = 0;
|
protected sumKills = 0;
|
||||||
protected sumDeaths = 0;
|
protected sumDeaths = 0;
|
||||||
|
protected timeUntilRespawn = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly playerInfo: PlayerInformation,
|
protected readonly playerInfo: PlayerInformation,
|
||||||
|
|
@ -53,6 +55,44 @@ export abstract class PlayerBase extends CommandReceiver {
|
||||||
|
|
||||||
public abstract step(deltaTimeInSeconds: number): void;
|
public abstract step(deltaTimeInSeconds: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The death/respawn lifecycle both a connected player and an NPC go through:
|
||||||
|
* bank the score off a body that just died, hold the corpse for the respawn
|
||||||
|
* timeout, then build a new one. Returns whether there is a living character
|
||||||
|
* to act with this tick — returned as the character itself, so a subclass can
|
||||||
|
* `const character = this.stepLifecycle(dt); if (!character) return;` and go
|
||||||
|
* on to use it without re-narrowing.
|
||||||
|
*/
|
||||||
|
protected stepLifecycle(deltaTimeInSeconds: number): CharacterPhysical | undefined {
|
||||||
|
if (this.character) {
|
||||||
|
this.center = this.character.center;
|
||||||
|
|
||||||
|
if (this.character.isAlive) {
|
||||||
|
return this.character;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sumDeaths++;
|
||||||
|
this.sumKills = this.character.killCount;
|
||||||
|
this.onCharacterDied(this.character);
|
||||||
|
this.character = null;
|
||||||
|
this.timeUntilRespawn = settings.playerDiedTimeout;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
|
||||||
|
this.onBeforeRespawn();
|
||||||
|
this.createCharacter();
|
||||||
|
this.center = this.character!.center;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hooks for what a subclass wants to do with the body it just lost, and just
|
||||||
|
// before a new one is built. Both are no-ops by default.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected onCharacterDied(character: CharacterPhysical) {}
|
||||||
|
protected onBeforeRespawn() {}
|
||||||
|
|
||||||
private findSpawnCenter(): vec2 {
|
private findSpawnCenter(): vec2 {
|
||||||
const planets = this.objectContainer
|
const planets = this.objectContainer
|
||||||
.findIntersecting(
|
.findIntersecting(
|
||||||
|
|
@ -96,10 +136,7 @@ export abstract class PlayerBase extends CommandReceiver {
|
||||||
radius * Math.sin(rotation) + preferredCenter.y,
|
radius * Math.sin(rotation) + preferredCenter.y,
|
||||||
);
|
);
|
||||||
|
|
||||||
const playerBoundingCircle = new Circle(
|
const playerBoundingCircle = new Circle(playerPosition, boundRadius);
|
||||||
playerPosition,
|
|
||||||
CharacterPhysical.boundRadius,
|
|
||||||
);
|
|
||||||
|
|
||||||
const playerBoundingBox = getBoundingBoxOfCircle(playerBoundingCircle);
|
const playerBoundingBox = getBoundingBoxOfCircle(playerBoundingCircle);
|
||||||
const possibleIntersectors =
|
const possibleIntersectors =
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,11 @@ export class PlayerContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
public step(deltaTimeInSeconds: number) {
|
public step(deltaTimeInSeconds: number) {
|
||||||
this.players.forEach((p) => p.step(deltaTimeInSeconds));
|
// Iterates the two backing arrays rather than the merged `players` getter:
|
||||||
|
// this runs once per 200 Hz physics substep, and the getter builds a new
|
||||||
|
// array every call.
|
||||||
|
this._players.forEach((p) => p.step(deltaTimeInSeconds));
|
||||||
|
this._npcs.forEach((p) => p.step(deltaTimeInSeconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
public stepCommunication(deltaTimeInSeconds: number) {
|
public stepCommunication(deltaTimeInSeconds: number) {
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,11 @@ const maximumBufferedBytes = settings.maxBufferedBytesPerClient;
|
||||||
export class Player extends PlayerBase {
|
export class Player extends PlayerBase {
|
||||||
// default, until the clients sends its real value
|
// default, until the clients sends its real value
|
||||||
private aspectRatio: number = 16 / 9;
|
private aspectRatio: number = 16 / 9;
|
||||||
private timeUntilRespawn = 0;
|
|
||||||
private timeSinceLastMessage = 0;
|
private timeSinceLastMessage = 0;
|
||||||
private objectsPreviouslyInViewArea: Array<GameObject> = [];
|
// What this client has already been told about, in the order it was told.
|
||||||
|
// A Set because the per-snapshot diff below is membership-heavy; iteration
|
||||||
|
// order is insertion order, so the streamed object order is unchanged.
|
||||||
|
private objectsInViewArea = new Set<GameObject>();
|
||||||
private lastInputClientTimeMs = 0;
|
private lastInputClientTimeMs = 0;
|
||||||
private lastInputReceiptMs = 0;
|
private lastInputReceiptMs = 0;
|
||||||
private lastLeapClientTimeMs = 0;
|
private lastLeapClientTimeMs = 0;
|
||||||
|
|
@ -190,7 +192,7 @@ export class Player extends PlayerBase {
|
||||||
protected createCharacter() {
|
protected createCharacter() {
|
||||||
super.createCharacter();
|
super.createCharacter();
|
||||||
|
|
||||||
this.objectsPreviouslyInViewArea.push(this.character!);
|
this.objectsInViewArea.add(this.character!);
|
||||||
this.queueCommandSend(new CreatePlayerCommand(this.character!));
|
this.queueCommandSend(new CreatePlayerCommand(this.character!));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,27 +207,18 @@ export class Player extends PlayerBase {
|
||||||
private dyingCharacter?: CharacterPhysical | null;
|
private dyingCharacter?: CharacterPhysical | null;
|
||||||
|
|
||||||
public step(deltaTimeInSeconds: number) {
|
public step(deltaTimeInSeconds: number) {
|
||||||
if (this.character) {
|
this.stepLifecycle(deltaTimeInSeconds);
|
||||||
this.center = this.character?.center;
|
|
||||||
|
|
||||||
if (!this.character.isAlive) {
|
|
||||||
this.sumDeaths++;
|
|
||||||
this.sumKills = this.character.killCount;
|
|
||||||
|
|
||||||
this.dyingCharacter = this.character;
|
|
||||||
this.character = null;
|
|
||||||
this.timeUntilRespawn = settings.playerDiedTimeout;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) {
|
protected onCharacterDied(character: CharacterPhysical) {
|
||||||
|
this.dyingCharacter = character;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onBeforeRespawn() {
|
||||||
if (this.dyingCharacter) {
|
if (this.dyingCharacter) {
|
||||||
this.sumKills = Math.max(this.sumKills, this.dyingCharacter.killCount);
|
this.sumKills = Math.max(this.sumKills, this.dyingCharacter.killCount);
|
||||||
this.dyingCharacter = null;
|
this.dyingCharacter = null;
|
||||||
}
|
}
|
||||||
this.createCharacter();
|
|
||||||
this.center = this.character!.center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleViewAreaUpdate() {
|
private handleViewAreaUpdate() {
|
||||||
|
|
@ -234,26 +227,30 @@ export class Player extends PlayerBase {
|
||||||
bb.topLeft = viewArea.topLeft;
|
bb.topLeft = viewArea.topLeft;
|
||||||
bb.size = viewArea.size;
|
bb.size = viewArea.size;
|
||||||
|
|
||||||
const objectsInViewArea = Array.from(
|
const inViewArea = new Set(
|
||||||
new Set(this.objectContainer.findIntersecting(bb).map((o) => o.gameObject)),
|
this.objectContainer.findIntersecting(bb).map((o) => o.gameObject),
|
||||||
);
|
);
|
||||||
|
|
||||||
// The owning character must always be in its own snapshot, regardless of the
|
// The owning character must always be in its own snapshot, regardless of the
|
||||||
// view-area query, so the client predictor never loses its authoritative
|
// view-area query, so the client predictor never loses its authoritative
|
||||||
// anchor (the body can ride a fast spinner to the very edge of the box).
|
// anchor (the body can ride a fast spinner to the very edge of the box).
|
||||||
if (this.character && !objectsInViewArea.includes(this.character)) {
|
if (this.character) {
|
||||||
objectsInViewArea.push(this.character);
|
inViewArea.add(this.character);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newlyIntersecting = objectsInViewArea.filter(
|
// Set membership rather than Array.includes: this diff runs per player per
|
||||||
(o) => !this.objectsPreviouslyInViewArea.includes(o),
|
// snapshot over everything on their screen, and was quadratic in that.
|
||||||
|
// Set membership rather than Array.includes: this diff runs per player per
|
||||||
|
// snapshot over everything on their screen, and was quadratic in that.
|
||||||
|
const newlyIntersecting = [...inViewArea].filter(
|
||||||
|
(o) => !this.objectsInViewArea.has(o),
|
||||||
);
|
);
|
||||||
|
|
||||||
const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter(
|
const noLongerIntersecting = [...this.objectsInViewArea].filter(
|
||||||
(o) => !objectsInViewArea.includes(o),
|
(o) => !inViewArea.has(o),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.objectsPreviouslyInViewArea = objectsInViewArea;
|
this.objectsInViewArea = inViewArea;
|
||||||
|
|
||||||
if (noLongerIntersecting.length > 0) {
|
if (noLongerIntersecting.length > 0) {
|
||||||
this.queueCommandSend(
|
this.queueCommandSend(
|
||||||
|
|
@ -267,13 +264,15 @@ export class Player extends PlayerBase {
|
||||||
|
|
||||||
this.queueCommandSend(new UpdateMinimap(this.getMinimapPlayers()));
|
this.queueCommandSend(new UpdateMinimap(this.getMinimapPlayers()));
|
||||||
|
|
||||||
|
const propertyUpdates: Array<PropertyUpdatesForObject> = [];
|
||||||
|
for (const object of this.objectsInViewArea) {
|
||||||
|
const update = object.getPropertyUpdatesForFrame();
|
||||||
|
if (update) {
|
||||||
|
propertyUpdates.push(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.queueCommandSend(
|
this.queueCommandSend(
|
||||||
new PropertyUpdatesForObjects(
|
new PropertyUpdatesForObjects(propertyUpdates, performance.now() / 1000),
|
||||||
this.objectsPreviouslyInViewArea
|
|
||||||
.map((o) => o.getPropertyUpdatesForFrame())
|
|
||||||
.filter((u) => u) as Array<PropertyUpdatesForObject>,
|
|
||||||
performance.now() / 1000,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tell the client how much of its own input is reflected in the snapshot it
|
// Tell the client how much of its own input is reflected in the snapshot it
|
||||||
|
|
@ -310,11 +309,17 @@ export class Player extends PlayerBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public stepCommunications(deltaTime: number) {
|
public stepCommunications(deltaTime: number) {
|
||||||
const remoteCalls = this.objectsPreviouslyInViewArea
|
// Runs at the physics rate, and on most ticks nothing has fired — so test
|
||||||
.map((g) => new RemoteCallsForObject(g.id, g.getRemoteCalls()))
|
// first and only build the command when there is something to send.
|
||||||
.filter((c) => c.calls.length > 0);
|
let remoteCalls: Array<RemoteCallsForObject> | undefined;
|
||||||
|
for (const object of this.objectsInViewArea) {
|
||||||
|
const calls = object.getRemoteCalls();
|
||||||
|
if (calls.length > 0) {
|
||||||
|
(remoteCalls ??= []).push(new RemoteCallsForObject(object.id, calls));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (remoteCalls.length > 0) {
|
if (remoteCalls) {
|
||||||
this.queueCommandSend(new RemoteCallsForObjects(remoteCalls));
|
this.queueCommandSend(new RemoteCallsForObjects(remoteCalls));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ const main = async () => {
|
||||||
});
|
});
|
||||||
window.onpopstate = () => game.destroy();
|
window.onpopstate = () => game.destroy();
|
||||||
|
|
||||||
for (; ;) {
|
for (;;) {
|
||||||
show(spinner);
|
show(spinner);
|
||||||
hide(logoutButton, true);
|
hide(logoutButton, true);
|
||||||
show(landingUI, true, 'flex');
|
show(landingUI, true, 'flex');
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,7 @@ export class TouchListener extends CommandGenerator {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public update(_deltaTimeInSeconds: number) {
|
public update() {
|
||||||
if (!this.fireButton.parentElement) {
|
if (!this.fireButton.parentElement) {
|
||||||
this.overlay.appendChild(this.fireButton);
|
this.overlay.appendChild(this.fireButton);
|
||||||
}
|
}
|
||||||
|
|
@ -305,7 +305,8 @@ export class TouchListener extends CommandGenerator {
|
||||||
|
|
||||||
const character = this.game.gameObjects.player;
|
const character = this.game.gameObjects.player;
|
||||||
if (character) {
|
if (character) {
|
||||||
this.fireStrengthRing.style.background = `conic-gradient(rgba(255, 255, 255, 0.75) ${character.strengthFraction * 360
|
this.fireStrengthRing.style.background = `conic-gradient(rgba(255, 255, 255, 0.75) ${
|
||||||
|
character.strengthFraction * 360
|
||||||
}deg, transparent 0deg)`;
|
}deg, transparent 0deg)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,6 @@ const isDevelopment =
|
||||||
typeof location !== 'undefined' &&
|
typeof location !== 'undefined' &&
|
||||||
(location.hostname === 'localhost' || location.hostname === '127.0.0.1');
|
(location.hostname === 'localhost' || location.hostname === '127.0.0.1');
|
||||||
|
|
||||||
const servers: Array<string> = isDevelopment
|
export const servers: Array<string> = isDevelopment
|
||||||
? [`http://${location.hostname}:3000`, ...productionServers]
|
? [`http://${location.hostname}:3000`, ...productionServers]
|
||||||
: productionServers;
|
: productionServers;
|
||||||
|
|
||||||
export abstract class Configuration {
|
|
||||||
public static async initialize(): Promise<void> {
|
|
||||||
// Kept async for call-site compatibility; the server list is static now.
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get servers(): Array<string> {
|
|
||||||
return servers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -384,7 +384,7 @@ export class Game extends CommandReceiver {
|
||||||
new RenderCommand(this.renderer, this.overlay, shouldChangeLayout),
|
new RenderCommand(this.renderer, this.overlay, shouldChangeLayout),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.touchListener.update(deltaTime);
|
this.touchListener.update();
|
||||||
|
|
||||||
this.tutorial.step(this.gameObjects);
|
this.tutorial.step(this.gameObjects);
|
||||||
|
|
||||||
|
|
@ -406,8 +406,10 @@ export class Game extends CommandReceiver {
|
||||||
|
|
||||||
this.handleKeystoneArrow();
|
this.handleKeystoneArrow();
|
||||||
|
|
||||||
|
if (this.announcementText.innerHTML !== this.lastAnnouncementText) {
|
||||||
this.announcementText.innerHTML = this.lastAnnouncementText;
|
this.announcementText.innerHTML = this.lastAnnouncementText;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Points an off-screen chevron toward the keystone "Heart" planet, tinted by
|
// Points an off-screen chevron toward the keystone "Heart" planet, tinted by
|
||||||
// who currently holds it, so the match's focal objective is always findable.
|
// who currently holds it, so the match's focal objective is always findable.
|
||||||
|
|
@ -438,14 +440,7 @@ export class Game extends CommandReceiver {
|
||||||
display.y >= margin &&
|
display.y >= margin &&
|
||||||
display.y <= height - margin;
|
display.y <= height - margin;
|
||||||
|
|
||||||
const control = Math.abs(keystone.ownership - 0.5);
|
this.keystoneArrow.className = 'keystone-arrow ' + keystone.team;
|
||||||
const team =
|
|
||||||
control < settings.planetControlThreshold
|
|
||||||
? 'neutral'
|
|
||||||
: keystone.ownership < 0.5
|
|
||||||
? 'blue'
|
|
||||||
: 'red';
|
|
||||||
this.keystoneArrow.className = 'keystone-arrow ' + team;
|
|
||||||
|
|
||||||
if (onScreen) {
|
if (onScreen) {
|
||||||
this.keystoneArrow.style.display = 'none';
|
this.keystoneArrow.style.display = 'none';
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,11 @@ import {
|
||||||
import { ClientCharacterWorld, PredictablePlanet } from './client-character-world';
|
import { ClientCharacterWorld, PredictablePlanet } from './client-character-world';
|
||||||
import { InputHistory } from './input-history';
|
import { InputHistory } from './input-history';
|
||||||
|
|
||||||
const stepMs = 1000 / 200; // match the server's 200 Hz fixed tick
|
// The server's fixed tick, read from the same place the server reads it — the
|
||||||
const stepSeconds = 1 / 200;
|
// replay integrates over this dt, so a comment promising they match is not
|
||||||
|
// enough.
|
||||||
|
const stepSeconds = settings.targetPhysicsDeltaTimeInSeconds;
|
||||||
|
const stepMs = stepSeconds * 1000;
|
||||||
|
|
||||||
// The client's clock: the timestamp of the frame currently being rendered, set
|
// The client's clock: the timestamp of the frame currently being rendered, set
|
||||||
// once per frame by the game loop (requestAnimationFrame hands it to us
|
// once per frame by the game loop (requestAnimationFrame hands it to us
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ServerInformation, serverInformationEndpoint, TransportEvents } from 'shared';
|
import { ServerInformation, serverInformationEndpoint, TransportEvents } from 'shared';
|
||||||
import { io, Socket } from 'socket.io-client';
|
import { io, Socket } from 'socket.io-client';
|
||||||
import { Configuration } from './configuration';
|
import { servers } from './configuration';
|
||||||
import parser from 'socket.io-msgpack-parser';
|
import parser from 'socket.io-msgpack-parser';
|
||||||
import { SoundHandler, Sounds } from './sound-handler';
|
import { SoundHandler, Sounds } from './sound-handler';
|
||||||
|
|
||||||
|
|
@ -69,11 +69,7 @@ export class JoinFormHandler {
|
||||||
|
|
||||||
private servers: Array<ServerChooserOption> = [];
|
private servers: Array<ServerChooserOption> = [];
|
||||||
private async loadServers() {
|
private async loadServers() {
|
||||||
await Configuration.initialize();
|
const serverList = servers.filter((u) => !this.servers.find((s) => s.url === u));
|
||||||
|
|
||||||
const serverList = Configuration.servers.filter(
|
|
||||||
(u) => !this.servers.find((s) => s.url === u),
|
|
||||||
);
|
|
||||||
|
|
||||||
serverList.map(async (url) => {
|
serverList.map(async (url) => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { vec2, vec3 } from 'gl-matrix';
|
||||||
import { CircleLight, Renderer } from 'sdf-2d';
|
import { CircleLight, Renderer } from 'sdf-2d';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
characterCenter,
|
||||||
Circle,
|
Circle,
|
||||||
Id,
|
Id,
|
||||||
CharacterBase,
|
CharacterBase,
|
||||||
|
|
@ -10,6 +11,7 @@ import {
|
||||||
clamp,
|
clamp,
|
||||||
clamp01,
|
clamp01,
|
||||||
mix,
|
mix,
|
||||||
|
strengthToCharge,
|
||||||
CommandExecutors,
|
CommandExecutors,
|
||||||
UpdatePropertyCommand,
|
UpdatePropertyCommand,
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
|
|
@ -123,9 +125,7 @@ export class CharacterView extends CharacterBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get bodyCenter(): vec2 {
|
public get bodyCenter(): vec2 {
|
||||||
const center = vec2.add(vec2.create(), this.head!.center, this.leftFoot!.center);
|
return characterCenter(this.head!, this.leftFoot!, this.rightFoot!);
|
||||||
vec2.add(center, center, this.rightFoot!.center);
|
|
||||||
return vec2.scale(center, center, 1 / 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get facingDirection(): vec2 {
|
public get facingDirection(): vec2 {
|
||||||
|
|
@ -271,10 +271,7 @@ export class CharacterView extends CharacterBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public onShoot(strength: number) {
|
public onShoot(strength: number) {
|
||||||
const q = clamp01(
|
const q = strengthToCharge(strength);
|
||||||
(strength - settings.chargeShotStrengthMin) /
|
|
||||||
(settings.chargeShotStrengthMax - settings.chargeShotStrengthMin),
|
|
||||||
);
|
|
||||||
SoundHandler.play(Sounds.shoot, mix(0.55, 1, q), mix(1.15, 0.8, q));
|
SoundHandler.play(Sounds.shoot, mix(0.55, 1, q), mix(1.15, 0.8, q));
|
||||||
this.muzzleFlashIntensity = mix(0.35, 1, q);
|
this.muzzleFlashIntensity = mix(0.35, 1, q);
|
||||||
}
|
}
|
||||||
|
|
@ -332,16 +329,11 @@ export class CharacterView extends CharacterBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateTextPosition(): vec2 {
|
private calculateTextPosition(): vec2 {
|
||||||
const footAverage = vec2.add(
|
return vec2.scaleAndAdd(
|
||||||
vec2.create(),
|
vec2.create(),
|
||||||
this.leftFoot!.center,
|
this.head!.center,
|
||||||
this.rightFoot!.center,
|
this.facingDirection,
|
||||||
|
this.head!.radius + 80,
|
||||||
);
|
);
|
||||||
vec2.scale(footAverage, footAverage, 0.5);
|
|
||||||
|
|
||||||
const headFeetDelta = vec2.subtract(footAverage, this.head!.center, footAverage);
|
|
||||||
vec2.normalize(headFeetDelta, headFeetDelta);
|
|
||||||
const textOffset = vec2.scale(headFeetDelta, headFeetDelta, this.head!.radius + 80);
|
|
||||||
return vec2.add(textOffset, this.head!.center, textOffset);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue