From c532485e588c2ea8e692e54811da75a59dee04fc Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 16 Jan 2025 16:19:16 +0000 Subject: [PATCH] More rxjsification --- src/entities/phys/bazookaShell.ts | 4 +- src/entities/phys/homingMissile.ts | 6 +- src/entities/phys/physicsEntity.ts | 9 +- src/entities/phys/timedExplosive.ts | 4 +- src/entities/playable/playable.ts | 44 ++-- src/entities/playable/testDummy.ts | 64 ++--- src/entities/playable/worm.ts | 27 +- src/frontend/components/menu.tsx | 2 +- src/frontend/components/menus/lobby.tsx | 218 +++++++++------- src/frontend/components/menus/online-play.tsx | 4 +- src/frontend/components/menus/team-editor.tsx | 22 +- src/game.ts | 1 + src/logic/gamestate.ts | 150 ++++++----- src/logic/teams.ts | 35 +-- src/logic/worminstance.ts | 35 ++- .../{bodyWireframe..ts => bodyWireframe.ts} | 0 src/net/client.ts | 18 +- src/net/models.ts | 2 +- src/net/netGameState.ts | 174 ++++++------- src/net/netfloat.ts | 57 +++-- src/overlays/debugOverlay.ts | 2 + src/overlays/gameStateOverlay.ts | 77 +++--- src/overlays/roundTimer.ts | 49 ++++ src/scenarios/netGame.ts | 64 +++-- src/scenarios/netGameTest.ts | 33 +-- src/scenarios/replayTesting.ts | 237 ------------------ src/scenarios/tiledMap.ts | 13 +- src/scenarios/uiTest.ts | 6 + src/settings.ts | 3 +- src/state/model.ts | 7 +- src/state/player.ts | 60 +++-- src/state/recorder.ts | 47 ++-- src/utils/coodinate.ts | 2 +- src/world.ts | 12 +- 34 files changed, 701 insertions(+), 787 deletions(-) rename src/mixins/{bodyWireframe..ts => bodyWireframe.ts} (100%) create mode 100644 src/overlays/roundTimer.ts delete mode 100644 src/scenarios/replayTesting.ts diff --git a/src/entities/phys/bazookaShell.ts b/src/entities/phys/bazookaShell.ts index 9726cbe..eee2215 100644 --- a/src/entities/phys/bazookaShell.ts +++ b/src/entities/phys/bazookaShell.ts @@ -25,9 +25,7 @@ export class BazookaShell extends TimedExplosive { BazookaShell.create( parent, gameWorld, - Coordinate.fromWorld( - new Vector2(state.tra.x, state.tra.y), - ), + Coordinate.fromWorld(new Vector2(state.tra.x, state.tra.y)), new Vector2(0, 0), ); } diff --git a/src/entities/phys/homingMissile.ts b/src/entities/phys/homingMissile.ts index bddbdfc..4d144c1 100644 --- a/src/entities/phys/homingMissile.ts +++ b/src/entities/phys/homingMissile.ts @@ -22,15 +22,15 @@ const ACTIVATION_TIME_MS = 65; const ADJUSTMENT_TIME_MS = 6; const forceMult = new Vector2(7, 7); -export interface HomingMissileRecordedState extends TimedExplosiveRecordedState { +export interface HomingMissileRecordedState + extends TimedExplosiveRecordedState { target: { x: number; y: number; - } + }; hasActivated: boolean; } - /** * Homing missile that attempts to hit a point target. */ diff --git a/src/entities/phys/physicsEntity.ts b/src/entities/phys/physicsEntity.ts index 1854c1c..537f349 100644 --- a/src/entities/phys/physicsEntity.ts +++ b/src/entities/phys/physicsEntity.ts @@ -1,7 +1,7 @@ import { UPDATE_PRIORITY, Sprite, Point } from "pixi.js"; import { IPhysicalEntity, OnDamageOpts } from "../entity"; import { Water } from "../water"; -import { BodyWireframe } from "../../mixins/bodyWireframe."; +import { BodyWireframe } from "../../mixins/bodyWireframe"; import globalFlags, { DebugLevel } from "../../flags"; import { IMediaInstance, Sound } from "@pixi/sound"; import { GameWorld, PIXELS_PER_METER, RapierPhysicsObject } from "../../world"; @@ -10,6 +10,7 @@ import { magnitude, MetersValue, mult, sub } from "../../utils"; import { AssetPack } from "../../assets"; import type { RecordedEntityState } from "../../state/model"; import { CameraLockPriority } from "../../camera"; +import { BehaviorSubject, distinct, Observable } from "rxjs"; /** * Abstract class for any physical object in the world. The @@ -49,6 +50,9 @@ export abstract class PhysicsEntity< return this.physObject.body; } + private readonly bodyMoving: BehaviorSubject; + public readonly bodyMoving$: Observable; + constructor( public readonly sprite: Sprite, protected physObject: RapierPhysicsObject, @@ -61,6 +65,8 @@ export abstract class PhysicsEntity< globalFlags.on("toggleDebugView", (level: DebugLevel) => { this.wireframe.enabled = level >= DebugLevel.BasicOverlay; }); + this.bodyMoving = new BehaviorSubject(false); + this.bodyMoving$ = this.bodyMoving.pipe(distinct()); } destroy(): void { @@ -73,6 +79,7 @@ export abstract class PhysicsEntity< } update(dt: number): void { + this.bodyMoving.next(this.body.isMoving()); const pos = this.physObject.body.translation(); const rotation = this.physObject.body.rotation() + this.rotationOffset; this.sprite.updateTransform({ diff --git a/src/entities/phys/timedExplosive.ts b/src/entities/phys/timedExplosive.ts index 7991b5d..d4beea2 100644 --- a/src/entities/phys/timedExplosive.ts +++ b/src/entities/phys/timedExplosive.ts @@ -36,7 +36,9 @@ export interface TimedExplosiveRecordedState extends RecordedEntityState { * Any projectile type that can explode after a set timer. Implementing classes * must include their own timer. */ -export abstract class TimedExplosive +export abstract class TimedExplosive< + T extends TimedExplosiveRecordedState = TimedExplosiveRecordedState, + > extends PhysicsEntity implements IWeaponEntity { diff --git a/src/entities/playable/playable.ts b/src/entities/playable/playable.ts index 8fc76fa..b5b4b06 100644 --- a/src/entities/playable/playable.ts +++ b/src/entities/playable/playable.ts @@ -17,9 +17,7 @@ import { Viewport } from "pixi-viewport"; import { handleDamageInRadius } from "../../utils/damage"; import { RecordedEntityState } from "../../state/model"; import { HEALTH_CHANGE_TENSION_TIMER } from "../../consts"; -import Logger from "../../log"; - -const logger = new Logger("Playable"); +import { first, skip, Subscription } from "rxjs"; interface Opts { explosionRadius: MetersValue; @@ -44,24 +42,14 @@ export abstract class PlayableEntity extends PhysicsEntity { protected healthTextBox: Graphics; private visibleHealth: number; + private healthTarget: number; private healthChangeTensionTimer: number | null = null; get position() { return this.physObject.body.translation(); } - get health() { - return this.wormIdent.health; - } - - set health(v: number) { - this.wormIdent.health = v; - logger.info( - `Worm (${this.wormIdent.uuid}, ${this.wormIdent.name}) health adjusted`, - ); - // Potentially further delay until the player has stopped moving. - this.healthChangeTensionTimer = HEALTH_CHANGE_TENSION_TIMER; - } + private readonly healthSub: Subscription; constructor( sprite: Sprite, @@ -82,15 +70,28 @@ export abstract class PlayableEntity extends PhysicsEntity { align: "center", }, }); + this.visibleHealth = -1; + this.healthTarget = -1; this.healthText = new Text({ - text: this.health, + text: this.visibleHealth, style: { ...DefaultTextStyle, fill: fg, align: "center", }, }); - this.visibleHealth = this.health; + + this.wormIdent.health$.pipe(first()).subscribe((h) => { + this.healthTarget = h; + this.visibleHealth = h; + this.healthText.text = h; + }); + + this.healthSub = this.wormIdent.health$.pipe(skip(1)).subscribe((h) => { + this.healthTarget = h; + // TODO: Potentially further delay until the player has stopped moving. + this.healthChangeTensionTimer = HEALTH_CHANGE_TENSION_TIMER; + }); this.nameText.position.set(0, -5); this.healthTextBox = new Graphics(); @@ -174,12 +175,12 @@ export abstract class PlayableEntity extends PhysicsEntity { // If the timer is null, decrease the rendered health if nessacery. if (this.healthChangeTensionTimer === null) { - if (this.visibleHealth > this.health) { + if (this.visibleHealth > this.healthTarget) { this.onHealthTensionTimerExpired(true); this.visibleHealth--; this.healthText.text = this.visibleHealth; this.setHealthTextPosition(); - if (this.visibleHealth <= this.health) { + if (this.visibleHealth <= this.healthTarget) { this.onHealthTensionTimerExpired(false); } } @@ -213,7 +214,7 @@ export abstract class PlayableEntity extends PhysicsEntity { ): boolean { if (super.onCollision(otherEnt, contactPoint)) { if (this.isSinking) { - this.wormIdent.health = 0; + this.wormIdent.setHealth(0); this.healthTextBox.destroy(); this.physObject.body.setRotation(DEG_TO_RAD * 180, false); } @@ -235,7 +236,7 @@ export abstract class PlayableEntity extends PhysicsEntity { opts.maxDamage ?? 100, Math.round((forceMag / 20) * this.opts.damageMultiplier), ); - this.health = Math.max(0, this.health - damage); + this.wormIdent.setHealth(this.wormIdent.health - damage); const force = mult( sub(point, bodyTranslation), new Vector2(-forceMag, -forceMag), @@ -251,6 +252,7 @@ export abstract class PlayableEntity extends PhysicsEntity { } public destroy(): void { + this.healthSub.unsubscribe(); super.destroy(); if (!this.healthTextBox.destroyed) { this.healthTextBox.destroy(); diff --git a/src/entities/playable/testDummy.ts b/src/entities/playable/testDummy.ts index 53c6183..7141263 100644 --- a/src/entities/playable/testDummy.ts +++ b/src/entities/playable/testDummy.ts @@ -1,4 +1,4 @@ -import { Sprite, Texture, UPDATE_PRIORITY } from "pixi.js"; +import { Sprite, Texture } from "pixi.js"; import { AssetPack } from "../../assets"; import { collisionGroupBitmask, @@ -16,6 +16,7 @@ import { WormInstance } from "../../logic/teams"; import { PlayableEntity } from "./playable"; import { Viewport } from "pixi-viewport"; import { EntityType } from "../type"; +import { combineLatest, Subscription } from "rxjs"; /** * Test dummy entity that may be associated with a worm identity. These @@ -43,7 +44,8 @@ export class TestDummy extends PlayableEntity { private static texture_damage_3: Texture; private static texture_damage_blush_3: Texture; - priority = UPDATE_PRIORITY.LOW; + private readonly textureSub: Subscription; + private static readonly collisionBitmask = collisionGroupBitmask( [CollisionGroups.WorldObjects], [CollisionGroups.Terrain, CollisionGroups.WorldObjects], @@ -87,40 +89,38 @@ export class TestDummy extends PlayableEntity { explosionRadius: new MetersValue(3), damageMultiplier: 250, }); - } - - private getTexture() { - const isBlush = this.health < 100 && this.physObject.body.isMoving(); - - if (this.health >= 80) { - return isBlush ? TestDummy.texture_blush : TestDummy.texture_normal; - } else if (this.health >= 60) { - return isBlush - ? TestDummy.texture_damage_blush_1 - : TestDummy.texture_damage_1; - } else if (this.health >= 25) { - return isBlush - ? TestDummy.texture_damage_blush_2 - : TestDummy.texture_damage_2; - } else { - return isBlush - ? TestDummy.texture_damage_blush_3 - : TestDummy.texture_damage_3; - } - } - - public update(dt: number): void { - const expectedTexture = this.getTexture(); - if (this.sprite.texture !== expectedTexture) { - this.sprite.texture = expectedTexture; - } - super.update(dt); + this.textureSub = combineLatest([ + this.bodyMoving$, + this.wormIdent.health$, + ]).subscribe(([moving, health]) => { + const isBlush = health < 100 && moving; + let expectedTexture; + if (health >= 80) { + expectedTexture = isBlush + ? TestDummy.texture_blush + : TestDummy.texture_normal; + } else if (health >= 60) { + expectedTexture = isBlush + ? TestDummy.texture_damage_blush_1 + : TestDummy.texture_damage_1; + } else if (health >= 25) { + expectedTexture = isBlush + ? TestDummy.texture_damage_blush_2 + : TestDummy.texture_damage_2; + } else { + expectedTexture = isBlush + ? TestDummy.texture_damage_blush_3 + : TestDummy.texture_damage_3; + } + if (this.sprite.texture !== expectedTexture) { + this.sprite.texture = expectedTexture; + } + }); } public destroy(): void { super.destroy(); - this.parent.plugins.remove("follow"); - this.parent.snap(800, 0); + this.textureSub.unsubscribe(); } public recordState() { diff --git a/src/entities/playable/worm.ts b/src/entities/playable/worm.ts index 6acb566..7c95241 100644 --- a/src/entities/playable/worm.ts +++ b/src/entities/playable/worm.ts @@ -50,6 +50,7 @@ import { CameraLockPriority } from "../../camera"; import { OnDamageOpts } from "../entity"; import Logger from "../../log"; import { WormState, InnerWormState } from "./wormState"; +import { filter, first } from "rxjs"; export enum EndTurnReason { TimerElapsed = 0, @@ -204,6 +205,21 @@ export class Worm extends PlayableEntity { }); this.targettingGfx = new Graphics({ visible: false }); this.updateTargettingGfx(); + this.wormIdent.health$ + .pipe( + filter((v) => v === 0), + first(), + ) + .subscribe(() => { + // Generic death + this.toaster?.pushToast( + templateRandomText(WormDeathGeneric, { + WormName: this.wormIdent.name, + TeamName: this.wormIdent.team.name, + }), + 3000, + ); + }); } public selectWeapon(weapon: IWeaponDefiniton) { @@ -715,7 +731,7 @@ export class Worm extends PlayableEntity { //this.body.setGravityScale(0, false); if (this.impactVelocity > Worm.minImpactForDamage) { const damage = this.impactVelocity * Worm.impactDamageMultiplier; - this.health -= damage; + this.wormIdent.setHealth(this.wormIdent.health - damage); this.state.transition(InnerWormState.Inactive); this.turnEndedReason = EndTurnReason.FallDamage; } @@ -767,15 +783,6 @@ export class Worm extends PlayableEntity { 3000, ); // Sinking death - } else if (this.health === 0) { - // Generic death - this.toaster?.pushToast( - templateRandomText(WormDeathGeneric, { - WormName: this.wormIdent.name, - TeamName: this.wormIdent.team.name, - }), - 3000, - ); } } } diff --git a/src/frontend/components/menu.tsx b/src/frontend/components/menu.tsx index 45515aa..85ee3d9 100644 --- a/src/frontend/components/menu.tsx +++ b/src/frontend/components/menu.tsx @@ -164,7 +164,7 @@ export function Menu({ } else if (currentMenu === GameMenu.Lobby) { const onOpenIngame = (gameInstance: RunningNetGameInstance) => { // TODO: Hardcoded level. - onNewGame("netGame", gameInstance, "levels_testing"); + onNewGame("netGameTest", gameInstance, "levels_testing"); }; if (!currentLobbyId) { throw Error("Current Lobby ID must be set!"); diff --git a/src/frontend/components/menus/lobby.tsx b/src/frontend/components/menus/lobby.tsx index ea19806..b854dea 100644 --- a/src/frontend/components/menus/lobby.tsx +++ b/src/frontend/components/menus/lobby.tsx @@ -41,20 +41,20 @@ export function TeamEntry({ const backgroundColor = `var(--team-${TeamGroup[team.group].toLocaleLowerCase()}-fg)`; return (
- - + + - - )) - ) : (storedLocalTeams.length === 0 ?

You have no teams

: null)} - -
-
-

In-play

-
    - {proposedTeams.map((t) => { - const canAlter = - gameInstance.isHost || - t.playerUserId === gameInstance.myUserId; - if (!canAlter) { - return ( -
  1. - -
  2. - ); - } - const onRemoveTeam = () => removeTeam(t); - const incrementWormCount = () => { - const wormCount = - t.wormCount >= MAX_WORMS ? 1 : t.wormCount + 1; - gameInstance.updateProposedTeam(t, { wormCount }); - }; - const changeTeamColor = () => { - let teamGroup = t.group + 1; - if (TeamGroup[teamGroup] === undefined) { - teamGroup = TeamGroup.Red; - } - gameInstance.updateProposedTeam(t, { teamGroup }); - }; - return ( -
  3. - -
  4. - ); - })} -
-
- -; + return ( +
+

Teams

+
+
+

Your teams

+
    + {localTeams.length > 0 ? ( + localTeams.map((t) => ( +
  1. + +
  2. + )) + ) : storedLocalTeams.length === 0 ? ( +

    You have no teams

    + ) : null} +
+
+
+

In-play

+
    + {proposedTeams.map((t) => { + const canAlter = + gameInstance.isHost || t.playerUserId === gameInstance.myUserId; + if (!canAlter) { + return ( +
  1. + +
  2. + ); + } + const onRemoveTeam = () => removeTeam(t); + const incrementWormCount = () => { + const wormCount = + t.wormCount >= MAX_WORMS ? 1 : t.wormCount + 1; + gameInstance.updateProposedTeam(t, { wormCount }); + }; + const changeTeamColor = () => { + let teamGroup = t.group + 1; + if (TeamGroup[teamGroup] === undefined) { + teamGroup = TeamGroup.Red; + } + gameInstance.updateProposedTeam(t, { teamGroup }); + }; + return ( +
  3. + +
  4. + ); + })} +
+
+
+
+ ); } export function ActiveLobby({ @@ -174,8 +198,6 @@ export function ActiveLobby({ onOpenIngame: () => void; exitToMenu: () => void; }) { - const [error, setError] = useState(); - const membersMap = useObservableEagerState(gameInstance.members); const members = useMemo( () => @@ -186,20 +208,26 @@ export function ActiveLobby({ ); const proposedTeams = useObservableEagerState(gameInstance.proposedTeams); - const viableToStart = useMemo(() => - gameInstance.isHost && - members.length >= 2 && - proposedTeams.length >= 2 && - Object.keys(proposedTeams.reduce>>((v, o) => ({ - ...v, - [o.group]: (v[o.group] ?? 0) + 1 - }), { })).length >= 2 - , [gameInstance, members, proposedTeams]); + const viableToStart = useMemo( + () => + gameInstance.isHost && + members.length >= 2 && + proposedTeams.length >= 2 && + Object.keys( + proposedTeams.reduce>>( + (v, o) => ({ + ...v, + [o.group]: (v[o.group] ?? 0) + 1, + }), + {}, + ), + ).length >= 2, + [gameInstance, members, proposedTeams], + ); const lobbyLink = `${window.location.origin}${window.location.pathname}?gameRoomId=${encodeURIComponent(gameInstance.roomId)}`; return ( <> - {error &&

{error}

}

This area is the staging area for a new networked game.

You can invite players by sending them a link to{" "} @@ -221,7 +249,7 @@ export function ActiveLobby({ - +

diff --git a/src/frontend/components/menus/team-editor.tsx b/src/frontend/components/menus/team-editor.tsx index 1eb2626..88325bd 100644 --- a/src/frontend/components/menus/team-editor.tsx +++ b/src/frontend/components/menus/team-editor.tsx @@ -196,19 +196,15 @@ export default function TeamEditorMenu() { lastModified: Date.now(), uuid: crypto.randomUUID(), } satisfies StoredTeam; - setLocalTeams((t: StoredTeam[]) => [ - ...t, - newTeam, - ]); - console.log( - { - name: newTeamName, - worms: Array.from({ length: MAX_WORM_NAMES }).map( - (_, i) => `Worm #${i + 1}`, - ), - lastModified: Date.now(), - uuid: crypto.randomUUID(), - } satisfies StoredTeam); + setLocalTeams((t: StoredTeam[]) => [...t, newTeam]); + console.log({ + name: newTeamName, + worms: Array.from({ length: MAX_WORM_NAMES }).map( + (_, i) => `Worm #${i + 1}`, + ), + lastModified: Date.now(), + uuid: crypto.randomUUID(), + } satisfies StoredTeam); setSelectedTeam(teamLength); }, [localTeams]); diff --git a/src/game.ts b/src/game.ts index 44163dd..113f51b 100644 --- a/src/game.ts +++ b/src/game.ts @@ -112,6 +112,7 @@ export class Game { this.pixiApp.ticker, this.pixiApp.stage, this.viewport, + undefined, ); this.pixiApp.stage.addChildAt(this.rapierGfx, 0); diff --git a/src/logic/gamestate.ts b/src/logic/gamestate.ts index 32561a9..7420ba3 100644 --- a/src/logic/gamestate.ts +++ b/src/logic/gamestate.ts @@ -1,9 +1,9 @@ -import { InternalTeam, Team, WormInstance } from "./teams"; -import type { StateRecordWormGameState } from "../state/model"; +import { TeamInstance, Team, WormInstance } from "./teams"; import Logger from "../log"; import { EntityType } from "../entities/type"; import { GameWorld } from "../world"; import { IWeaponCode } from "../weapons/weapon"; +import { BehaviorSubject, distinctUntilChanged, map, skip } from "rxjs"; export interface GameRules { roundDurationMs?: number; @@ -13,8 +13,6 @@ export interface GameRules { ammoSchema: Record; } -const logger = new Logger("GameState"); - export enum RoundState { WaitingToBegin = "waiting_to_begin", Preround = "preround", @@ -23,6 +21,10 @@ export enum RoundState { Finished = "finished", } +const PREROUND_TIMER_MS = 5000; + +const logger = new Logger("GameState"); + export class GameState { static getTeamMaxHealth(team: Team) { return team.worms.map((w) => w.maxHealth).reduce((a, b) => a + b); @@ -42,9 +44,12 @@ export class GameState { ); } - protected currentTeam?: InternalTeam; - protected readonly teams: Map; - protected nextTeamStack: InternalTeam[]; + protected currentTeam = new BehaviorSubject( + undefined, + ); + public readonly currentTeam$ = this.currentTeam.asObservable(); + protected readonly teams: Map; + protected nextTeamStack: TeamInstance[]; /** * Wind strength. Integer between -10 and 10. @@ -52,11 +57,16 @@ export class GameState { protected wind = 0; private readonly roundDurationMs: number; - protected remainingRoundTimeMs = 0; + protected remainingRoundTimeMs = new BehaviorSubject(0); + public readonly remainingRoundTimeSeconds$ = this.remainingRoundTimeMs.pipe( + map((v) => Math.ceil(v / 1000)), + distinctUntilChanged(), + ); private stateIteration = 0; - protected roundState: RoundState = RoundState.Finished; + protected roundState = new BehaviorSubject(RoundState.Finished); + public readonly roundState$ = this.roundState.asObservable(); public iterateRound() { const prev = this.stateIteration; @@ -69,15 +79,19 @@ export class GameState { } get remainingRoundTime() { - return this.remainingRoundTimeMs; + return this.remainingRoundTimeMs.value; } get isPreRound() { - return this.roundState === RoundState.Preround; + return this.roundState.value === RoundState.Preround; } + /** + * Use `this.currentTeam` + * @deprecated + */ get activeTeam() { - return this.currentTeam; + return this.currentTeam.value; } constructor( @@ -88,27 +102,35 @@ export class GameState { if (teams.length < 1) { throw Error("Must have at least one team"); } - this.teams = new Map(teams.map( - (team) => - [team.uuid, new InternalTeam(team)], - )); + this.teams = new Map( + teams.map((team) => { + const iTeam = new TeamInstance(team); + // N.B. Skip the first health update. + iTeam.health$.pipe(skip(1)).subscribe(() => this.iterateRound()); + return [team.uuid, iTeam]; + }), + ); + if (this.teams.size !== teams.length) { + throw Error("Team had duplicate uuid, cannot start"); + } this.nextTeamStack = [...this.teams.values()]; this.roundDurationMs = rules.roundDurationMs ?? 45000; } public pauseTimer() { - this.roundState = RoundState.Paused; + this.roundState.next(RoundState.Paused); this.iterateRound(); } public unpauseTimer() { - this.roundState = RoundState.Playing; + this.roundState.next(RoundState.Playing); this.iterateRound(); } public setTimer(milliseconds: number) { - this.remainingRoundTimeMs = milliseconds; - }s + logger.debug("setTimer", milliseconds); + this.remainingRoundTimeMs.next(milliseconds); + } public getTeams() { return [...this.teams.values()]; @@ -122,77 +144,87 @@ export class GameState { return this.stateIteration; } + /** + * @deprecated Use `this.roundState$` + */ public get paused() { - return this.roundState === RoundState.Paused; + return this.roundState.value === RoundState.Paused; } public markAsFinished() { - this.roundState = RoundState.Finished; + this.roundState.next(RoundState.Finished); this.recordGameStare(); } - public update(ticker: { deltaMS: number}) { + public update(ticker: { deltaMS: number }) { + const roundState = this.roundState.value; + let remainingRoundTimeMs = this.remainingRoundTimeMs.value; if ( - this.roundState === RoundState.Finished || - this.roundState === RoundState.Paused || - this.roundState === RoundState.WaitingToBegin + roundState === RoundState.Finished || + roundState === RoundState.Paused || + roundState === RoundState.WaitingToBegin ) { return; } - this.remainingRoundTimeMs = Math.max( - 0, - this.remainingRoundTimeMs - ticker.deltaMS, - ); - if (this.remainingRoundTimeMs) { + + remainingRoundTimeMs = Math.max(0, remainingRoundTimeMs - ticker.deltaMS); + this.remainingRoundTimeMs.next(remainingRoundTimeMs); + if (remainingRoundTimeMs) { return; } - if (this.isPreRound) { + if (roundState === RoundState.Preround) { this.playerMoved(); } else { - this.roundState = RoundState.Finished; + this.roundState.next(RoundState.Finished); this.recordGameStare(); } } public playerMoved() { - this.roundState = RoundState.Playing; - this.remainingRoundTimeMs = this.roundDurationMs; + this.roundState.next(RoundState.Playing); + logger.debug("playerMoved", this.roundDurationMs); + this.remainingRoundTimeMs.next(this.roundDurationMs); this.recordGameStare(); } public beginRound() { - if (this.roundState !== RoundState.WaitingToBegin) { - throw Error(`Expected round to be WaitingToBegin for advanceRound(), but got ${this.roundState}`); + if (this.roundState.value !== RoundState.WaitingToBegin) { + throw Error( + `Expected round to be WaitingToBegin for advanceRound(), but got ${this.roundState}`, + ); } - this.roundState = RoundState.Preround; - this.remainingRoundTimeMs = 5000; + this.roundState.next(RoundState.Preround); + logger.debug("beginRound", PREROUND_TIMER_MS); + this.remainingRoundTimeMs.next(PREROUND_TIMER_MS); this.recordGameStare(); } public advanceRound(): - | { nextTeam: InternalTeam; nextWorm: WormInstance } - | { winningTeams: InternalTeam[] } { - if (this.roundState !== RoundState.Finished) { - throw Error(`Expected round to be Finished for advanceRound(), but got ${this.roundState}`); + | { nextTeam: TeamInstance; nextWorm: WormInstance } + | { winningTeams: TeamInstance[] } { + if (this.roundState.value !== RoundState.Finished) { + throw Error( + `Expected round to be Finished for advanceRound(), but got ${this.roundState}`, + ); } logger.debug("Advancing round"); this.wind = Math.ceil(Math.random() * 20 - 11); - if (!this.currentTeam) { + if (!this.currentTeam.value) { const [firstTeam] = this.nextTeamStack.splice(0, 1); - this.currentTeam = firstTeam; + this.currentTeam.next(firstTeam); // 5 seconds preround this.stateIteration++; - this.roundState = RoundState.WaitingToBegin; + this.roundState.next(RoundState.WaitingToBegin); this.recordGameStare(); return { - nextTeam: this.currentTeam, + nextTeam: firstTeam, // Team *should* have at least one healthy worm. - nextWorm: this.currentTeam.popNextWorm(), + nextWorm: firstTeam.popNextWorm(), }; } - const previousTeam = this.currentTeam; + const previousTeam = this.currentTeam.value; this.nextTeamStack.push(previousTeam); for (let index = 0; index < this.nextTeamStack.length; index++) { @@ -200,9 +232,9 @@ export class GameState { if (nextTeam.group === previousTeam.group) { continue; } - if (nextTeam.worms.some((w) => w.health > 0)) { + if (nextTeam.health > 0) { this.nextTeamStack.splice(index, 1); - this.currentTeam = nextTeam; + this.currentTeam.next(nextTeam); } } if (this.rules.winWhenAllObjectsOfTypeDestroyed) { @@ -212,7 +244,7 @@ export class GameState { if (!hasEntityRemaining) { logger.debug("Game stopped because type of entity no longer exists"); return { - winningTeams: [this.currentTeam], + winningTeams: [previousTeam], }; } else { logger.debug( @@ -221,7 +253,7 @@ export class GameState { } } // We wrapped around. - if (this.currentTeam === previousTeam) { + if (this.currentTeam.value === previousTeam) { this.stateIteration++; if (this.rules.winWhenOneGroupRemains) { // All remaining teams are part of the same group @@ -229,7 +261,7 @@ export class GameState { return { winningTeams: this.getActiveTeams(), }; - } else if (this.currentTeam.health === 0) { + } else if (previousTeam.health === 0) { // This is a draw this.recordGameStare(); return { @@ -239,17 +271,17 @@ export class GameState { } this.stateIteration++; // 5 seconds preround - this.remainingRoundTimeMs = 0; - this.roundState = RoundState.WaitingToBegin; + this.remainingRoundTimeMs.next(0); + this.roundState.next(RoundState.WaitingToBegin); this.recordGameStare(); return { - nextTeam: this.currentTeam, + nextTeam: this.currentTeam.value, // We should have already validated that this team has healthy worms. - nextWorm: this.currentTeam.popNextWorm(), + nextWorm: this.currentTeam.value.popNextWorm(), }; } protected recordGameStare() { return; } -} \ No newline at end of file +} diff --git a/src/logic/teams.ts b/src/logic/teams.ts index f3f758e..2fc533f 100644 --- a/src/logic/teams.ts +++ b/src/logic/teams.ts @@ -9,6 +9,7 @@ export interface WormIdentity { import { IWeaponCode, IWeaponDefiniton } from "../weapons/weapon"; import { WormInstance } from "./worminstance"; import { getDefinitionForCode } from "../weapons"; +import { BehaviorSubject, combineLatest, first, map } from "rxjs"; export enum TeamGroup { Red, @@ -52,7 +53,7 @@ export function teamGroupToColorSet(group: TeamGroup): { } } -export class InternalTeam implements Team { +export class TeamInstance { public readonly worms: WormInstance[]; private nextWormStack: WormInstance[]; public readonly ammo: Team["ammo"]; @@ -66,15 +67,25 @@ export class InternalTeam implements Team { ]); } - constructor( - private readonly team: Team, - onHealthChange: () => void, - ) { - this.worms = team.worms.map( - (w) => new WormInstance(w, this, onHealthChange), - ); + // XXX: Stopgap until we can rxjs more things. + private healthSubject = new BehaviorSubject(0); + public readonly health$ = this.healthSubject.asObservable(); + public readonly maxHealth$ = this.healthSubject.pipe(first((v) => v !== 0)); + + /** + * @deprecated Stopgap, use health. + */ + public get health() { + return this.healthSubject.value; + } + + constructor(private readonly team: Team) { + this.worms = team.worms.map((w) => new WormInstance(w, this)); this.nextWormStack = [...this.worms]; this.ammo = { ...team.ammo }; + combineLatest(this.worms.map((w) => w.health$)) + .pipe(map((v) => v.reduce((p, c) => p + c))) + .subscribe((v) => this.healthSubject.next(v)); } get name() { @@ -97,14 +108,6 @@ export class InternalTeam implements Team { return this.team.flag; } - get health() { - return this.worms.map((w) => w.health).reduce((a, b) => a + b); - } - - get maxHealth() { - return this.worms.map((w) => w.maxHealth).reduce((a, b) => a + b); - } - public popNextWorm(): WormInstance { // Clear any dead worms this.nextWormStack = this.nextWormStack.filter((w) => w.health > 0); diff --git a/src/logic/worminstance.ts b/src/logic/worminstance.ts index 2744ac0..367fc16 100644 --- a/src/logic/worminstance.ts +++ b/src/logic/worminstance.ts @@ -1,5 +1,6 @@ +import { BehaviorSubject, distinct, Observable } from "rxjs"; import Logger from "../log"; -import { InternalTeam } from "./teams"; +import { TeamInstance } from "./teams"; const logger = new Logger("WormInstance"); @@ -15,12 +16,30 @@ export interface WormIdentity { */ export class WormInstance { public readonly uuid; + + private readonly healthSubject: BehaviorSubject; + public readonly health$: Observable; + + /** + * @deprecated Use `this.health`. + */ + public get health(): number { + return this.healthSubject.value; + } + constructor( private readonly identity: WormIdentity, - public readonly team: InternalTeam, - private readonly onHealthUpdated: () => void, + public readonly team: TeamInstance, ) { + this.identity = { ...identity }; this.uuid = identity.uuid ?? globalThis.crypto.randomUUID(); + this.healthSubject = new BehaviorSubject(this.identity.health); + this.health$ = this.healthSubject.pipe(distinct()); + this.health$.subscribe((health) => { + logger.debug( + `Worm (${this.uuid}, ${this.name}) health updated ${health}`, + ); + }); } get name() { @@ -31,13 +50,7 @@ export class WormInstance { return this.identity.maxHealth; } - get health() { - return this.identity.health; - } - - set health(health: number) { - logger.debug(`Worm (${this.uuid}, ${this.name}) health updated ${health}`); - this.identity.health = health; - this.onHealthUpdated(); + setHealth(health: number) { + this.healthSubject.next(health); } } diff --git a/src/mixins/bodyWireframe..ts b/src/mixins/bodyWireframe.ts similarity index 100% rename from src/mixins/bodyWireframe..ts rename to src/mixins/bodyWireframe.ts diff --git a/src/net/client.ts b/src/net/client.ts index a89c53a..2ad00de 100644 --- a/src/net/client.ts +++ b/src/net/client.ts @@ -185,11 +185,10 @@ export class NetGameInstance { proposedTeam.uuid, ); } - public async updateProposedTeam( proposedTeam: ProposedTeam, - updates: { wormCount?: number, teamGroup?: TeamGroup}, + updates: { wormCount?: number; teamGroup?: TeamGroup }, ) { await this.client.client.sendStateEvent( this.roomId, @@ -197,7 +196,9 @@ export class NetGameInstance { { ...proposedTeam, ...(updates.teamGroup !== undefined && { group: updates.teamGroup }), - ...(updates.wormCount !== undefined && { wormCount: updates.wormCount }) + ...(updates.wormCount !== undefined && { + wormCount: updates.wormCount, + }), }, proposedTeam.uuid, ); @@ -482,7 +483,6 @@ export class NetGameClient extends EventEmitter { } export class RunningNetGameInstance extends NetGameInstance { - private readonly _gameState: BehaviorSubject; public readonly gameState: Observable; public readonly player: MatrixStateReplay; @@ -528,13 +528,13 @@ export class RunningNetGameInstance extends NetGameInstance { writeAction(act: StateRecordLine) { const packet: Record = { - ts: toNetworkFloat(act.ts), - kind: act.kind, - index: act.index, - data: toNetObject(act.data), + ts: toNetworkFloat(act.ts), + kind: act.kind, + index: act.index, + data: toNetObject(act.data), }; return this.client.client.sendEvent(this.roomId, GameActionEventType, { - action: packet + action: packet, }); } diff --git a/src/net/models.ts b/src/net/models.ts index b29cb23..2c8476e 100644 --- a/src/net/models.ts +++ b/src/net/models.ts @@ -4,7 +4,7 @@ import { Team, TeamGroup } from "../logic/teams"; import { StoredTeam } from "../settings"; export interface EntityDescriptor { - pos: { x: number; y: number; } + pos: { x: number; y: number }; rot: number; } diff --git a/src/net/netGameState.ts b/src/net/netGameState.ts index 3fc5a58..c9eb453 100644 --- a/src/net/netGameState.ts +++ b/src/net/netGameState.ts @@ -5,99 +5,105 @@ import { Team } from "../logic/teams"; import { StateRecordWormGameState } from "../state/model"; export class NetGameState extends GameState { - get clientActive() { - return this.activeTeam?.playerUserId === this.myUserId; - } + get clientActive() { + return this.activeTeam?.playerUserId === this.myUserId; + } - get peekNextTeam() { - for (let index = 0; index < this.nextTeamStack.length; index++) { - const nextTeam = this.nextTeamStack[index]; - if (nextTeam.group === this.currentTeam?.group) { - continue; - } - if (nextTeam.worms.some((w) => w.health > 0)) { - return nextTeam; - } - } - return null; + get peekNextTeam() { + for (let index = 0; index < this.nextTeamStack.length; index++) { + const nextTeam = this.nextTeamStack[index]; + if (nextTeam.group === this.activeTeam?.group) { + continue; + } + if (nextTeam.worms.some((w) => w.health > 0)) { + return nextTeam; + } } + return null; + } - get peekNextPlayer() { - return this.peekNextTeam?.playerUserId; - } + get peekNextPlayer() { + return this.peekNextTeam?.playerUserId; + } - constructor(teams: Team[], world: GameWorld, rules: GameRules, private readonly recorder: StateRecorder, private readonly myUserId: string) { - super(teams, world, rules); - } + constructor( + teams: Team[], + world: GameWorld, + rules: GameRules, + private readonly recorder: StateRecorder, + private readonly myUserId: string, + ) { + super(teams, world, rules); + } - protected networkSelectNextTeam() { - if (!this.currentTeam) { - const [firstTeam] = this.nextTeamStack.splice(0, 1); - this.currentTeam = firstTeam; - return; - } - const previousTeam = this.currentTeam; - this.nextTeamStack.push(previousTeam); + protected networkSelectNextTeam() { + const previousTeam = this.currentTeam.value; + if (!previousTeam) { + const [firstTeam] = this.nextTeamStack.splice(0, 1); + this.currentTeam.next(firstTeam); + return; + } + this.nextTeamStack.push(previousTeam); - for (let index = 0; index < this.nextTeamStack.length; index++) { - const nextTeam = this.nextTeamStack[index]; - if (nextTeam.group === previousTeam.group) { - continue; - } - if (nextTeam.worms.some((w) => w.health > 0)) { - this.nextTeamStack.splice(index, 1); - this.currentTeam = nextTeam; - } - } + for (let index = 0; index < this.nextTeamStack.length; index++) { + const nextTeam = this.nextTeamStack[index]; + if (nextTeam.group === previousTeam.group) { + continue; + } + if (nextTeam.worms.some((w) => w.health > 0)) { + this.nextTeamStack.splice(index, 1); + this.currentTeam.next(nextTeam); + } } + } - public applyGameStateUpdate(stateUpdate: StateRecordWormGameState["data"]) { - let index = -1; - for (const teamData of stateUpdate.teams) { - index++; - const teamWormSet = this.teams.get(teamData.uuid)?.worms; - if (!teamWormSet) { - throw new Error(`Missing local team data for team ${teamData.uuid}`); - } - for (const wormData of teamData.worms) { - const foundWorm = teamWormSet.find((w) => w.uuid === wormData.uuid); - if (foundWorm) { - foundWorm.health = wormData.health; - } - } - } - if (this.roundState !== RoundState.Preround && stateUpdate.round_state === RoundState.Preround) { - this.remainingRoundTimeMs = 5000; - } - this.roundState = stateUpdate.round_state; - console.log("beep >", stateUpdate.round_state); - this.wind = stateUpdate.wind; - if (this.roundState === RoundState.WaitingToBegin) { - this.networkSelectNextTeam(); + public applyGameStateUpdate(stateUpdate: StateRecordWormGameState["data"]) { + for (const teamData of stateUpdate.teams) { + const teamWormSet = this.teams.get(teamData.uuid)?.worms; + if (!teamWormSet) { + throw new Error(`Missing local team data for team ${teamData.uuid}`); + } + for (const wormData of teamData.worms) { + const foundWorm = teamWormSet.find((w) => w.uuid === wormData.uuid); + if (foundWorm) { + foundWorm.setHealth(wormData.health); } + } } + if ( + this.roundState.value !== RoundState.Preround && + stateUpdate.round_state === RoundState.Preround + ) { + this.remainingRoundTimeMs.next(5000); + } + this.roundState.next(stateUpdate.round_state); + this.wind = stateUpdate.wind; + if (this.roundState.value === RoundState.WaitingToBegin) { + this.networkSelectNextTeam(); + } + } - protected recordGameStare() { - if (!this.clientActive) { - console.log("Not active client"); - return; - } - const iteration = this.iteration; - const teams = this.getTeams(); - this.recorder.recordGameState({ - round_state: this.roundState, - iteration: iteration, - wind: this.currentWind, - teams: teams.map((t) => ({ - uuid: t.uuid, - worms: t.worms.map((w) => ({ - uuid: w.uuid, - name: w.name, - health: w.health, - maxHealth: w.maxHealth, - })), - ammo: t.ammo, - })), - }); + protected recordGameStare() { + if (!this.clientActive) { + console.log("Not active client"); + return; } -} \ No newline at end of file + const iteration = this.iteration; + const teams = this.getTeams(); + this.recorder.recordGameState({ + round_state: this.roundState.value, + iteration: iteration, + wind: this.currentWind, + teams: teams.map((t) => ({ + uuid: t.uuid, + worms: t.worms.map((w) => ({ + uuid: w.uuid, + name: w.name, + health: w.health, + maxHealth: w.maxHealth, + })), + ammo: t.ammo, + })), + }); + } +} diff --git a/src/net/netfloat.ts b/src/net/netfloat.ts index da2679c..7aeae56 100644 --- a/src/net/netfloat.ts +++ b/src/net/netfloat.ts @@ -1,43 +1,58 @@ - /** - * Specific type to + * Specific type to */ -export type NetworkFloat = {nf: true, e: string}; +export type NetworkFloat = { nf: true; e: string }; export function toNetworkFloat(v: number): NetworkFloat { - return {nf: true, e: v.toExponential()}; + return { nf: true, e: v.toExponential() }; } export function fromNetworkFloat(v: NetworkFloat): number { - return Number(v.e); + return Number(v.e); } -export function toNetObject(o: Record): Record { - return Object.fromEntries(Object.entries(o).map<[string, unknown]>(([key,v]) => { - if (typeof v === "number" && !Number.isInteger(v)) { +export function toNetObject( + o: Record, +): Record { + return Object.fromEntries( + Object.entries(o).map<[string, unknown]>(([key, v]) => { + if (typeof v === "number" && !Number.isInteger(v)) { return [key, toNetworkFloat(v)]; - } else if (Array.isArray(v)) { - return [key, v.map(v2 => typeof v2 === "number" && !Number.isInteger(v2) ? toNetworkFloat(v2) : v2)]; - } else if (typeof v === "object") { + } else if (Array.isArray(v)) { + return [ + key, + v.map((v2) => + typeof v2 === "number" && !Number.isInteger(v2) + ? toNetworkFloat(v2) + : v2, + ), + ]; + } else if (typeof v === "object") { return [key, toNetObject(v as Record)]; - } - return [key, v]; - })); + } + return [key, v]; + }), + ); } export function fromNetObject(o: Record): unknown { - function isNF(v: unknown): v is NetworkFloat { - return (v !== null && typeof v === "object" && 'nf' in v && v.nf === true) || false; - } + function isNF(v: unknown): v is NetworkFloat { + return ( + (v !== null && typeof v === "object" && "nf" in v && v.nf === true) || + false + ); + } - return Object.fromEntries(Object.entries(o).map<[string, unknown|NetworkFloat]>(([key,v]) => { + return Object.fromEntries( + Object.entries(o).map<[string, unknown | NetworkFloat]>(([key, v]) => { if (isNF(v)) { return [key, fromNetworkFloat(v)]; } else if (Array.isArray(v)) { - return [key, v.map(v2 => isNF(v2) ? fromNetworkFloat(v2) : v2)]; + return [key, v.map((v2) => (isNF(v2) ? fromNetworkFloat(v2) : v2))]; } else if (typeof v === "object") { return [key, fromNetObject(v as Record)]; } return [key, v]; - })); -} \ No newline at end of file + }), + ); +} diff --git a/src/overlays/debugOverlay.ts b/src/overlays/debugOverlay.ts index 7a3ccd1..f91cebc 100644 --- a/src/overlays/debugOverlay.ts +++ b/src/overlays/debugOverlay.ts @@ -12,6 +12,7 @@ import { PIXELS_PER_METER } from "../world"; import { Viewport } from "pixi-viewport"; import { debugData } from "../movementController"; import { DefaultTextStyle } from "../mixins/styles"; +import { RunningNetGameInstance } from "../net/client"; const PHYSICS_SAMPLES = 60; const FRAME_SAMPLES = 60; @@ -33,6 +34,7 @@ export class GameDebugOverlay { private readonly ticker: Ticker, private readonly stage: Container, private readonly viewport: Viewport, + private readonly gameInstance?: RunningNetGameInstance, ) { this.text = new Text({ text: "", diff --git a/src/overlays/gameStateOverlay.ts b/src/overlays/gameStateOverlay.ts index d47981a..935054b 100644 --- a/src/overlays/gameStateOverlay.ts +++ b/src/overlays/gameStateOverlay.ts @@ -7,17 +7,15 @@ import { UPDATE_PRIORITY, } from "pixi.js"; import { GameState } from "../logic/gamestate"; -import { - applyGenericBoxStyle, - DefaultTextStyle, - LargeTextStyle, -} from "../mixins/styles"; +import { applyGenericBoxStyle, DefaultTextStyle } from "../mixins/styles"; import { teamGroupToColorSet } from "../logic/teams"; import { GameWorld } from "../world"; import { Toaster } from "./toaster"; import { WindDial } from "./windDial"; import { HEALTH_CHANGE_TENSION_TIMER } from "../consts"; import Logger from "../log"; +import { combineLatest, first, map } from "rxjs"; +import { RoundTimer } from "./roundTimer"; const logger = new Logger("GameStateOverlay"); @@ -25,18 +23,17 @@ const TEAM_HEALTH_WIDTH_PX = 204; export class GameStateOverlay { public readonly physicsSamples: number[] = []; - private readonly roundTimer: Text; private readonly tickerFn: (dt: Ticker) => void; private readonly gfx: Graphics; private previousStateIteration = -1; private visibleTeamHealth: Record = {}; private healthChangeTensionTimer: number | null = null; - private readonly largestHealthPool: number; + private largestHealthPool = 0; public readonly toaster: Toaster; private readonly winddial: WindDial; + private readonly roundTimer: RoundTimer; private readonly bottomOfScreenY; - private readonly roundTimerWidth; private readonly teamFlagTextures: Record = {}; constructor( @@ -47,14 +44,6 @@ export class GameStateOverlay { private readonly screenWidth: number, private readonly screenHeight: number, ) { - this.roundTimer = new Text({ - text: "00", - style: { - ...LargeTextStyle, - align: "center", - }, - }); - this.roundTimerWidth = this.roundTimer.width; this.bottomOfScreenY = (this.screenHeight / 10) * 8.75; this.toaster = new Toaster(screenWidth, screenHeight); @@ -63,29 +52,36 @@ export class GameStateOverlay { this.bottomOfScreenY, this.gameWorld, ); - - this.roundTimer.position.set( + this.roundTimer = new RoundTimer( this.screenWidth / 30 + 14, this.bottomOfScreenY + 12, + this.gameState.remainingRoundTimeSeconds$, + this.gameState.currentTeam$.pipe( + map((t) => t && teamGroupToColorSet(t.group)), + ), ); + this.gfx = new Graphics(); this.stage.addChild(this.toaster.container); this.stage.addChild(this.gfx); - this.stage.addChild(this.roundTimer); + this.stage.addChild(this.roundTimer.container); this.stage.addChild(this.winddial.container); this.tickerFn = this.update.bind(this); this.ticker.add(this.tickerFn, undefined, UPDATE_PRIORITY.UTILITY); - this.largestHealthPool = this.gameState - .getTeams() - .reduceRight((value, team) => Math.max(value, team.maxHealth), 0); + combineLatest(this.gameState.getTeams().map((t) => t.maxHealth$)) + .pipe( + map((v) => v.reduce((v1, v2) => Math.max(v1, v2))), + first(), + ) + .subscribe((s) => { + this.largestHealthPool = s; + }); this.gameState.getActiveTeams().forEach((t) => { - this.visibleTeamHealth[t.name] = t.health; if (t.flag) { this.teamFlagTextures[t.name] = Texture.from( `team-flag-${t.name}`, true, ); - console.log(this.teamFlagTextures); } }); } @@ -102,11 +98,6 @@ export class GameStateOverlay { this.healthChangeTensionTimer !== null && this.healthChangeTensionTimer <= 0; - this.roundTimer.text = - this.gameState.remainingRoundTime === 0 - ? "--" - : Math.ceil(this.gameState.remainingRoundTime / 1000); - if ( this.previousStateIteration === this.gameState.iteration && !shouldChangeTeamHealth @@ -135,23 +126,6 @@ export class GameStateOverlay { // Remove any previous text. this.gfx.removeChildren(0, this.gfx.children.length); - const currentTeamColors = - !this.gameState.paused && this.gameState.activeTeam - ? teamGroupToColorSet(this.gameState.activeTeam?.group) - : { fg: 0xaaaaaa }; - - // Round timer - applyGenericBoxStyle(this.gfx, currentTeamColors.fg) - .roundRect( - this.roundTimer.x - 8, - this.roundTimer.y + 8, - this.roundTimerWidth + 16, - this.roundTimer.height, - 4, - ) - .stroke() - .fill(); - // For each team: // TODO: Sort by health and group // TODO: Evenly space. @@ -162,15 +136,20 @@ export class GameStateOverlay { this.bottomOfScreenY - (teamSeperationHeight * (activeTeams.length - 2)) / 2; for (const team of activeTeams) { + console.log(team.name, team.health); + if (this.visibleTeamHealth[team.uuid] === undefined) { + this.visibleTeamHealth[team.uuid] = team.health; + } if ( - this.visibleTeamHealth[team.name] > team.health && + this.visibleTeamHealth[team.uuid] > team.health && shouldChangeTeamHealth ) { - this.visibleTeamHealth[team.name] -= 1; + this.visibleTeamHealth[team.uuid] -= 1; allHealthAccurate = false; } const teamHealthPercentage = - this.visibleTeamHealth[team.name] / this.largestHealthPool; + this.visibleTeamHealth[team.uuid] / this.largestHealthPool; + const { bg, fg } = teamGroupToColorSet(team.group); const nameTag = new Text({ text: team.name, diff --git a/src/overlays/roundTimer.ts b/src/overlays/roundTimer.ts new file mode 100644 index 0000000..e158d7a --- /dev/null +++ b/src/overlays/roundTimer.ts @@ -0,0 +1,49 @@ +import { ColorSource, Container, Graphics, Text } from "pixi.js"; +import { applyGenericBoxStyle, LargeTextStyle } from "../mixins/styles"; +import { Observable } from "rxjs"; + +/** + * Displays a round timer duing gameplay. + */ +export class RoundTimer { + private readonly gfx: Graphics; + public readonly container: Container; + + constructor( + x: number, + y: number, + private readonly roundTimeRemaining: Observable, + private readonly currentTeamColors: Observable< + { bg: ColorSource; fg: ColorSource } | undefined + >, + ) { + this.gfx = new Graphics({}); + const text = new Text({ + text: "00", + style: { + ...LargeTextStyle, + align: "center", + }, + }); + this.container = new Container({ + x, + y, + }); + this.container.addChild(this.gfx); + this.container.addChild(text); + + this.roundTimeRemaining.subscribe((timeSeconds) => { + console.log("Timer time", timeSeconds); + text.text = timeSeconds === 0 ? "--" : timeSeconds; + }); + + this.currentTeamColors.subscribe((color) => { + this.gfx.clear(); + // Round timer + applyGenericBoxStyle(this.gfx, color?.fg) + .roundRect(-8, 8, text.width + 16, text.height, 4) + .stroke() + .fill(); + }); + } +} diff --git a/src/scenarios/netGame.ts b/src/scenarios/netGame.ts index c53da3e..93c7426 100644 --- a/src/scenarios/netGame.ts +++ b/src/scenarios/netGame.ts @@ -5,7 +5,6 @@ import type { Game } from "../game"; import { Water } from "../entities/water"; import { FireFn, Worm } from "../entities/playable/worm"; import { Coordinate, MetersValue } from "../utils/coodinate"; -import { GameState } from "../logic/gamestate"; import { GameStateOverlay } from "../overlays/gameStateOverlay"; import { GameDrawText, @@ -186,7 +185,8 @@ export default async function runScenario(game: Game) { game.viewport.screenHeight, ); - const waterLevel = level.objects.find((v) => v.type === "wormgine.water")?.tra.y ?? 0; + const waterLevel = + level.objects.find((v) => v.type === "wormgine.water")?.tra.y ?? 0; const water = world.addEntity( new Water( @@ -208,10 +208,7 @@ export default async function runScenario(game: Game) { if (levelObject.type === "wormgine.target") { const t = new WeaponTarget( world, - Coordinate.fromScreen( - levelObject.tra.x, - levelObject.tra.y, - ), + Coordinate.fromScreen(levelObject.tra.x, levelObject.tra.y), parent, ); world.addEntity(t); @@ -235,10 +232,7 @@ export default async function runScenario(game: Game) { throw Error("No location to spawn worm"); } const nextLocation = spawnPositions[nextLocationIdx]; - const pos = Coordinate.fromScreen( - nextLocation.tra.x, - nextLocation.tra.y, - ); + const pos = Coordinate.fromScreen(nextLocation.tra.x, nextLocation.tra.y); const fireFn: FireFn = async (worm, definition, opts) => { const newProjectile = definition.fireFn(parent, world, worm, opts); if (newProjectile instanceof PhysicsEntity) { @@ -422,30 +416,30 @@ export default async function runScenario(game: Game) { } endOfRoundWaitDuration -= dt.deltaMS; }); - player.on("gameState", (dataUpdate) => { - const nextState = gameState.applyGameStateUpdate(dataUpdate); - logger.info("New game state", dataUpdate, nextState); - if ("winningTeams" in nextState) { - if (nextState.winningTeams.length) { - overlay.toaster.pushToast( - templateRandomText(TeamWinnerText, { - TeamName: nextState.winningTeams.map((t) => t.name).join(", "), - }), - 8000, - ); - } else { - // Draw - overlay.toaster.pushToast(templateRandomText(GameDrawText), 8000); - } - endOfGameFadeOut = 8000; - } else { - currentWorm?.onEndOfTurn(); - currentWorm?.currentState.off("transition", transitionHandler); - currentWorm = wormInstances.get(nextState.nextWorm.uuid); - // Turn just ended. - endOfRoundWaitDuration = 5000; - } - return; - }); + // player.on("gameState", (dataUpdate) => { + // const nextState = gameState.applyGameStateUpdate(dataUpdate); + // logger.info("New game state", dataUpdate, nextState); + // if ("winningTeams" in nextState) { + // if (nextState.winningTeams.length) { + // overlay.toaster.pushToast( + // templateRandomText(TeamWinnerText, { + // TeamName: nextState.winningTeams.map((t) => t.name).join(", "), + // }), + // 8000, + // ); + // } else { + // // Draw + // overlay.toaster.pushToast(templateRandomText(GameDrawText), 8000); + // } + // endOfGameFadeOut = 8000; + // } else { + // currentWorm?.onEndOfTurn(); + // currentWorm?.currentState.off("transition", transitionHandler); + // currentWorm = wormInstances.get(nextState.nextWorm.uuid); + // // Turn just ended. + // endOfRoundWaitDuration = 5000; + // } + // return; + // }); } } diff --git a/src/scenarios/netGameTest.ts b/src/scenarios/netGameTest.ts index 2a9b94b..1f22baa 100644 --- a/src/scenarios/netGameTest.ts +++ b/src/scenarios/netGameTest.ts @@ -1,30 +1,17 @@ -import { Assets, Ticker } from "pixi.js"; +import { Assets } from "pixi.js"; import { Background } from "../entities/background"; import { BitmapTerrain } from "../entities/bitmapTerrain"; import type { Game } from "../game"; import { Water } from "../entities/water"; -import { FireFn, Worm } from "../entities/playable/worm"; import { Coordinate, MetersValue } from "../utils/coodinate"; -import { GameState } from "../logic/gamestate"; import { GameStateOverlay } from "../overlays/gameStateOverlay"; -import { - GameDrawText, - TeamWinnerText, - templateRandomText, -} from "../text/toasts"; -import { PhysicsEntity } from "../entities/phys/physicsEntity"; -import staticController, { InputKind } from "../input"; import { StateRecorder } from "../state/recorder"; import { CameraLockPriority, ViewportCamera } from "../camera"; import { getAssets } from "../assets"; import { scenarioParser } from "../levels/scenarioParser"; import { WeaponTarget } from "../entities/phys/target"; -import { WormSpawnRecordedState } from "../entities/state/wormSpawn"; -import { InnerWormState } from "../entities/playable/wormState"; import Logger from "../log"; -import { RemoteWorm } from "../entities/playable/remoteWorm"; import { logger } from "matrix-js-sdk/lib/logger"; -import { getDefinitionForCode } from "../weapons"; import { NetGameState } from "../net/netGameState"; const log = new Logger("scenario"); @@ -97,7 +84,7 @@ export default async function runScenario(game: Game) { world.addEntity(terrain); terrain.addToWorld(parent); - const overlay = new GameStateOverlay( + new GameStateOverlay( game.pixiApp.ticker, game.pixiApp.stage, gameState, @@ -106,7 +93,8 @@ export default async function runScenario(game: Game) { game.viewport.screenHeight, ); - const waterLevel = level.objects.find((v) => v.type === "wormgine.water")?.tra.y ?? 0; + const waterLevel = + level.objects.find((v) => v.type === "wormgine.water")?.tra.y ?? 0; const water = world.addEntity( new Water( @@ -128,19 +116,16 @@ export default async function runScenario(game: Game) { if (levelObject.type === "wormgine.target") { const t = new WeaponTarget( world, - Coordinate.fromScreen( - levelObject.tra.x, - levelObject.tra.y, - ), + Coordinate.fromScreen(levelObject.tra.x, levelObject.tra.y), parent, ); world.addEntity(t); parent.addChild(t.sprite); } } - - player.on('gameState', (s) => { - log.info('New game state recieved:', s.iteration); + + player.on("gameState", (s) => { + log.info("New game state recieved:", s.iteration); gameState.applyGameStateUpdate(s); }); @@ -164,7 +149,7 @@ export default async function runScenario(game: Game) { gameState.markAsFinished(); gameState.advanceRound(); gameState.beginRound(); - }, 3000); + }, 3000); } log.info("Game can now begin"); diff --git a/src/scenarios/replayTesting.ts b/src/scenarios/replayTesting.ts deleted file mode 100644 index 95db06e..0000000 --- a/src/scenarios/replayTesting.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { Assets, Ticker } from "pixi.js"; -import { Background } from "../entities/background"; -import { BitmapTerrain } from "../entities/bitmapTerrain"; -import type { Game } from "../game"; -import { Water } from "../entities/water"; -import { Worm } from "../entities/playable/worm"; -import { Coordinate, MetersValue } from "../utils/coodinate"; -import { GameState } from "../logic/gamestate"; -import { GameStateOverlay } from "../overlays/gameStateOverlay"; -import { - GameDrawText, - TeamWinnerText, - templateRandomText, -} from "../text/toasts"; -import { PhysicsEntity } from "../entities/phys/physicsEntity"; -import { TextStateReplay } from "../state/player"; -import { RemoteWorm } from "../entities/playable/remoteWorm"; -import { EntityType } from "../entities/type"; -import { getDefinitionForCode } from "../weapons"; -import { DefaultWeaponSchema } from "../weapons/schema"; - -export default async function runScenario(game: Game) { - const parent = game.viewport; - const world = game.world; - const { worldWidth, worldHeight } = game.viewport; - - const terrain = BitmapTerrain.create(game.world, Assets.get("testingGround")); - - const player = new TextStateReplay(replayData); - player.on("started", () => { - console.log("started playback"); - }); - - const dataPromise = player.waitForFullGameState(); - player.play(); - - const initialData = await dataPromise; - - const gameState = new GameState(initialData.gameState.teams, world, { - // TODO: Rules. - winWhenOneGroupRemains: true, - wormHealth: 100, - ammoSchema: DefaultWeaponSchema, - }); - - // TODO: Background - const bg = await world.addEntity( - Background.create( - game.viewport.screenWidth, - game.viewport.screenHeight, - game.viewport, - [20, 21, 50, 35], - terrain, - world, - ), - ); - bg.addToWorld(game.pixiApp.stage, parent); - await world.addEntity(terrain); - bg.addToWorld(game.pixiApp.stage, parent); - terrain.addToWorld(parent); - - const overlay = new GameStateOverlay( - game.pixiApp.ticker, - game.pixiApp.stage, - gameState, - world, - game.viewport.screenWidth, - game.viewport.screenHeight, - ); - - const water = world.addEntity( - new Water( - MetersValue.fromPixels(worldWidth * 4), - MetersValue.fromPixels(worldHeight), - world, - ), - ); - water.addToWorld(parent, world); - - const wormInstances = new Map(); - - player.on("wormAction", (wormAction) => { - const wormInst = wormInstances.get(wormAction.id); - if (!wormInst) { - throw Error("Worm not found"); - } - wormInst.replayWormAction(wormAction.action); - }); - - player.on("wormSelectWeapon", (wormWeapon) => { - const wormInst = wormInstances.get(wormWeapon.id); - if (!wormInst) { - throw Error("Worm not found"); - } - wormInst.selectWeapon(getDefinitionForCode(wormWeapon.weapon)); - }); - - player.on("wormActionAim", ({ id, dir, angle }) => { - const wormInst = wormInstances.get(id); - if (!wormInst) { - throw Error("Worm not found"); - } - wormInst.replayAim(dir, parseFloat(angle)); - }); - - player.on("wormActionMove", ({ id, action, cycles }) => { - const wormInst = wormInstances.get(id); - if (!wormInst) { - throw Error("Worm not found"); - } - wormInst.replayMovement(action, cycles); - }); - - player.on("wormActionFire", ({ id, duration }) => { - const wormInst = wormInstances.get(id); - if (!wormInst) { - throw Error("Worm not found"); - } - wormInst.replayFire(duration); - }); - - const worms = initialData.entitySync - .filter((v) => v.type === EntityType.Worm) - .reverse(); - - for (const team of gameState.getActiveTeams()) { - for (const wormInstance of team.worms) { - const existingEntData = worms.pop()!; - console.log("existing worm data", existingEntData); - const wormEnt = world.addEntity( - await RemoteWorm.create( - parent, - world, - new Coordinate(existingEntData.tra.x, existingEntData.tra.y), - wormInstance, - async (worm, definition, opts) => { - const newProjectile = definition.fireFn(parent, world, worm, opts); - if (newProjectile instanceof PhysicsEntity) { - parent.follow(newProjectile.sprite); - world.addEntity(newProjectile); - } - applyEntityData(); - const res = await newProjectile.onFireResult; - if (newProjectile instanceof PhysicsEntity) { - parent.follow(worm.sprite); - } - applyEntityData(); - return res; - }, - overlay.toaster, - ), - existingEntData.uuid, - ); - wormInstances.set(wormInstance.uuid, wormEnt); - } - } - - let endOfRoundWaitDuration: number | null = null; - let endOfGameFadeOut: number | null = null; - let currentWorm: Worm | undefined; - - function applyEntityData() { - console.log("Applying entity state data"); - for (const ent of player.latestEntityData) { - const existingEnt = world.entities.get(ent.uuid); - if (!existingEnt) { - throw new Error( - `Ent ${ent.uuid} ${ent.type} was not found during entity sync`, - ); - } else if (existingEnt instanceof PhysicsEntity === false) { - throw new Error( - `Ent ${ent.uuid} ${ent.type} was unexpectedly not a PhysicsEntity`, - ); - } - existingEnt.loadState(ent); - } - } - - player.on("gameState", (dataUpdate) => { - const nextState = gameState.applyGameStateUpdate(dataUpdate); - applyEntityData(); - console.log("advancing round", nextState); - if ("winningTeams" in nextState) { - if (nextState.winningTeams.length) { - overlay.toaster.pushToast( - templateRandomText(TeamWinnerText, { - TeamName: nextState.winningTeams.map((t) => t.name).join(", "), - }), - 8000, - ); - } else { - // Draw - overlay.toaster.pushToast(templateRandomText(GameDrawText), 8000); - } - endOfGameFadeOut = 8000; - } else { - currentWorm?.onEndOfTurn(); - currentWorm = wormInstances.get(nextState.nextWorm.uuid); - // Turn just ended. - endOfRoundWaitDuration = 5000; - } - }); - - const roundHandlerFn = (dt: Ticker) => { - if (endOfGameFadeOut !== null) { - endOfGameFadeOut -= dt.deltaMS; - if (endOfGameFadeOut < 0) { - game.pixiApp.ticker.remove(roundHandlerFn); - game.goToMenu(gameState.getActiveTeams()); - } - return; - } - if (currentWorm && currentWorm.currentState.active) { - return; - } - if (endOfRoundWaitDuration === null) { - return; - } - if (endOfRoundWaitDuration <= 0) { - if (!currentWorm) { - throw Error("Expected next worm"); - } - world.setWind(gameState.currentWind); - currentWorm.onWormSelected(); - game.viewport.follow(currentWorm.sprite); - endOfRoundWaitDuration = null; - return; - } - endOfRoundWaitDuration -= dt.deltaMS; - }; - game.pixiApp.ticker.add(roundHandlerFn); -} - -const replayData = - `{"index":0,"data":{"version":2},"kind":0,"ts":"3939.836"}|{"index":1,"data":{"iteration":0,"wind":0,"teams":[{"name":"The Prawns","group":0,"playerUserId":null,"worms":[{"uuid":"9a3919e0-e756-428e-9ccb-094b4b012183","name":"Shrimp","health":100,"maxHealth":100}]},{"name":"The Whales","group":1,"playerUserId":null,"worms":[{"uuid":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","name":"Welsh boy","health":100,"maxHealth":100}]}]},"kind":7,"ts":"3946.394"}|{"index":2,"data":{"entities":[{"uuid":"55939cbf-e318-4a06-aa10-e9a228892a5a","type":0,"tra":{"x":"45","y":"5.25"},"rot":"0","vel":{"x":"0","y":"0"},"wormIdent":"9a3919e0-e756-428e-9ccb-094b4b012183"},{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"55","y":"5.25"},"rot":"0","vel":{"x":"0","y":"0"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"}]},"kind":1,"ts":"3946.562"}|{"index":3,"data":{"entities":[{"uuid":"55939cbf-e318-4a06-aa10-e9a228892a5a","type":0,"tra":{"x":"45","y":"5.3399248123168945"},"rot":"0","vel":{"x":"0","y":"1.3079999685287476"},"wormIdent":"9a3919e0-e756-428e-9ccb-094b4b012183"},{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"55","y":"5.3399248123168945"},"rot":"0","vel":{"x":"0","y":"1.3079999685287476"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"}]},"kind":1,"ts":"3951.805"}|{"index":4,"data":{"iteration":0,"wind":3,"teams":[{"name":"The Prawns","group":0,"playerUserId":null,"worms":[{"uuid":"9a3919e0-e756-428e-9ccb-094b4b012183","name":"Shrimp","health":100,"maxHealth":100}]},{"name":"The Whales","group":1,"playerUserId":null,"worms":[{"uuid":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","name":"Welsh boy","health":100,"maxHealth":100}]}]},"kind":7,"ts":"3951.903"}|{"index":5,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":100,"action":0},"kind":3,"ts":"13566.692"}|{"index":6,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"3.6415926535897936","dir":"up","action":2},"kind":4,"ts":"13857.656"}|{"index":7,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":24,"action":1},"kind":3,"ts":"14179.635"}|{"index":8,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"0.12000000000000001","dir":"down","action":2},"kind":4,"ts":"16613.708"}|{"index":9,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"5.323185307179607","dir":"up","action":2},"kind":4,"ts":"17098.375"}|{"index":10,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"5.983185307179593","dir":"down","action":2},"kind":4,"ts":"17519.758"}|{"index":11,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"0.02","dir":"down","action":2},"kind":4,"ts":"17839.937"}|{"index":12,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"5.863185307179595","dir":"up","action":2},"kind":4,"ts":"18100.703"}|{"index":13,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","weapon":0},"kind":6,"ts":"18139.988"}|{"index":14,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"5.16318530717961","dir":"up","action":2},"kind":4,"ts":"18615.055"}|{"index":15,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","duration":27.959999999999955,"action":3},"kind":5,"ts":"19278.808"}|{"index":16,"data":{"entities":[{"uuid":"55939cbf-e318-4a06-aa10-e9a228892a5a","type":0,"tra":{"x":"43.098121643066406","y":"6.4760661125183105"},"rot":"0","vel":{"x":"0","y":"0"},"wormIdent":"9a3919e0-e756-428e-9ccb-094b4b012183"},{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"55","y":"6.476071834564209"},"rot":"0","vel":{"x":"0","y":"0"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"},{"uuid":"cdff34b1-cdbe-4be3-82dc-03fe11b6c572","timer":180,"owner":"9a3919e0-e756-428e-9ccb-094b4b012183","timerSecs":3,"type":1,"tra":{"x":"44.62083053588867","y":"6.176065921783447"},"rot":"0","vel":{"x":"5.321871757507324","y":"-10.994749069213867"}}]},"kind":1,"ts":"19280.034"}|{"index":17,"data":{"entities":[]},"kind":1,"ts":"22336.575"}|{"index":18,"data":{"iteration":1,"wind":2,"teams":[{"name":"The Prawns","group":0,"playerUserId":null,"worms":[{"uuid":"9a3919e0-e756-428e-9ccb-094b4b012183","name":"Shrimp","health":100,"maxHealth":100}]},{"name":"The Whales","group":1,"playerUserId":null,"worms":[{"uuid":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","name":"Welsh boy","health":100,"maxHealth":100}]}]},"kind":7,"ts":"22336.731"}|{"index":19,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","cycles":19,"action":0},"kind":3,"ts":"28405.955"}|{"index":20,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","angle":"3.3815926535897933","dir":"up","action":2},"kind":4,"ts":"28954.054"}|{"index":21,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","duration":29.039999999999953,"action":3},"kind":5,"ts":"31201.008"}|{"index":22,"data":{"entities":[{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"54.526031494140625","y":"6.4760661125183105"},"rot":"0","vel":{"x":"0","y":"0"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"},{"uuid":"0f6d3da6-fb15-443f-b2c5-8ad51ece502d","timer":1800,"owner":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","timerSecs":30,"type":2,"tra":{"x":"51.41943359375","y":"5.9760661125183105"},"rot":"0","vel":{"x":"-14.90359878540039","y":"-2.4314396381378174"}}]},"kind":1,"ts":"31202.144"}|{"index":23,"data":{"entities":[{"uuid":"55939cbf-e318-4a06-aa10-e9a228892a5a","type":0,"tra":{"x":"43.006202697753906","y":"6.541950702667236"},"rot":"0","vel":{"x":"-1.3786027431488037","y":"1.2948399782180786"},"wormIdent":"9a3919e0-e756-428e-9ccb-094b4b012183"}]},"kind":1,"ts":"31610.144"}|{"index":24,"data":{"iteration":3,"wind":-4,"teams":[{"name":"The Prawns","group":0,"playerUserId":null,"worms":[{"uuid":"9a3919e0-e756-428e-9ccb-094b4b012183","name":"Shrimp","health":69,"maxHealth":100}]},{"name":"The Whales","group":1,"playerUserId":null,"worms":[{"uuid":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","name":"Welsh boy","health":100,"maxHealth":100}]}]},"kind":7,"ts":"31610.292"}|{"index":25,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":53,"action":1},"kind":3,"ts":"37177.894"}|{"index":26,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":0,"action":1},"kind":3,"ts":"37269.168"}|{"index":27,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":0,"action":1},"kind":3,"ts":"37732.093"}|{"index":28,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":66,"action":0},"kind":3,"ts":"38115.122"}|{"index":29,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":66,"action":0},"kind":3,"ts":"38415.036"}|{"index":30,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":0,"action":1},"kind":3,"ts":"38433.039"}|{"index":31,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":0,"action":1},"kind":3,"ts":"38437.593"}|{"index":32,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","action":4},"kind":2,"ts":"38626.191"}|{"index":33,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":216,"action":0},"kind":3,"ts":"40563.095"}|{"index":34,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":11,"action":1},"kind":3,"ts":"40810.796"}|{"index":35,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"5.563185307179602","dir":"down","action":2},"kind":4,"ts":"41470.206"}|{"index":36,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"5.123185307179611","dir":"up","action":2},"kind":4,"ts":"43925.224"}|{"index":37,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","duration":28.49999999999995,"action":3},"kind":5,"ts":"44867.26"}|{"index":38,"data":{"entities":[{"uuid":"55939cbf-e318-4a06-aa10-e9a228892a5a","type":0,"tra":{"x":"31.91230010986328","y":"6.4760661125183105"},"rot":"0","vel":{"x":"0","y":"0"},"wormIdent":"9a3919e0-e756-428e-9ccb-094b4b012183"},{"uuid":"b85b4a20-26cf-45ae-98cb-16e81a9dbf89","timer":240,"owner":"9a3919e0-e756-428e-9ccb-094b4b012183","timerSecs":4,"type":1,"tra":{"x":"33.33494567871094","y":"6.176065921783447"},"rot":"0","vel":{"x":"5.068180084228516","y":"-11.635520935058594"}}]},"kind":1,"ts":"44868.576"}|{"index":39,"data":{"entities":[{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"54.829036712646484","y":"6.479506492614746"},"rot":"0","vel":{"x":"1.9895514249801636","y":"0.4295259714126587"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"}]},"kind":1,"ts":"48936.236"}|{"index":40,"data":{"iteration":5,"wind":-10,"teams":[{"name":"The Prawns","group":0,"playerUserId":null,"worms":[{"uuid":"9a3919e0-e756-428e-9ccb-094b4b012183","name":"Shrimp","health":69,"maxHealth":100}]},{"name":"The Whales","group":1,"playerUserId":null,"worms":[{"uuid":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","name":"Welsh boy","health":71,"maxHealth":100}]}]},"kind":7,"ts":"48936.392"}|{"index":41,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","angle":"3.8215926535897937","dir":"up","action":2},"kind":4,"ts":"55217.401"}|{"index":42,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","duration":36.299999999999955,"action":3},"kind":5,"ts":"56594.373"}|{"index":43,"data":{"entities":[{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"55.38034439086914","y":"6.947047233581543"},"rot":"0","vel":{"x":"0","y":"0"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"},{"uuid":"0cc99087-d69f-4d45-bc79-17171980db99","timer":1800,"owner":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","timerSecs":30,"type":2,"tra":{"x":"52.372928619384766","y":"6.447047233581543"},"rot":"0","vel":{"x":"-17.447668075561523","y":"-9.406169891357422"}}]},"kind":1,"ts":"56596.217"}|{"index":44,"data":{"entities":[{"uuid":"0cc99087-d69f-4d45-bc79-17171980db99","timer":0,"owner":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","timerSecs":30,"type":2,"tra":{"x":"-108.63345336914062","y":"47.79460906982422"},"rot":"2.7007789611816406","vel":{"x":"-60.218833923339844","y":"28.409711837768555"}}]},"kind":1,"ts":"59306.059"}|{"index":45,"data":{"iteration":6,"wind":-3,"teams":[{"name":"The Prawns","group":0,"playerUserId":null,"worms":[{"uuid":"9a3919e0-e756-428e-9ccb-094b4b012183","name":"Shrimp","health":69,"maxHealth":100}]},{"name":"The Whales","group":1,"playerUserId":null,"worms":[{"uuid":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","name":"Welsh boy","health":71,"maxHealth":100}]}]},"kind":7,"ts":"59306.182"}|{"index":46,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"5.483185307179603","dir":"down","action":2},"kind":4,"ts":"65216.52"}|{"index":47,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","duration":27.779999999999955,"action":3},"kind":5,"ts":"66294.166"}|{"index":48,"data":{"entities":[{"uuid":"72ff6276-1d6b-4675-b37b-266b79290b2c","timer":240,"owner":"9a3919e0-e756-428e-9ccb-094b4b012183","timerSecs":4,"type":1,"tra":{"x":"34.33161544799805","y":"6.176065921783447"},"rot":"0","vel":{"x":"8.401067733764648","y":"-8.650063514709473"}}]},"kind":1,"ts":"66294.701"}|{"index":49,"data":{"entities":[{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"55.69258499145508","y":"6.912735462188721"},"rot":"0","vel":{"x":"-5.8586010709404945e-8","y":"0.21543896198272705"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"}]},"kind":1,"ts":"70338.791"}|{"index":50,"data":{"iteration":8,"wind":-10,"teams":[{"name":"The Prawns","group":0,"playerUserId":null,"worms":[{"uuid":"9a3919e0-e756-428e-9ccb-094b4b012183","name":"Shrimp","health":69,"maxHealth":100}]},{"name":"The Whales","group":1,"playerUserId":null,"worms":[{"uuid":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","name":"Welsh boy","health":60,"maxHealth":100}]}]},"kind":7,"ts":"70338.933"}|{"index":51,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","weapon":2},"kind":6,"ts":"76609.55"}|{"index":52,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","weapon":1},"kind":6,"ts":"77290.666"}|{"index":53,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","duration":18.959999999999997,"action":3},"kind":5,"ts":"78094.748"}|{"index":54,"data":{"entities":[{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"55.683189392089844","y":"6.952401161193848"},"rot":"0","vel":{"x":"0","y":"0"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"},{"uuid":"8a2b33ff-9c75-4751-84f4-7bebb6239076","timer":1800,"owner":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","timerSecs":30,"type":2,"tra":{"x":"54.19084167480469","y":"6.452401161193848"},"rot":"0","vel":{"x":"-4.29625940322876","y":"-2.316145896911621"}}]},"kind":1,"ts":"78096.702"}|{"index":55,"data":{"entities":[]},"kind":1,"ts":"78594.493"}|{"index":56,"data":{"iteration":9,"wind":-4,"teams":[{"name":"The Prawns","group":0,"playerUserId":null,"worms":[{"uuid":"9a3919e0-e756-428e-9ccb-094b4b012183","name":"Shrimp","health":69,"maxHealth":100}]},{"name":"The Whales","group":1,"playerUserId":null,"worms":[{"uuid":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","name":"Welsh boy","health":60,"maxHealth":100}]}]},"kind":7,"ts":"78594.645"}|{"index":57,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"5.883185307179595","dir":"down","action":2},"kind":4,"ts":"84118.353"}|{"index":58,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"6.263185307179587","dir":"down","action":2},"kind":4,"ts":"84447.741"}|{"index":59,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":24,"action":1},"kind":3,"ts":"84872.747"}|{"index":60,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","action":4},"kind":2,"ts":"84994.826"}|{"index":61,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":137,"action":1},"kind":3,"ts":"86407.776"}|{"index":62,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","action":4},"kind":2,"ts":"86544.828"}|{"index":63,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":67,"action":1},"kind":3,"ts":"87701.747"}|{"index":64,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":6,"action":1},"kind":3,"ts":"87760.094"}|{"index":65,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","cycles":41,"action":1},"kind":3,"ts":"88140.772"}|{"index":66,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","weapon":0},"kind":6,"ts":"88598.485"}|{"index":67,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","weapon":2},"kind":6,"ts":"89069.803"}|{"index":68,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"0.27999999999999997","dir":"down","action":2},"kind":4,"ts":"90286.762"}|{"index":69,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","angle":"0.019999999999999993","dir":"up","action":2},"kind":4,"ts":"91005.665"}|{"index":70,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","duration":0,"action":3},"kind":5,"ts":"92014.813"}|{"index":71,"data":{"entities":[{"uuid":"55939cbf-e318-4a06-aa10-e9a228892a5a","type":0,"tra":{"x":"46.71908950805664","y":"6.518653869628906"},"rot":"0","vel":{"x":"0","y":"0"},"wormIdent":"9a3919e0-e756-428e-9ccb-094b4b012183"},{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"55.683189392089844","y":"6.952401161193848"},"rot":"0","vel":{"x":"0.8804620504379272","y":"0.5019922852516174"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"}]},"kind":1,"ts":"92074.706"}|{"index":72,"data":{"entities":[{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"55.694698333740234","y":"6.906276226043701"},"rot":"0","vel":{"x":"0.22071585059165955","y":"0"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"}]},"kind":1,"ts":"92077.909"}|{"index":73,"data":{"iteration":11,"wind":3,"teams":[{"name":"The Prawns","group":0,"playerUserId":null,"worms":[{"uuid":"9a3919e0-e756-428e-9ccb-094b4b012183","name":"Shrimp","health":69,"maxHealth":100}]},{"name":"The Whales","group":1,"playerUserId":null,"worms":[{"uuid":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","name":"Welsh boy","health":35,"maxHealth":100}]}]},"kind":7,"ts":"92078.071"}|{"index":74,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","cycles":16,"action":0},"kind":3,"ts":"97635.973"}|{"index":75,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","angle":"3.5415926535897935","dir":"down","action":2},"kind":4,"ts":"98789.926"}|{"index":76,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","angle":"3.181592653589793","dir":"down","action":2},"kind":4,"ts":"99194.973"}|{"index":77,"data":{"id":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","duration":45.239999999999995,"action":3},"kind":5,"ts":"100522.604"}|{"index":78,"data":{"entities":[{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"55.269622802734375","y":"7.552298069000244"},"rot":"0","vel":{"x":"0","y":"0"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"},{"uuid":"3e42877e-8537-4539-9640-a8d0d6dca372","timer":1800,"owner":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","timerSecs":30,"type":2,"tra":{"x":"50.74505615234375","y":"7.052298069000244"},"rot":"0","vel":{"x":"-30.73215103149414","y":"-0.8199613690376282"}}]},"kind":1,"ts":"100523.866"}|{"index":79,"data":{"entities":[{"uuid":"55939cbf-e318-4a06-aa10-e9a228892a5a","type":0,"tra":{"x":"46.614994049072266","y":"6.458955764770508"},"rot":"0","vel":{"x":"-1.0410246849060059","y":"-0.12692099809646606"},"wormIdent":"9a3919e0-e756-428e-9ccb-094b4b012183"}]},"kind":1,"ts":"100676.134"}|{"index":80,"data":{"iteration":13,"wind":2,"teams":[{"name":"The Prawns","group":0,"playerUserId":null,"worms":[{"uuid":"9a3919e0-e756-428e-9ccb-094b4b012183","name":"Shrimp","health":29,"maxHealth":100}]},{"name":"The Whales","group":1,"playerUserId":null,"worms":[{"uuid":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","name":"Welsh boy","health":35,"maxHealth":100}]}]},"kind":7,"ts":"100676.345"}|{"index":81,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","weapon":1},"kind":6,"ts":"107131.033"}|{"index":82,"data":{"id":"9a3919e0-e756-428e-9ccb-094b4b012183","duration":80.27999999999977,"action":3},"kind":5,"ts":"109001.501"}|{"index":83,"data":{"entities":[{"uuid":"55939cbf-e318-4a06-aa10-e9a228892a5a","type":0,"tra":{"x":"46.07716751098633","y":"7.702179908752441"},"rot":"0","vel":{"x":"0","y":"0"},"wormIdent":"9a3919e0-e756-428e-9ccb-094b4b012183"},{"uuid":"c4b9767b-6305-401e-829a-1ddebfade691","timer":1800,"owner":"9a3919e0-e756-428e-9ccb-094b4b012183","timerSecs":30,"type":2,"tra":{"x":"52.32472229003906","y":"7.202179908752441"},"rot":"0","vel":{"x":"58.55965805053711","y":"0.7808995246887207"}}]},"kind":1,"ts":"109003.155"}|{"index":84,"data":{"entities":[{"uuid":"7165b582-f2df-49a8-8bed-25ff0c6cbb6c","type":0,"tra":{"x":"55.271026611328125","y":"7.579299449920654"},"rot":"0","vel":{"x":"0.08451084792613983","y":"1.6814192533493042"},"wormIdent":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242"}]},"kind":1,"ts":"109057.754"}|{"index":85,"data":{"iteration":15,"wind":0,"teams":[{"name":"The Prawns","group":0,"playerUserId":null,"worms":[{"uuid":"9a3919e0-e756-428e-9ccb-094b4b012183","name":"Shrimp","health":29,"maxHealth":100}]},{"name":"The Whales","group":1,"playerUserId":null,"worms":[{"uuid":"d39e19a9-f8e5-4c2a-b4dc-eecb2d741242","name":"Welsh boy","health":0,"maxHealth":100}]}]},"kind":7,"ts":"109057.95"}`.split( - "|", - ); diff --git a/src/scenarios/tiledMap.ts b/src/scenarios/tiledMap.ts index bb5511e..0d6f3df 100644 --- a/src/scenarios/tiledMap.ts +++ b/src/scenarios/tiledMap.ts @@ -114,7 +114,8 @@ export default async function runScenario(game: Game) { game.viewport.screenHeight, ); - const waterLevel = level.objects.find((v) => v.type === "wormgine.water")?.tra.y ?? 0; + const waterLevel = + level.objects.find((v) => v.type === "wormgine.water")?.tra.y ?? 0; const water = world.addEntity( new Water( @@ -136,10 +137,7 @@ export default async function runScenario(game: Game) { if (levelObject.type === "wormgine.target") { const t = new WeaponTarget( world, - Coordinate.fromScreen( - levelObject.tra.x, - levelObject.tra.y, - ), + Coordinate.fromScreen(levelObject.tra.x, levelObject.tra.y), parent, ); world.addEntity(t); @@ -164,10 +162,7 @@ export default async function runScenario(game: Game) { await Worm.create( parent, world, - Coordinate.fromScreen( - nextLocation.tra.x, - nextLocation.tra.y, - ), + Coordinate.fromScreen(nextLocation.tra.x, nextLocation.tra.y), wormInstance, async (worm, definition, opts) => { const newProjectile = definition.fireFn(parent, world, worm, opts); diff --git a/src/scenarios/uiTest.ts b/src/scenarios/uiTest.ts index 1869823..cf67a4a 100644 --- a/src/scenarios/uiTest.ts +++ b/src/scenarios/uiTest.ts @@ -24,6 +24,7 @@ export default async function runScenario(game: Game) { ammo: { [IWeaponCode.Bazooka]: 999, }, + uuid: "red", }, { name: "The Whales", @@ -39,6 +40,7 @@ export default async function runScenario(game: Game) { ammo: { [IWeaponCode.Bazooka]: 999, }, + uuid: "blue", }, { name: "Purple Rain", @@ -54,6 +56,7 @@ export default async function runScenario(game: Game) { ammo: { [IWeaponCode.Bazooka]: 999, }, + uuid: "purple", }, { name: "The Yellow Raincoats", @@ -69,6 +72,7 @@ export default async function runScenario(game: Game) { ammo: { [IWeaponCode.Bazooka]: 999, }, + uuid: "yellow", }, { name: "The Onion Enjoyers", @@ -84,6 +88,7 @@ export default async function runScenario(game: Game) { ammo: { [IWeaponCode.Bazooka]: 999, }, + uuid: "green", }, { name: "Creamy Orange Grease Gang", @@ -99,6 +104,7 @@ export default async function runScenario(game: Game) { ammo: { [IWeaponCode.Bazooka]: 999, }, + uuid: "orange", }, ], world, diff --git a/src/settings.ts b/src/settings.ts index b3f0e02..b722ea1 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -9,7 +9,8 @@ export interface StoredTeam { name: string; worms: string[]; flagb64?: string; - synced: boolean | null; + lastModified: number; + uuid: string; } export interface GameSettings { diff --git a/src/state/model.ts b/src/state/model.ts index c995abe..8db8f45 100644 --- a/src/state/model.ts +++ b/src/state/model.ts @@ -1,8 +1,7 @@ import { RoundState } from "../logic/gamestate"; -import { Team, TeamGroup } from "../logic/teams"; +import { Team } from "../logic/teams"; import { IWeaponCode } from "../weapons/weapon"; - export interface RecordedEntityState { type: number | string; // Translation @@ -13,8 +12,6 @@ export interface RecordedEntityState { vel: { x: number; y: number }; } - - export enum StateRecordKind { Header = "header", EntitySync = "ent_sync", @@ -78,7 +75,7 @@ export type StateRecordWormSelectWeapon = StateRecordLine<{ }>; export type StateRecordWormGameState = StateRecordLine<{ - round_state: RoundState, + round_state: RoundState; teams: { uuid: string; worms: { diff --git a/src/state/player.ts b/src/state/player.ts index 7177378..65640e7 100644 --- a/src/state/player.ts +++ b/src/state/player.ts @@ -13,6 +13,7 @@ import { } from "./model"; import { GameActionEvent } from "../net/models"; import Logger from "../log"; +import { fromNetObject } from "../net/netfloat"; interface EventTypes { started: void; @@ -71,9 +72,13 @@ export class StateReplay extends EventEmitter { return [...(this._latestEntityData ?? [])]; } - protected async parseData(data: StateRecordLine): Promise { - const ts = parseFloat(data.ts); - if (data.kind === StateRecordKind.Header) { + protected async parseData({ + ts, + kind, + index, + data, + }: StateRecordLine): Promise { + if (kind === StateRecordKind.Header) { this.emit("started"); this.lastActionTs = ts; this.hostStartTs = ts; @@ -90,36 +95,55 @@ export class StateReplay extends EventEmitter { await new Promise((r) => setTimeout(r, waitFor)); this.lastActionTs = ts; - log.info(`> ${data.ts} ${data.kind} ${data.index} ${data.data}`); + log.info(`> ${ts} ${kind} ${index} ${data}`); - switch (data.kind) { + const processedData = data as unknown; + + switch (kind) { case StateRecordKind.EntitySync: // TODO: Apply deltas somehow. - this._latestEntityData = (data as StateRecordEntitySync).data.entities; - this.emit("entitySync", (data as StateRecordEntitySync).data.entities); + this._latestEntityData = ( + processedData as StateRecordEntitySync + ).data.entities; + this.emit( + "entitySync", + (processedData as StateRecordEntitySync).data.entities, + ); break; case StateRecordKind.WormAction: { - const actionData = data as StateRecordWormAction; + const actionData = processedData as StateRecordWormAction; this.emit("wormAction", actionData.data); break; } case StateRecordKind.WormActionAim: - this.emit("wormActionAim", (data as StateRecordWormActionAim).data); + this.emit( + "wormActionAim", + processedData as StateRecordWormActionAim["data"], + ); break; case StateRecordKind.WormActionMove: - this.emit("wormActionMove", (data as StateRecordWormActionMove).data); + this.emit( + "wormActionMove", + processedData as StateRecordWormActionMove["data"], + ); break; case StateRecordKind.WormActionFire: - this.emit("wormActionFire", (data as StateRecordWormActionFire).data); + this.emit( + "wormActionFire", + processedData as StateRecordWormActionFire["data"], + ); break; case StateRecordKind.WormSelectWeapon: this.emit( "wormSelectWeapon", - (data as StateRecordWormSelectWeapon).data, + (processedData as StateRecordWormSelectWeapon).data, ); break; case StateRecordKind.GameState: - this.emit("gameState", (data as StateRecordWormGameState).data); + this.emit( + "gameState", + processedData as StateRecordWormGameState["data"], + ); break; default: throw Error("Unknown state action, possibly older format!"); @@ -127,7 +151,7 @@ export class StateReplay extends EventEmitter { } } export class TextStateReplay extends StateReplay { - private stateLines: StateRecordLine[]; + private stateLines: StateRecordLine[]; constructor(state: string[]) { super(); @@ -152,9 +176,11 @@ export class MatrixStateReplay extends StateReplay { public async handleEvent(content: GameActionEvent["content"]) { this.prevPromise = this.prevPromise.finally(() => - this.parseData(content.action).catch((ex) => { - console.error("Failed to process line", ex); - }), + this.parseData(fromNetObject(content.action) as StateRecordLine).catch( + (ex) => { + console.error("Failed to process line", ex); + }, + ), ); } } diff --git a/src/state/recorder.ts b/src/state/recorder.ts index 76979b7..3cbe725 100644 --- a/src/state/recorder.ts +++ b/src/state/recorder.ts @@ -14,8 +14,8 @@ import { StateWormAction, } from "./model"; -interface StateRecorderStore { - writeLine(data: StateRecordLine): Promise; +export interface StateRecorderStore { + writeLine(data: StateRecordLine): Promise; } function hashCode(str: string) { @@ -32,21 +32,20 @@ export class StateRecorder { public static RecorderVersion = 2; private recordIndex = 0; private entHashes = new Map(); // uuid -> hash - constructor( - private readonly gameWorld: GameWorld, - private readonly store: StateRecorderStore, - ) { + constructor(private readonly store: StateRecorderStore) {} + + public writeHeader() { this.store.writeLine({ - index: this.recordIndex++, + index: ++this.recordIndex, data: { version: StateRecorder.RecorderVersion }, kind: StateRecordKind.Header, - ts: performance.now().toString(), + ts: performance.now(), } satisfies StateRecordHeader); } - public syncEntityState() { + public syncEntityState(gameWorld: GameWorld) { const stateToSend = []; - for (const entState of this.gameWorld.collectEntityState()) { + for (const entState of gameWorld.collectEntityState()) { const newHash = hashCode(JSON.stringify(entState)); if (this.entHashes.get(entState.uuid) !== newHash) { stateToSend.push(entState); @@ -54,24 +53,24 @@ export class StateRecorder { this.entHashes.set(entState.uuid, newHash); } this.store.writeLine({ - index: this.recordIndex++, + index: ++this.recordIndex, data: { entities: stateToSend, }, kind: StateRecordKind.EntitySync, - ts: performance.now().toString(), + ts: performance.now(), } satisfies StateRecordEntitySync); } public recordWormAction(worm: string, action: StateWormAction) { this.store.writeLine({ - index: this.recordIndex++, + index: ++this.recordIndex, data: { id: worm, action, }, kind: StateRecordKind.WormAction, - ts: performance.now().toString(), + ts: performance.now(), } satisfies StateRecordWormAction); } @@ -81,7 +80,7 @@ export class StateRecorder { cycles: number, ) { this.store.writeLine({ - index: this.recordIndex++, + index: ++this.recordIndex, data: { id: worm, cycles, @@ -91,13 +90,13 @@ export class StateRecorder { : StateWormAction.MoveRight, }, kind: StateRecordKind.WormActionMove, - ts: performance.now().toString(), + ts: performance.now(), } satisfies StateRecordWormActionMove); } public recordWormAim(worm: string, direction: "up" | "down", angle: number) { this.store.writeLine({ - index: this.recordIndex++, + index: ++this.recordIndex, data: { id: worm, angle: angle.toString(), @@ -105,40 +104,40 @@ export class StateRecorder { action: StateWormAction.Aim, }, kind: StateRecordKind.WormActionAim, - ts: performance.now().toString(), + ts: performance.now(), } satisfies StateRecordWormActionAim); } public recordWormFire(worm: string, duration: number) { this.store.writeLine({ - index: this.recordIndex++, + index: ++this.recordIndex, data: { id: worm, duration, action: StateWormAction.Fire, }, kind: StateRecordKind.WormActionFire, - ts: performance.now().toString(), + ts: performance.now(), } satisfies StateRecordWormActionFire); } public recordWormSelectWeapon(worm: string, weapon: IWeaponCode) { this.store.writeLine({ - index: this.recordIndex++, + index: ++this.recordIndex, data: { id: worm, weapon: weapon, }, kind: StateRecordKind.WormSelectWeapon, - ts: performance.now().toString(), + ts: performance.now(), } satisfies StateRecordWormSelectWeapon); } public recordGameState(data: StateRecordWormGameState["data"]) { this.store.writeLine({ - index: this.recordIndex++, + index: ++this.recordIndex, data: data, kind: StateRecordKind.GameState, - ts: performance.now().toString(), + ts: performance.now(), } satisfies StateRecordWormGameState); } } diff --git a/src/utils/coodinate.ts b/src/utils/coodinate.ts index e3425a6..306701d 100644 --- a/src/utils/coodinate.ts +++ b/src/utils/coodinate.ts @@ -80,6 +80,6 @@ export class Coordinate { public hash() { // Cloes enough approximation. - return this.worldX + (this.worldY * 575); + return this.worldX + this.worldY * 575; } } diff --git a/src/world.ts b/src/world.ts index 3ccab87..89e7103 100644 --- a/src/world.ts +++ b/src/world.ts @@ -19,6 +19,7 @@ import type { PhysicsEntity } from "./entities/phys/physicsEntity"; import { RecordedEntityState } from "./state/model"; import Logger from "./log"; import globalFlags from "./flags"; +import { BehaviorSubject } from "rxjs"; const logger = new Logger("World"); @@ -62,10 +63,15 @@ export class GameWorld { public readonly bodyEntityMap = new Map(); public readonly entities = new Map(); private readonly eventQueue = new EventQueue(true); - private _wind = 0; + private readonly windSubject = new BehaviorSubject(0); + public readonly wind$ = this.windSubject.asObservable(); + + /** + * @deprecated Use `this.wind$` + */ get wind() { - return this._wind; + return this.windSubject.value; } constructor( @@ -74,7 +80,7 @@ export class GameWorld { ) {} public setWind(windSpeed: number) { - this._wind = windSpeed; + this.windSubject.next(windSpeed); } public areEntitiesMoving() {