Skip to content

Commit

Permalink
Add support for gameplay saving and replaying.
Browse files Browse the repository at this point in the history
This will form part of the basis for the multiplayer component that is
to come.
  • Loading branch information
Half-Shot committed Oct 9, 2024
1 parent 0087c7c commit 5d8829b
Show file tree
Hide file tree
Showing 24 changed files with 750 additions and 26 deletions.
2 changes: 0 additions & 2 deletions src/components/changelog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export function ChangelogModal({buildNumber, buildCommit, lastCommit}: {buildNum
setLatestChanges(['Could not load changes']);
}
const result = await req.json();
console.log(result);
setLatestChanges(result.commits.map((c: any) => `${c.commit.message}`).reverse());

Check warning on line 20 in src/components/changelog.tsx

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
})();
}, [buildCommit, lastCommit, setLatestChanges]);
Expand All @@ -28,7 +27,6 @@ export function ChangelogModal({buildNumber, buildCommit, lastCommit}: {buildNum
}, [modalRef]);

const newChangesModal = useMemo(() => {
console.log(latestChanges);
const title = buildNumber ? `Build #${buildNumber}` : `Developer Build ${buildCommit}`;
return <dialog ref={modalRef}>
<h1>{title}</h1>
Expand Down
3 changes: 3 additions & 0 deletions src/components/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export function Menu({onNewGame}: Props) {
<li>
<button onClick={() => onStartNewGame("uiTest")}>UI Test</button>
</li>
<li>
<button onClick={() => onStartNewGame("replayTesting")}>Test gameplay replay</button>
</li>
<li>
<button onClick={() => onStartNewGame("boneIsles")}>Bone Isles</button>
</li>
Expand Down
4 changes: 4 additions & 0 deletions src/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ export function readAssetsForEntities(assets: AssetPack): void {
Worm.readAssets(assets);
Explosion.readAssets(assets);
PhysicsEntity.readAssets(assets);
}

function entityToType() {

Check warning on line 27 in src/entities/index.ts

View workflow job for this annotation

GitHub Actions / ci

'entityToType' is defined but never used

Check failure on line 27 in src/entities/index.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected empty function 'entityToType'

}
10 changes: 10 additions & 0 deletions src/entities/phys/bazookaShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { Coordinate, MetersValue } from '../../utils/coodinate';
import { AssetPack } from '../../assets';
import { WormInstance } from '../../logic/teams';
import { angleForVector } from '../../utils';
import { RecordedEntityState } from '../../state/model';

Check warning on line 9 in src/entities/phys/bazookaShell.ts

View workflow job for this annotation

GitHub Actions / ci

'RecordedEntityState' is defined but never used
import { EntityType } from '../type';


/**
* Standard shell, affected by wind.
Expand Down Expand Up @@ -83,4 +86,11 @@ export class BazookaShell extends TimedExplosive {
super.destroy();
this.gfx.destroy();
}

recordState() {
return {
...super.recordState(),
type: EntityType.BazookaShell,
}
}
}
8 changes: 8 additions & 0 deletions src/entities/phys/firework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Coordinate, MetersValue } from '../../utils/coodinate';
import { AssetPack } from '../../assets';
import { BitmapTerrain } from '../bitmapTerrain';
import { angleForVector } from '../../utils';
import { EntityType } from '../type';


const COLOUR_SET = [
Expand Down Expand Up @@ -156,6 +157,13 @@ export class Firework extends TimedExplosive {
return false;
}

recordState() {
return {
...super.recordState(),
type: EntityType.Firework,
}
}

destroy(): void {
if (this.trail.length) {
super.destroy();
Expand Down
11 changes: 8 additions & 3 deletions src/entities/phys/grenade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Coordinate, MetersValue } from '../../utils/coodinate';
import { AssetPack } from '../../assets';
import { DefaultTextStyle } from '../../mixins/styles';
import { WormInstance } from '../../logic/teams';
import { EntityType } from '../type';


/**
Expand Down Expand Up @@ -53,8 +54,6 @@ export class Grenade extends TimedExplosive {
RigidBodyDesc
.dynamic()
.setTranslation(position.worldX, position.worldY)
// .setLinvel(initialForce.x, initialForce.y)
// .setLinearDamping(Grenade.FRICTION)
);
sprite.position = body.body.translation();
super(sprite, body, world, parent, {
Expand All @@ -65,7 +64,6 @@ export class Grenade extends TimedExplosive {
ownerWorm: owner,
maxDamage: 40,
});
//Body.applyForce(body, Vector.create(body.position.x - 20, body.position.y), initialForce);
this.timerText = new Text({
text: '',
style: {
Expand Down Expand Up @@ -115,6 +113,13 @@ export class Grenade extends TimedExplosive {
return false;
}

recordState() {
return {
...super.recordState(),
type: EntityType.Grenade,
}
}

destroy(): void {
super.destroy();
}
Expand Down
8 changes: 8 additions & 0 deletions src/entities/phys/mine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Coordinate, MetersValue } from '../../utils/coodinate';
import { AssetPack } from '../../assets';
import { BitmapTerrain } from '../bitmapTerrain';
import { DefaultTextStyle } from '../../mixins/styles';
import { EntityType } from '../type';

/**
* Proximity mine.
Expand Down Expand Up @@ -122,6 +123,13 @@ export class Mine extends TimedExplosive {
return false;
}

recordState() {
return {
...super.recordState(),
type: EntityType.Mine,
}
}

destroy(): void {
this.beeping?.then((b) => {
b.stop();
Expand Down
25 changes: 24 additions & 1 deletion src/entities/phys/physicsEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { GameWorld, PIXELS_PER_METER, RapierPhysicsObject } from "../../world";
import { Vector2 } from "@dimforge/rapier2d-compat";
import { magnitude, MetersValue, mult, sub } from "../../utils";
import { AssetPack } from "../../assets";
import type { RecordedEntityState } from "../../state/model";

/**
* Abstract class for any physical object in the world. The
Expand All @@ -16,7 +17,7 @@ import { AssetPack } from "../../assets";
* Collision on water and force from explosions are automatically
* calculated.
*/
export abstract class PhysicsEntity implements IPhysicalEntity {
export abstract class PhysicsEntity<T extends RecordedEntityState = RecordedEntityState> implements IPhysicalEntity {
public static readAssets({sounds}: AssetPack) {
PhysicsEntity.splashSound = sounds.splash;
}
Expand Down Expand Up @@ -109,4 +110,26 @@ export abstract class PhysicsEntity implements IPhysicalEntity {
const force = mult(sub(point, bodyTranslation), new Vector2(-forceMag, -forceMag*1.5));
this.physObject.body.applyImpulse(force, true)
}

recordState(): T {
const translation = this.body.translation();
const rotation = this.body.rotation();
const linvel = this.body.linvel();
return {
type: -1,
tra: {
x: translation.x.toString(),
y: translation.y.toString(),
},
rot: rotation.toString(),
vel: {
x: linvel.x.toString(),
y: linvel.y.toString(),
}
} as T;
}

loadState(d: T) {

Check warning on line 132 in src/entities/phys/physicsEntity.ts

View workflow job for this annotation

GitHub Actions / ci

'd' is defined but never used

Check failure on line 132 in src/entities/phys/physicsEntity.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected empty method 'loadState'

}
}
18 changes: 17 additions & 1 deletion src/entities/phys/timedExplosive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { MetersValue } from "../../utils/coodinate";
import { WeaponFireResult } from "../../weapons/weapon";
import { WormInstance } from "../../logic/teams";
import { handleDamageInRadius } from "../../utils/damage";
import { RecordedEntityState } from "../../state/model";

interface Opts {
explosionRadius: MetersValue,
Expand All @@ -19,11 +20,17 @@ interface Opts {
maxDamage: number,
}

interface RecordedState extends RecordedEntityState {
owner?: string,
timerSecs?: number,
timer?: number,
}

/**
* Any projectile type that can explode after a set timer. Implementing classes
* must include their own timer.
*/
export abstract class TimedExplosive extends PhysicsEntity implements IWeaponEntity {
export abstract class TimedExplosive extends PhysicsEntity<RecordedState> implements IWeaponEntity {
protected timer: number|undefined;
protected hasExploded = false;

Expand Down Expand Up @@ -105,4 +112,13 @@ export abstract class TimedExplosive extends PhysicsEntity implements IWeaponEnt

return false;
}

recordState() {
return {
timer: this.timer,
owner: this.opts.ownerWorm?.uuid,
timerSecs: this.opts.timerSecs,
...super.recordState(),
}
}
}
14 changes: 13 additions & 1 deletion src/entities/playable/playable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { teamGroupToColorSet, WormInstance } from "../../logic/teams";
import { applyGenericBoxStyle, DefaultTextStyle } from "../../mixins/styles";
import { Viewport } from "pixi-viewport";
import { handleDamageInRadius } from "../../utils/damage";
import { RecordedEntityState } from "../../state/model";


interface Opts {
Expand All @@ -19,10 +20,14 @@ interface Opts {
const HEALTH_TENSION_MS = 75;
const SELF_EXPLODE_MAX_DAMAGE = 25;

interface RecordedState extends RecordedEntityState {
wormIdent: string
}

/**
* Entity that can be directly controlled by a player.
*/
export abstract class PlayableEntity extends PhysicsEntity {
export abstract class PlayableEntity extends PhysicsEntity<RecordedState> {
priority = UPDATE_PRIORITY.LOW;

private nameText: Text;
Expand Down Expand Up @@ -165,6 +170,13 @@ export abstract class PlayableEntity extends PhysicsEntity {
this.physObject.body.applyImpulse(force, true)
}

public recordState() {
return {
...super.recordState(),
wormIdent: this.wormIdent.uuid,
}
}

public destroy(): void {
super.destroy();
if (!this.healthTextBox.destroyed) {
Expand Down
60 changes: 60 additions & 0 deletions src/entities/playable/remoteWorm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Viewport } from "pixi-viewport";
import { WormInstance } from "../../logic/teams";
import { Toaster } from "../../overlays/toaster";
import { StateRecorder } from "../../state/recorder";

Check warning on line 4 in src/entities/playable/remoteWorm.ts

View workflow job for this annotation

GitHub Actions / ci

'StateRecorder' is defined but never used
import { Coordinate } from "../../utils";
import { GameWorld } from "../../world";
import { FireFn, Worm, WormState } from "./worm";
import { StateWormAction } from "../../state/model";
import { InputKind } from "../../input";

/**
* An instance of the worm class controlled by a remote (or AI) player.
*/
export class RemoteWorm extends Worm {
static create(parent: Viewport, world: GameWorld, position: Coordinate, wormIdent: WormInstance, onFireWeapon: FireFn, toaster?: Toaster) {
const ent = new RemoteWorm(position, world, parent, wormIdent, onFireWeapon, toaster);
world.addBody(ent, ent.physObject.collider);
parent.addChild(ent.targettingGfx);
parent.addChild(ent.sprite);
parent.addChild(ent.wireframe.renderable);
parent.addChild(ent.healthTextBox);
return ent;
}

private constructor(position: Coordinate, world: GameWorld, parent: Viewport, wormIdent: WormInstance, onFireWeapon: FireFn, toaster?: Toaster) {
super(position, world, parent, wormIdent, onFireWeapon, toaster, undefined);
}

public replayWormAction(remoteAction: StateWormAction) {
switch (remoteAction) {
case StateWormAction.MoveLeft:
this.setMoveDirection(InputKind.MoveLeft);
break;
case StateWormAction.MoveRight:
this.setMoveDirection(InputKind.MoveRight);
break;
case StateWormAction.Jump:
this.onJump();
break;
case StateWormAction.Backflip:
this.onBackflip();
break;
case StateWormAction.AimUp:
this.state = WormState.AimingUp;
break;
case StateWormAction.AimDown:
this.state = WormState.AimingDown;
break;
case StateWormAction.Fire:
this.onBeginFireWeapon();
break;
case StateWormAction.EndFire:
this.onEndFireWeapon();
break;
case StateWormAction.Stop:
this.state = WormState.Idle;
break;
}
}
}
8 changes: 8 additions & 0 deletions src/entities/playable/testDummy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ActiveEvents, ColliderDesc, RigidBodyDesc } from "@dimforge/rapier2d-co
import { WormInstance } from "../../logic/teams";
import { PlayableEntity } from "./playable";
import { Viewport } from "pixi-viewport";
import { EntityType } from "../type";

/**
* Test dummy entity that may be associated with a worm identity. These
Expand Down Expand Up @@ -92,4 +93,11 @@ export class TestDummy extends PlayableEntity {
this.parent.plugins.remove('follow');
this.parent.snap(800,0);
}

public recordState() {
return {
...super.recordState(),
type: EntityType.TestDummy,
}
}
}
Loading

0 comments on commit 5d8829b

Please sign in to comment.