even more
This commit is contained in:
parent
82e092ba55
commit
8f50ebdb97
1 changed files with 1842 additions and 3095 deletions
|
|
@ -2,16 +2,13 @@ import { vec2 } from 'gl-matrix';
|
|||
import {
|
||||
id,
|
||||
settings,
|
||||
MoveActionCommand,
|
||||
serializesTo,
|
||||
Circle,
|
||||
CharacterBase,
|
||||
CharacterTeam,
|
||||
PropertyUpdatesForObject,
|
||||
UpdatePropertyCommand,
|
||||
CommandExecutors,
|
||||
CommandReceiver,
|
||||
CharacterMovementSnapshot,
|
||||
GameObject,
|
||||
mix,
|
||||
clamp01,
|
||||
strengthToCharge,
|
||||
|
|
@ -20,6 +17,7 @@ import {
|
|||
decayMomentum,
|
||||
tickPlanetDetachment,
|
||||
characterCenter,
|
||||
sumGravity,
|
||||
CharacterWorld,
|
||||
GroundSurface,
|
||||
headRadius,
|
||||
|
|
@ -29,69 +27,45 @@ import {
|
|||
rightFootOffset,
|
||||
boundRadius,
|
||||
} from 'shared';
|
||||
import { DynamicPhysical } from '../physics/physicals/dynamic-physical';
|
||||
import { Physical } from '../physics/physical';
|
||||
import { CirclePhysical } from './circle-physical';
|
||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
||||
import { PhysicalContainer } from '../physics/physical-container';
|
||||
import { BoundingBox } from '../physics/bounding-box';
|
||||
import { ProjectilePhysical } from './projectile-physical';
|
||||
import { forceAtPosition } from '../physics/functions/force-at-position';
|
||||
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
||||
import { PlanetPhysical } from './planet-physical';
|
||||
import {
|
||||
StepCommand,
|
||||
ReactToCollisionCommand,
|
||||
GeneratePointsCommand,
|
||||
} from '../commands/commands';
|
||||
import { PlanetPhysical, planetsIn } from './planet-physical';
|
||||
|
||||
interface BodyPose {
|
||||
head: Circle;
|
||||
leftFoot: Circle;
|
||||
rightFoot: Circle;
|
||||
}
|
||||
const placeholder = new Circle(vec2.create(), 0);
|
||||
|
||||
@serializesTo(CharacterBase)
|
||||
export class CharacterPhysical extends CharacterBase implements DynamicPhysical {
|
||||
export class CharacterPhysical extends CharacterBase implements Physical {
|
||||
public readonly canCollide = true;
|
||||
public readonly canMove = true;
|
||||
|
||||
private projectileStrength = settings.playerMaxStrength;
|
||||
|
||||
private timeSinceDying = 0;
|
||||
private isDestroyed = false;
|
||||
private timeSinceBorn = 0;
|
||||
private hasJustBorn = true;
|
||||
private timeAlive = 0;
|
||||
|
||||
private timeSinceLastShot = settings.projectileCreationInterval;
|
||||
private timeSinceLastDamage = settings.playerOutOfCombatDelaySeconds;
|
||||
private lastSyncedHealth = settings.playerMaxHealth;
|
||||
|
||||
private killStreak = 0;
|
||||
|
||||
// Held in place: the shared simulation reads and writes these directly, so
|
||||
// the character IS the state it is stepped as (client predictor agrees).
|
||||
public direction = 0;
|
||||
public currentPlanet: GroundSurface | undefined;
|
||||
public secondsSinceOnSurface = settings.planetDetachmentSeconds;
|
||||
public readonly bodyVelocity = vec2.create();
|
||||
|
||||
private timeSinceLastLeap = settings.leapCooldownSeconds;
|
||||
|
||||
public head: CirclePhysical;
|
||||
public leftFoot: CirclePhysical;
|
||||
public rightFoot: CirclePhysical;
|
||||
|
||||
private movementActions: Array<MoveActionCommand> = [];
|
||||
private lastMovementAction: MoveActionCommand = new MoveActionCommand(vec2.create());
|
||||
public direction = 0;
|
||||
public currentPlanet: GroundSurface | undefined;
|
||||
public secondsSinceOnSurface = settings.planetDetachmentSeconds;
|
||||
public readonly bodyVelocity = vec2.create();
|
||||
|
||||
private headVelocity = new Circle(vec2.create(), 0);
|
||||
private leftFootVelocity = new Circle(vec2.create(), 0);
|
||||
private rightFootVelocity = new Circle(vec2.create(), 0);
|
||||
private projectileStrength = settings.playerMaxStrength;
|
||||
private timeAlive = 0;
|
||||
private timeSinceDying = 0;
|
||||
private isDestroyed = false;
|
||||
private hasJustBorn = true;
|
||||
private hasFiredSinceSpawn = false;
|
||||
private timeSinceLastShot = settings.projectileCreationInterval;
|
||||
private timeSinceLastLeap = settings.leapCooldownSeconds;
|
||||
private timeSinceLastDamage = settings.playerOutOfCombatDelaySeconds;
|
||||
private lastSyncedHealth = settings.playerMaxHealth;
|
||||
private killStreak = 0;
|
||||
|
||||
protected commandExecutors: CommandExecutors = {
|
||||
[StepCommand.type]: this.step.bind(this),
|
||||
[ReactToCollisionCommand.type]: this.onCollision.bind(this),
|
||||
};
|
||||
private movementDirections: Array<vec2> = [];
|
||||
private lastMovementDirection = vec2.create();
|
||||
|
||||
private readonly previousPose = [0, 1, 2].map(() => new Circle(vec2.create(), 0));
|
||||
private readonly velocities = [0, 1, 2].map(() => new Circle(vec2.create(), 0));
|
||||
private readonly box = new BoundingBox();
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
|
|
@ -101,43 +75,42 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
private readonly container: PhysicalContainer,
|
||||
startPosition: vec2,
|
||||
) {
|
||||
super(id(), name, killCount, deathCount, team, settings.playerMaxHealth);
|
||||
this.head = new CirclePhysical(
|
||||
vec2.add(vec2.create(), startPosition, headOffset),
|
||||
headRadius,
|
||||
this,
|
||||
container,
|
||||
super(
|
||||
id(),
|
||||
name,
|
||||
killCount,
|
||||
deathCount,
|
||||
team,
|
||||
settings.playerMaxHealth,
|
||||
placeholder,
|
||||
placeholder,
|
||||
placeholder,
|
||||
);
|
||||
this.leftFoot = new CirclePhysical(
|
||||
vec2.add(vec2.create(), startPosition, leftFootOffset),
|
||||
feetRadius,
|
||||
this,
|
||||
container,
|
||||
);
|
||||
this.rightFoot = new CirclePhysical(
|
||||
vec2.add(vec2.create(), startPosition, rightFootOffset),
|
||||
feetRadius,
|
||||
this,
|
||||
container,
|
||||
);
|
||||
container.addObject(this.head);
|
||||
container.addObject(this.leftFoot);
|
||||
container.addObject(this.rightFoot);
|
||||
const part = (offset: vec2, radius: number) =>
|
||||
new CirclePhysical(
|
||||
vec2.add(vec2.create(), startPosition, offset),
|
||||
radius,
|
||||
this,
|
||||
container,
|
||||
);
|
||||
this.head = part(headOffset, headRadius);
|
||||
this.leftFoot = part(leftFootOffset, feetRadius);
|
||||
this.rightFoot = part(rightFootOffset, feetRadius);
|
||||
}
|
||||
|
||||
private readonly movementWorld: CharacterWorld = {
|
||||
// Same set and order forceAtPosition uses, so the f64 gravity sum matches.
|
||||
groundsNear: (center, radius) =>
|
||||
this.container
|
||||
.findIntersecting(getBoundingBoxOfCircle(new Circle(center, radius)))
|
||||
.filter((o): o is PlanetPhysical => o instanceof PlanetPhysical),
|
||||
planetsIn(this.container.findIntersecting(BoundingBox.ofCircle(center, radius))),
|
||||
stepBody: (body, deltaTimeInSeconds) => {
|
||||
const { hitObject } = (body as CirclePhysical).stepManually(deltaTimeInSeconds);
|
||||
return hitObject instanceof PlanetPhysical ? hitObject : undefined;
|
||||
const hit = (body as CirclePhysical).stepManually(deltaTimeInSeconds);
|
||||
return hit instanceof PlanetPhysical ? hit : undefined;
|
||||
},
|
||||
};
|
||||
|
||||
private hasFiredSinceSpawn = false;
|
||||
private get parts(): Array<CirclePhysical> {
|
||||
return [this.head, this.leftFoot, this.rightFoot];
|
||||
}
|
||||
|
||||
private get isSpawnProtected(): boolean {
|
||||
return (
|
||||
!this.hasFiredSinceSpawn &&
|
||||
|
|
@ -146,38 +119,14 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
);
|
||||
}
|
||||
|
||||
private hasGeneratedPoints = false;
|
||||
private wasKilledInCombat = false;
|
||||
private getPoints(game: CommandReceiver) {
|
||||
if (!this.isAlive && !this.hasGeneratedPoints) {
|
||||
this.hasGeneratedPoints = true;
|
||||
|
||||
if (!this.wasKilledInCombat) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blue = this.team === CharacterTeam.blue ? 0 : settings.playerKillPoint;
|
||||
const red = this.team === CharacterTeam.red ? 0 : settings.playerKillPoint;
|
||||
|
||||
game.handleCommand(new GeneratePointsCommand(blue, red));
|
||||
}
|
||||
}
|
||||
|
||||
public get isAlive(): boolean {
|
||||
return !this.isDestroyed;
|
||||
}
|
||||
|
||||
public handleMovementAction(c: MoveActionCommand) {
|
||||
this.movementActions.push(c);
|
||||
}
|
||||
|
||||
// The world only ever hands back planets, so narrowing GroundSurface is safe.
|
||||
public get groundPlanet(): PlanetPhysical | undefined {
|
||||
return this.currentPlanet as PlanetPhysical | undefined;
|
||||
}
|
||||
|
||||
// Continuous movement state streamed to the owning client so its predictor
|
||||
// resumes this simulation instead of guessing. See CharacterMovementSnapshot.
|
||||
public get movementSnapshot(): CharacterMovementSnapshot {
|
||||
return new CharacterMovementSnapshot(
|
||||
this.direction,
|
||||
|
|
@ -189,6 +138,37 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
);
|
||||
}
|
||||
|
||||
public get boundingBox(): BoundingBox {
|
||||
const center = this.center;
|
||||
this.box.xMin = center[0] - boundRadius;
|
||||
this.box.xMax = center[0] + boundRadius;
|
||||
this.box.yMin = center[1] - boundRadius;
|
||||
this.box.yMax = center[1] + boundRadius;
|
||||
return this.box;
|
||||
}
|
||||
|
||||
public get gameObject(): this {
|
||||
return this;
|
||||
}
|
||||
|
||||
public get center(): vec2 {
|
||||
return characterCenter(this.head, this.leftFoot, this.rightFoot);
|
||||
}
|
||||
|
||||
public distance(target: vec2): number {
|
||||
return (
|
||||
Math.min(
|
||||
this.head.distance(target),
|
||||
this.leftFoot.distance(target),
|
||||
this.rightFoot.distance(target),
|
||||
) - 5
|
||||
);
|
||||
}
|
||||
|
||||
public setMoveDirection(direction: vec2) {
|
||||
this.movementDirections.push(direction);
|
||||
}
|
||||
|
||||
public addKill(victimName: string, charge = 0) {
|
||||
this.killCount++;
|
||||
this.killStreak++;
|
||||
|
|
@ -214,38 +194,38 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
}
|
||||
}
|
||||
|
||||
public onCollision({ other }: ReactToCollisionCommand) {
|
||||
public onCollision(other: GameObject) {
|
||||
if (
|
||||
// A corpse keeps its collidable circles for the despawn animation; the
|
||||
// isAlive guard stops a flying corpse from eating shots aimed past it.
|
||||
this.isAlive &&
|
||||
other instanceof ProjectilePhysical &&
|
||||
other.team !== this.team &&
|
||||
other.isAlive
|
||||
!this.isAlive ||
|
||||
!(other instanceof ProjectilePhysical) ||
|
||||
other.team === this.team ||
|
||||
!other.isAlive
|
||||
) {
|
||||
other.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isSpawnProtected) {
|
||||
return;
|
||||
}
|
||||
other.destroy();
|
||||
|
||||
this.timeSinceLastDamage = 0;
|
||||
this.health -= other.strength;
|
||||
this.lastSyncedHealth = Math.round(this.health);
|
||||
this.remoteCall('setHealth', this.health);
|
||||
if (this.isSpawnProtected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.health <= 0 && this.isAlive) {
|
||||
vec2.scaleAndAdd(
|
||||
this.bodyVelocity,
|
||||
this.bodyVelocity,
|
||||
other.direction,
|
||||
mix(settings.deathImpulseMin, settings.deathImpulseMax, other.charge),
|
||||
);
|
||||
this.onDie(true);
|
||||
other.originator.addKill(this.name, other.charge);
|
||||
} else {
|
||||
other.originator.registerHit(other.charge);
|
||||
}
|
||||
this.timeSinceLastDamage = 0;
|
||||
this.health -= other.strength;
|
||||
this.lastSyncedHealth = Math.round(this.health);
|
||||
this.remoteCall('setHealth', this.health);
|
||||
|
||||
if (this.health <= 0) {
|
||||
vec2.scaleAndAdd(
|
||||
this.bodyVelocity,
|
||||
this.bodyVelocity,
|
||||
other.direction,
|
||||
mix(settings.deathImpulseMin, settings.deathImpulseMax, other.charge),
|
||||
);
|
||||
this.onDie(true);
|
||||
other.originator.addKill(this.name, other.charge);
|
||||
} else {
|
||||
other.originator.registerHit(other.charge);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -266,41 +246,37 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
this.timeSinceLastShot = 0;
|
||||
this.hasFiredSinceSpawn = true;
|
||||
|
||||
const requestedCharge = clamp01(charge);
|
||||
const desiredStrength = mix(
|
||||
settings.chargeShotStrengthMin,
|
||||
settings.chargeShotStrengthMax,
|
||||
requestedCharge,
|
||||
clamp01(charge),
|
||||
);
|
||||
const strength = Math.min(desiredStrength, this.projectileStrength);
|
||||
this.projectileStrength -= strength;
|
||||
|
||||
const c = strengthToCharge(strength);
|
||||
|
||||
const radius = mix(settings.chargeShotRadiusMin, settings.chargeShotRadiusMax, c);
|
||||
const speed = mix(settings.chargeShotSpeedMin, settings.chargeShotSpeedMax, c);
|
||||
|
||||
vec2.normalize(direction, direction);
|
||||
// Keep the unit direction before vec2.scale repurposes it as the velocity.
|
||||
const shotDirection = vec2.clone(direction);
|
||||
const velocity = vec2.scale(direction, direction, speed);
|
||||
const projectile = new ProjectilePhysical(
|
||||
vec2.clone(this.center),
|
||||
radius,
|
||||
strength,
|
||||
this.team,
|
||||
velocity,
|
||||
this,
|
||||
this.container,
|
||||
c,
|
||||
this.container.addObject(
|
||||
new ProjectilePhysical(
|
||||
vec2.clone(this.center),
|
||||
radius,
|
||||
strength,
|
||||
this.team,
|
||||
vec2.scale(vec2.create(), direction, speed),
|
||||
this,
|
||||
this.container,
|
||||
c,
|
||||
),
|
||||
);
|
||||
this.container.addObject(projectile);
|
||||
|
||||
if (c > 0) {
|
||||
vec2.scaleAndAdd(
|
||||
this.bodyVelocity,
|
||||
this.bodyVelocity,
|
||||
shotDirection,
|
||||
direction,
|
||||
-settings.chargeShotRecoilMax * c,
|
||||
);
|
||||
}
|
||||
|
|
@ -321,67 +297,32 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
|
||||
this.timeSinceLastLeap = 0;
|
||||
this.projectileStrength -= settings.leapStrengthCost;
|
||||
|
||||
// Same impulse the client predicts with (shared), so a leap launches
|
||||
// identically on both sides.
|
||||
applyLeapImpulse(this, this.lastMovementAction.direction);
|
||||
applyLeapImpulse(this, this.lastMovementDirection);
|
||||
this.remoteCall('onLeap');
|
||||
}
|
||||
|
||||
public get boundingBox(): BoundingBoxBase {
|
||||
return getBoundingBoxOfCircle(new Circle(this.center, boundRadius));
|
||||
}
|
||||
|
||||
public get gameObject(): this {
|
||||
return this;
|
||||
}
|
||||
|
||||
public get center(): vec2 {
|
||||
return characterCenter(this.head, this.leftFoot, this.rightFoot);
|
||||
}
|
||||
|
||||
public distance(target: vec2): number {
|
||||
return (
|
||||
Math.min(
|
||||
this.head.distance(target),
|
||||
this.leftFoot.distance(target),
|
||||
this.rightFoot.distance(target),
|
||||
) - 5
|
||||
);
|
||||
}
|
||||
|
||||
private averageAndResetMovementActions(): vec2 {
|
||||
let direction: vec2;
|
||||
if (this.movementActions.length === 0) {
|
||||
direction = vec2.clone(this.lastMovementAction.direction);
|
||||
} else {
|
||||
direction = this.movementActions.reduce(
|
||||
(sum, current) => vec2.add(sum, sum, current.direction),
|
||||
vec2.create(),
|
||||
);
|
||||
|
||||
vec2.scale(direction, direction, 1 / this.movementActions.length);
|
||||
|
||||
const actions = this.movementActions;
|
||||
this.lastMovementAction = actions[actions.length - 1]!;
|
||||
this.movementActions = [];
|
||||
public onDie(killedInCombat = false) {
|
||||
if (this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
this.isDestroyed = true;
|
||||
this.remoteCall('onDie');
|
||||
|
||||
return vec2.length(direction) > 0
|
||||
? vec2.normalize(direction, direction)
|
||||
: vec2.create();
|
||||
}
|
||||
|
||||
private animateScaling(q: number) {
|
||||
this.head.radius = headRadius * q;
|
||||
this.leftFoot.radius = this.rightFoot.radius = feetRadius * q;
|
||||
if (killedInCombat) {
|
||||
const points = settings.playerKillPoint;
|
||||
this.container.game.addPoints(
|
||||
this.team === CharacterTeam.blue ? 0 : points,
|
||||
this.team === CharacterTeam.red ? 0 : points,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public getPropertyUpdates(): PropertyUpdatesForObject {
|
||||
const [headVelocity, leftFootVelocity, rightFootVelocity] = this.velocities;
|
||||
return new PropertyUpdatesForObject(this.id, [
|
||||
new UpdatePropertyCommand('head', this.head, this.headVelocity),
|
||||
new UpdatePropertyCommand('leftFoot', this.leftFoot, this.leftFootVelocity),
|
||||
new UpdatePropertyCommand('rightFoot', this.rightFoot, this.rightFootVelocity),
|
||||
new UpdatePropertyCommand('head', this.head, headVelocity),
|
||||
new UpdatePropertyCommand('leftFoot', this.leftFoot, leftFootVelocity),
|
||||
new UpdatePropertyCommand('rightFoot', this.rightFoot, rightFootVelocity),
|
||||
new UpdatePropertyCommand(
|
||||
'strength',
|
||||
this.projectileStrength,
|
||||
|
|
@ -390,107 +331,51 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
]);
|
||||
}
|
||||
|
||||
// `previous` is consumed as scratch so this allocates nothing beyond the
|
||||
// Circle it returns.
|
||||
private static rateOfChange(previous: Circle, current: Circle, deltaTime: number) {
|
||||
return new Circle(
|
||||
vec2.scale(
|
||||
previous.center,
|
||||
vec2.subtract(previous.center, current.center, previous.center),
|
||||
1 / deltaTime,
|
||||
),
|
||||
(current.radius - previous.radius) / deltaTime,
|
||||
);
|
||||
}
|
||||
|
||||
private setPropertyUpdates(previous: BodyPose, deltaTime: number) {
|
||||
const rate = CharacterPhysical.rateOfChange;
|
||||
this.headVelocity = rate(previous.head, this.head, deltaTime);
|
||||
this.leftFootVelocity = rate(previous.leftFoot, this.leftFoot, deltaTime);
|
||||
this.rightFootVelocity = rate(previous.rightFoot, this.rightFoot, deltaTime);
|
||||
}
|
||||
|
||||
private get pose(): BodyPose {
|
||||
const snapshot = (circle: Circle) =>
|
||||
new Circle(vec2.clone(circle.center), circle.radius);
|
||||
return {
|
||||
head: snapshot(this.head),
|
||||
leftFoot: snapshot(this.leftFoot),
|
||||
rightFoot: snapshot(this.rightFoot),
|
||||
};
|
||||
}
|
||||
|
||||
private step({ deltaTimeInSeconds, game }: StepCommand) {
|
||||
this.getPoints(game);
|
||||
public step(deltaTimeInSeconds: number) {
|
||||
this.timeAlive += deltaTimeInSeconds;
|
||||
this.timeSinceLastLeap += deltaTimeInSeconds;
|
||||
const previousPose = this.pose;
|
||||
this.parts.forEach((part, i) => {
|
||||
vec2.copy(this.previousPose[i].center, part.center);
|
||||
this.previousPose[i].radius = part.radius;
|
||||
});
|
||||
|
||||
if (this.isDestroyed) {
|
||||
if ((this.timeSinceDying += deltaTimeInSeconds) > settings.spawnDespawnTime) {
|
||||
this.destroy();
|
||||
this.container.removeObject(this);
|
||||
} else {
|
||||
this.freeFallCorpse(deltaTimeInSeconds);
|
||||
this.animateScaling(1 - this.timeSinceDying / settings.spawnDespawnTime);
|
||||
}
|
||||
this.setPropertyUpdates(previousPose, deltaTimeInSeconds);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasJustBorn) {
|
||||
if ((this.timeSinceBorn += deltaTimeInSeconds) > settings.spawnDespawnTime) {
|
||||
} else if (this.hasJustBorn) {
|
||||
if (this.timeAlive > settings.spawnDespawnTime) {
|
||||
this.hasJustBorn = false;
|
||||
this.animateScaling(1);
|
||||
} else {
|
||||
this.animateScaling(this.timeSinceBorn / settings.spawnDespawnTime);
|
||||
this.animateScaling(this.timeAlive / settings.spawnDespawnTime);
|
||||
}
|
||||
this.setPropertyUpdates(previousPose, deltaTimeInSeconds);
|
||||
return;
|
||||
} else {
|
||||
this.stepAlive(deltaTimeInSeconds);
|
||||
}
|
||||
|
||||
this.parts.forEach((part, i) => {
|
||||
const velocity = this.velocities[i];
|
||||
const previous = this.previousPose[i];
|
||||
vec2.subtract(velocity.center, part.center, previous.center);
|
||||
vec2.scale(velocity.center, velocity.center, 1 / deltaTimeInSeconds);
|
||||
velocity.radius = (part.radius - previous.radius) / deltaTimeInSeconds;
|
||||
});
|
||||
}
|
||||
|
||||
private stepAlive(deltaTimeInSeconds: number) {
|
||||
tickPlanetDetachment(this, deltaTimeInSeconds);
|
||||
|
||||
this.timeSinceLastShot += deltaTimeInSeconds;
|
||||
|
||||
this.projectileStrength = Math.min(
|
||||
settings.playerMaxStrength,
|
||||
this.projectileStrength +
|
||||
settings.playerStrengthRegenerationPerSeconds * deltaTimeInSeconds,
|
||||
);
|
||||
|
||||
this.regenerateHealth(deltaTimeInSeconds);
|
||||
|
||||
this.groundPlanet?.registerPresence(this);
|
||||
|
||||
// stepCharacterMovement is the shared simulation the client predicts with,
|
||||
// so the two can never drift.
|
||||
const direction = this.averageAndResetMovementActions();
|
||||
stepCharacterMovement(this, this.movementWorld, direction, deltaTimeInSeconds);
|
||||
|
||||
this.setPropertyUpdates(previousPose, deltaTimeInSeconds);
|
||||
}
|
||||
|
||||
private freeFallCorpse(deltaTime: number) {
|
||||
const intersecting = this.container.findIntersecting(
|
||||
getBoundingBoxOfCircle(
|
||||
new Circle(this.center, boundRadius + settings.maxGravityDistance),
|
||||
),
|
||||
);
|
||||
let grounded = false;
|
||||
for (const part of [this.leftFoot, this.rightFoot, this.head]) {
|
||||
part.applyForce(forceAtPosition(part.center, intersecting), deltaTime);
|
||||
vec2.add(part.velocity, part.velocity, this.bodyVelocity);
|
||||
const { hitObject } = part.stepManually(deltaTime);
|
||||
if (hitObject instanceof PlanetPhysical) {
|
||||
grounded = true;
|
||||
}
|
||||
}
|
||||
// Brake with the shared model the living body uses, so a flung corpse
|
||||
// comes to a definite stop instead of sliding forever.
|
||||
decayMomentum(this.bodyVelocity, grounded, deltaTime);
|
||||
}
|
||||
|
||||
private regenerateHealth(deltaTimeInSeconds: number) {
|
||||
this.timeSinceLastDamage += deltaTimeInSeconds;
|
||||
if (
|
||||
this.timeSinceLastDamage > settings.playerOutOfCombatDelaySeconds &&
|
||||
|
|
@ -502,18 +387,51 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical
|
|||
);
|
||||
this.syncHealth();
|
||||
}
|
||||
|
||||
this.groundPlanet?.registerPresence(this);
|
||||
|
||||
stepCharacterMovement(
|
||||
this,
|
||||
this.movementWorld,
|
||||
this.averageAndResetMovementDirections(),
|
||||
deltaTimeInSeconds,
|
||||
);
|
||||
}
|
||||
|
||||
public onDie(killedInCombat = false) {
|
||||
this.wasKilledInCombat = killedInCombat;
|
||||
this.isDestroyed = true;
|
||||
this.remoteCall('onDie');
|
||||
private averageAndResetMovementDirections(): vec2 {
|
||||
const direction = vec2.create();
|
||||
if (this.movementDirections.length === 0) {
|
||||
vec2.copy(direction, this.lastMovementDirection);
|
||||
} else {
|
||||
for (const d of this.movementDirections) {
|
||||
vec2.add(direction, direction, d);
|
||||
}
|
||||
vec2.scale(direction, direction, 1 / this.movementDirections.length);
|
||||
this.lastMovementDirection =
|
||||
this.movementDirections[this.movementDirections.length - 1];
|
||||
this.movementDirections = [];
|
||||
}
|
||||
return vec2.normalize(direction, direction);
|
||||
}
|
||||
|
||||
private destroy() {
|
||||
this.container.removeObject(this);
|
||||
this.container.removeObject(this.head);
|
||||
this.container.removeObject(this.leftFoot);
|
||||
this.container.removeObject(this.rightFoot);
|
||||
private animateScaling(q: number) {
|
||||
this.head.radius = headRadius * q;
|
||||
this.leftFoot.radius = this.rightFoot.radius = feetRadius * q;
|
||||
}
|
||||
|
||||
private freeFallCorpse(deltaTime: number) {
|
||||
const planets = this.movementWorld.groundsNear(
|
||||
this.center,
|
||||
boundRadius + settings.maxGravityDistance,
|
||||
);
|
||||
let grounded = false;
|
||||
for (const part of this.parts) {
|
||||
vec2.copy(part.velocity, this.bodyVelocity);
|
||||
part.applyForce(sumGravity(planets, part.center), deltaTime);
|
||||
if (part.stepManually(deltaTime) instanceof PlanetPhysical) {
|
||||
grounded = true;
|
||||
}
|
||||
}
|
||||
decayMomentum(this.bodyVelocity, grounded, deltaTime);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue