From 40b9e7fa4aadb39f7750554e785c4ef241332533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20=C5=BBegle=C5=84?= Date: Sat, 30 Aug 2025 02:43:37 +0200 Subject: [PATCH 1/4] Use pubsub to broadcast sim events --- core/entity.ts | 13 +- core/sim/Sim.ts | 132 +++++++++--------- core/systems/ai/facilityPlanning.ts | 6 +- core/systems/ai/orderPlanning.ts | 6 +- core/systems/ai/shipPlanning.ts | 6 +- core/systems/ai/shipReturning.ts | 6 +- core/systems/ai/spotting.ts | 6 +- core/systems/ai/tauHarassing.ts | 6 +- core/systems/attacking.ts | 6 +- core/systems/budgetPlanning.ts | 6 +- core/systems/collectibleUnregistering.ts | 6 +- core/systems/cooldowns.ts | 6 +- core/systems/crewGrowing.ts | 6 +- core/systems/deadUnregistering.ts | 6 +- core/systems/disposableUnregistering.ts | 9 +- core/systems/facilityBuilding.ts | 4 +- core/systems/fogOfWarUpdating.ts | 6 +- core/systems/hitpointsRegenerating.ts | 4 +- core/systems/mining.ts | 7 +- core/systems/mission/mission.ts | 6 +- core/systems/modifierRecalculating.ts | 9 +- core/systems/moving.ts | 7 +- core/systems/navigating.ts | 9 +- core/systems/orderExecuting/orderExecuting.ts | 24 ++-- core/systems/pathPlanning.ts | 6 +- core/systems/pirateSpawning.ts | 8 +- core/systems/producing.ts | 8 +- core/systems/reporting/avgFrameReporting.ts | 34 +++-- core/systems/sectorClaiming.ts | 4 +- core/systems/sectorStatisticGathering.ts | 4 +- core/systems/shipBuilding.ts | 4 +- core/systems/storageQuotaPlanning.ts | 4 +- core/systems/storageTransferring.ts | 4 +- core/systems/system.ts | 8 +- core/systems/trading.ts | 6 +- core/systems/undeploying.ts | 6 +- core/utils/trading.test.ts | 27 ++-- ui/components/MapOverlay/StrategicMap.tsx | 8 +- ui/components/TacticalMap/TacticalMap.tsx | 16 ++- ui/scenarios/shooting.stories.tsx | 34 ++--- ui/views/Game.tsx | 10 +- 41 files changed, 292 insertions(+), 201 deletions(-) diff --git a/core/entity.ts b/core/entity.ts index 5b8dede0..524e23be 100644 --- a/core/entity.ts +++ b/core/entity.ts @@ -83,7 +83,8 @@ export class Entity { }; this.componentsMask |= componentMask[componentName]; if (!exists) { - this.sim.hooks.addComponent.notify({ + this.sim.hooks.publish({ + type: "addComponent", entity: this, component: component.name, }); @@ -95,7 +96,11 @@ export class Entity { removeComponent(name: keyof CoreComponents): Entity { this.componentsMask &= ~componentMask[name]; delete this.components[name]; - this.sim.hooks.removeComponent.notify({ entity: this, component: name }); + this.sim.hooks.publish({ + type: "removeComponent", + entity: this, + component: name, + }); return this; } @@ -105,7 +110,7 @@ export class Entity { if (!hasTag) { this.tags.add(tag); - this.sim.hooks.addTag.notify({ tag, entity: this }); + this.sim.hooks.publish({ type: "addTag", tag, entity: this }); } return this; @@ -113,7 +118,7 @@ export class Entity { removeTag(tag: EntityTag): Entity { this.tags.delete(tag); - this.sim.hooks.removeTag.notify({ tag, entity: this }); + this.sim.hooks.publish({ type: "removeTag", tag, entity: this }); return this; } diff --git a/core/sim/Sim.ts b/core/sim/Sim.ts index cd505e33..8511420f 100644 --- a/core/sim/Sim.ts +++ b/core/sim/Sim.ts @@ -12,12 +12,12 @@ import type { CoreComponents } from "@core/components/component"; import type { EntityTag } from "@core/tags"; import { componentMask } from "@core/components/masks"; import LZString from "lz-string"; -import { Observable } from "@core/utils/observer"; import { defaultIndexer } from "@core/systems/utils/default"; import { Vec2 } from "ogl"; import { isVec2 } from "@core/utils/misc"; import { entityIndexer } from "@core/entityIndexer/entityIndexer"; import { defaultLogger } from "@core/log"; +import { PubSub } from "@core/utils/pubsub"; import { Entity, EntityComponents } from "../entity"; import { BaseSim } from "./BaseSim"; import type { System } from "../systems/system"; @@ -26,6 +26,40 @@ import { openDb } from "../db"; const logger = defaultLogger.sub("sim"); +export type ComponentAddEvent = { + type: "addComponent"; + entity: Entity; + component: keyof CoreComponents; +}; +export type ComponentRemoveEvent = { + type: "removeComponent"; + entity: Entity; + component: keyof CoreComponents; +}; +export type TagAddEvent = { type: "addTag"; entity: Entity; tag: EntityTag }; +export type TagRemoveEvent = { + type: "removeTag"; + entity: Entity; + tag: EntityTag; +}; +export type EntityRemoveEvent = { + type: "removeEntity"; + entity: Entity; + reason: string; +}; +export type DestroySimEvent = { + type: "destroy"; +}; +export type PhaseEvent = { + type: "phase"; + delta: number; + phase: "start" | "init" | "update" | "render" | "cleanup" | "end"; +}; +export type SpeedChangeEvent = { + type: "speedChange"; + newSpeed: number; +}; + export interface SimConfig { systems: System[]; } @@ -36,26 +70,16 @@ export class Sim extends BaseSim { @Expose() entityIdCounter: number = 1; - hooks: { - addComponent: Observable<{ - entity: Entity; - component: keyof CoreComponents; - }>; - removeComponent: Observable<{ - entity: Entity; - component: keyof CoreComponents; - }>; - addTag: Observable<{ entity: Entity; tag: EntityTag }>; - removeTag: Observable<{ entity: Entity; tag: EntityTag }>; - removeEntity: Observable<{ entity: Entity; reason: string }>; - destroy: Observable; - - phase: Record< - "start" | "init" | "update" | "render" | "cleanup" | "end", - Observable - >; - onSpeedChange: Observable; - }; + hooks: PubSub< + | ComponentAddEvent + | ComponentRemoveEvent + | TagAddEvent + | TagRemoveEvent + | EntityRemoveEvent + | DestroySimEvent + | PhaseEvent + | SpeedChangeEvent + >; @Expose() @Type(() => Entity) @@ -70,23 +94,7 @@ export class Sim extends BaseSim { super(); this.entities = new Map(); - this.hooks = { - addComponent: new Observable("addComponent"), - removeComponent: new Observable("removeComponent"), - addTag: new Observable("addTag"), - removeTag: new Observable("removeTag"), - removeEntity: new Observable("removeEntity"), - destroy: new Observable("destroy"), - phase: { - start: new Observable("phase.start", false), - init: new Observable("phase.init", false), - update: new Observable("phase.update", false), - render: new Observable("phase.render", false), - cleanup: new Observable("phase.cleanup", false), - end: new Observable("phase.end", false), - }, - onSpeedChange: new Observable("onSpeedChange", false), - }; + this.hooks = new PubSub(); entityIndexer.clear(); this.index = defaultIndexer; @@ -94,25 +102,19 @@ export class Sim extends BaseSim { index.apply(); } - this.hooks.addComponent.subscribe( - "EntityIndexer", - ({ entity, component }) => { - entityIndexer.updateMask(entity); - if (component === "position") { - entityIndexer.updateSector(entity.requireComponents(["position"])); - } + this.hooks.subscribe("addComponent", ({ entity, component }) => { + entityIndexer.updateMask(entity); + if (component === "position") { + entityIndexer.updateSector(entity.requireComponents(["position"])); } - ); - this.hooks.removeComponent.subscribe( - "EntityIndexer", - ({ entity, component }) => { - entityIndexer.updateMask(entity); - if (component === "position") { - entityIndexer.removeFromSectors(entity); - } + }); + this.hooks.subscribe("removeComponent", ({ entity, component }) => { + entityIndexer.updateMask(entity); + if (component === "position") { + entityIndexer.removeFromSectors(entity); } - ); - this.hooks.removeEntity.subscribe("EntityIndexer", ({ entity, reason }) => { + }); + this.hooks.subscribe("removeEntity", ({ entity, reason }) => { logger.log( `Removing entity ${entity.id} ${ entity.cp.name?.value ?? @@ -127,7 +129,7 @@ export class Sim extends BaseSim { ); entityIndexer.remove(entity); }); - this.hooks.destroy.subscribe("EntityIndexer", () => { + this.hooks.subscribe("destroy", () => { entityIndexer.clear(); }); @@ -141,7 +143,7 @@ export class Sim extends BaseSim { }; unregisterEntity = (entity: Entity, reason: string) => { - this.hooks.removeEntity.notify({ entity, reason }); + this.hooks.publish({ type: "removeEntity", entity, reason }); this.entities.delete(entity.id); }; @@ -154,19 +156,19 @@ export class Sim extends BaseSim { return; } - this.hooks.phase.start.notify(delta); - this.hooks.phase.init.notify(delta); - this.hooks.phase.update.notify(delta); - this.hooks.phase.render.notify(delta); - this.hooks.phase.cleanup.notify(delta); - this.hooks.phase.end.notify(delta); + this.hooks.publish({ type: "phase", delta, phase: "start" }); + this.hooks.publish({ type: "phase", delta, phase: "init" }); + this.hooks.publish({ type: "phase", delta, phase: "update" }); + this.hooks.publish({ type: "phase", delta, phase: "render" }); + this.hooks.publish({ type: "phase", delta, phase: "cleanup" }); + this.hooks.publish({ type: "phase", delta, phase: "end" }); this.updateTimer(delta); }; override setSpeed(value: number) { super.setSpeed(value); - this.hooks.onSpeedChange.notify(value); + this.hooks.publish({ type: "speedChange", newSpeed: this.speed }); } init = () => { @@ -225,7 +227,7 @@ export class Sim extends BaseSim { destroy = () => { this.stop(); - this.hooks.destroy.notify(); + this.hooks.publish({ type: "destroy" }); if (!isHeadless) { window.sim = undefined!; window.selected = undefined!; diff --git a/core/systems/ai/facilityPlanning.ts b/core/systems/ai/facilityPlanning.ts index 83c2be60..4c624a71 100644 --- a/core/systems/ai/facilityPlanning.ts +++ b/core/systems/ai/facilityPlanning.ts @@ -190,7 +190,11 @@ export class FacilityPlanningSystem extends System<"plan"> { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; createNewFactory = (faction: Faction): Facility => { diff --git a/core/systems/ai/orderPlanning.ts b/core/systems/ai/orderPlanning.ts index 8be5dd4b..756fa37a 100644 --- a/core/systems/ai/orderPlanning.ts +++ b/core/systems/ai/orderPlanning.ts @@ -483,7 +483,11 @@ export class OrderPlanningSystem extends System<"exec"> { apply = (sim: Sim): void => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; exec = (): void => { diff --git a/core/systems/ai/shipPlanning.ts b/core/systems/ai/shipPlanning.ts index eac3bf57..8d66d40e 100644 --- a/core/systems/ai/shipPlanning.ts +++ b/core/systems/ai/shipPlanning.ts @@ -124,7 +124,11 @@ export class ShipPlanningSystem extends System<"plan"> { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; getFacilityShipRequests = (faction: Faction): ShipRequest[] => diff --git a/core/systems/ai/shipReturning.ts b/core/systems/ai/shipReturning.ts index 826f30c5..5302f525 100644 --- a/core/systems/ai/shipReturning.ts +++ b/core/systems/ai/shipReturning.ts @@ -14,7 +14,11 @@ export class ShipReturningSystem extends System<"exec"> { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; exec = (): void => { diff --git a/core/systems/ai/spotting.ts b/core/systems/ai/spotting.ts index c2e41dc7..43cbe85e 100644 --- a/core/systems/ai/spotting.ts +++ b/core/systems/ai/spotting.ts @@ -17,7 +17,11 @@ export class SpottingSystem extends System<"exec"> { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; static getEnemies( diff --git a/core/systems/ai/tauHarassing.ts b/core/systems/ai/tauHarassing.ts index caf086cd..d1fd76b5 100644 --- a/core/systems/ai/tauHarassing.ts +++ b/core/systems/ai/tauHarassing.ts @@ -15,7 +15,11 @@ export class TauHarassingSystem extends System<"exec"> { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; getFleet = (): Ship | null => { diff --git a/core/systems/attacking.ts b/core/systems/attacking.ts index b4ac6e21..4095691a 100644 --- a/core/systems/attacking.ts +++ b/core/systems/attacking.ts @@ -75,7 +75,11 @@ export class AttackingSystem extends System { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; exec = (): void => { diff --git a/core/systems/budgetPlanning.ts b/core/systems/budgetPlanning.ts index 135b6bfb..cdb9e9f5 100644 --- a/core/systems/budgetPlanning.ts +++ b/core/systems/budgetPlanning.ts @@ -31,7 +31,11 @@ export class BudgetPlanningSystem extends System<"exec"> { apply = (sim: Sim): void => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; exec = (): void => { diff --git a/core/systems/collectibleUnregistering.ts b/core/systems/collectibleUnregistering.ts index 8b29ccbc..ed14fb5b 100644 --- a/core/systems/collectibleUnregistering.ts +++ b/core/systems/collectibleUnregistering.ts @@ -5,7 +5,11 @@ export class CollectibleUnregisteringSystem extends System { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.cleanup.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "cleanup") { + this.exec(); + } + }); }; exec = (): void => { diff --git a/core/systems/cooldowns.ts b/core/systems/cooldowns.ts index 36d4ef0e..c707d71c 100644 --- a/core/systems/cooldowns.ts +++ b/core/systems/cooldowns.ts @@ -5,7 +5,11 @@ export class CooldownUpdatingSystem extends System { apply = (sim: Sim): void => { super.apply(sim); - sim.hooks.phase.init.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase, delta }) => { + if (phase === "init") { + this.exec(delta); + } + }); }; exec = (delta: number): void => { this.sim.entities.forEach((entity) => entity.cooldowns.update(delta)); diff --git a/core/systems/crewGrowing.ts b/core/systems/crewGrowing.ts index 634228e9..76f68e6a 100644 --- a/core/systems/crewGrowing.ts +++ b/core/systems/crewGrowing.ts @@ -26,7 +26,11 @@ export class CrewGrowingSystem extends System<"exec"> { const offset = Math.floor(sim.getTime() / gameDay) + 1 - sim.getTime() / gameDay; this.cooldowns.use("exec", offset); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; exec = (): void => { diff --git a/core/systems/deadUnregistering.ts b/core/systems/deadUnregistering.ts index 15fd2533..0bc907e6 100644 --- a/core/systems/deadUnregistering.ts +++ b/core/systems/deadUnregistering.ts @@ -19,7 +19,11 @@ export class DeadUnregisteringSystem extends System { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.cleanup.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "cleanup") { + this.exec(); + } + }); }; exec = (): void => { diff --git a/core/systems/disposableUnregistering.ts b/core/systems/disposableUnregistering.ts index cd805df0..c5a23e4a 100644 --- a/core/systems/disposableUnregistering.ts +++ b/core/systems/disposableUnregistering.ts @@ -7,10 +7,11 @@ export class DisposableUnregisteringSystem extends System<"exec"> { apply(sim: Sim) { super.apply(sim); - sim.hooks.phase.cleanup.subscribe( - this.constructor.name, - this.exec.bind(this) - ); + sim.hooks.subscribe("phase", ({ phase, delta }) => { + if (phase === "cleanup") { + this.exec(delta); + } + }); NavigatingSystem.onTargetReached(this.constructor.name, (entity) => { if (entity.hasComponents(["disposable"])) { DisposableUnregisteringSystem.dispose(entity); diff --git a/core/systems/facilityBuilding.ts b/core/systems/facilityBuilding.ts index 11ba598a..18c766e7 100644 --- a/core/systems/facilityBuilding.ts +++ b/core/systems/facilityBuilding.ts @@ -17,7 +17,9 @@ export class FacilityBuildingSystem extends System<"build" | "offers"> { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase, delta }) => { + if (phase === "update") this.exec(delta); + }); }; createOffers = (): void => { diff --git a/core/systems/fogOfWarUpdating.ts b/core/systems/fogOfWarUpdating.ts index 1a234f8e..1236e57d 100644 --- a/core/systems/fogOfWarUpdating.ts +++ b/core/systems/fogOfWarUpdating.ts @@ -35,7 +35,11 @@ export class FogOfWarUpdatingSystem extends System<"exec"> { this.constructor.name ); - sim.hooks.phase.init.subscribe(this.constructor.name, this.updateFog); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "init") { + this.updateFog(); + } + }); }; destroy = (): void => { diff --git a/core/systems/hitpointsRegenerating.ts b/core/systems/hitpointsRegenerating.ts index f6ce8ae8..14eb9879 100644 --- a/core/systems/hitpointsRegenerating.ts +++ b/core/systems/hitpointsRegenerating.ts @@ -8,7 +8,9 @@ export class HitpointsRegeneratingSystem extends System<"exec"> { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", (event) => { + if (event.phase === "update") this.exec(); + }); }; exec = (): void => { diff --git a/core/systems/mining.ts b/core/systems/mining.ts index b98b19e8..9380d7f1 100644 --- a/core/systems/mining.ts +++ b/core/systems/mining.ts @@ -16,10 +16,9 @@ export class MiningSystem extends System<"exec"> { apply(sim: Sim): void { super.apply(sim); - sim.hooks.phase.update.subscribe( - this.constructor.name, - this.exec.bind(this) - ); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") this.exec.bind(this); + }); } exec(): void { this.cooldowns.doEvery("exec", tickTime, () => { diff --git a/core/systems/mission/mission.ts b/core/systems/mission/mission.ts index 5b599119..9c6b1e74 100644 --- a/core/systems/mission/mission.ts +++ b/core/systems/mission/mission.ts @@ -37,7 +37,11 @@ export class MissionSystem extends System<"generate" | "track"> { apply(sim: Sim): void { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); actionLoader.register( { diff --git a/core/systems/modifierRecalculating.ts b/core/systems/modifierRecalculating.ts index 7de025c6..a7b1999a 100644 --- a/core/systems/modifierRecalculating.ts +++ b/core/systems/modifierRecalculating.ts @@ -6,10 +6,11 @@ import { System } from "./system"; export class ModifierRecalculatingSystem extends System { apply(sim: Sim) { - sim.hooks.phase.start.subscribe( - this.constructor.name, - this.recalculateModifiers.bind(this) - ); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "start") { + this.recalculateModifiers(); + } + }); } // eslint-disable-next-line class-methods-use-this diff --git a/core/systems/moving.ts b/core/systems/moving.ts index 05ff3bc4..acc92181 100644 --- a/core/systems/moving.ts +++ b/core/systems/moving.ts @@ -64,10 +64,9 @@ export class MovingSystem extends System { apply = (sim: Sim): void => { super.apply(sim); - sim.hooks.phase.update.subscribe( - this.constructor.name, - this.exec.bind(this) - ); + sim.hooks.subscribe("phase", ({ phase, delta }) => { + if (phase === "update") this.exec(delta); + }); }; // eslint-disable-next-line class-methods-use-this diff --git a/core/systems/navigating.ts b/core/systems/navigating.ts index 3c46cd03..612c4860 100644 --- a/core/systems/navigating.ts +++ b/core/systems/navigating.ts @@ -190,11 +190,10 @@ export class NavigatingSystem extends System { apply(sim: Sim): void { super.apply(sim); - sim.hooks.phase.update.subscribe( - this.constructor.name, - this.exec.bind(this) - ); - sim.hooks.destroy.subscribe(this.constructor.name, () => { + sim.hooks.subscribe("phase", ({ phase, delta }) => { + if (phase === "update") this.exec(delta); + }); + sim.hooks.subscribe("destroy", () => { this.hook.observers.clear(); }); } diff --git a/core/systems/orderExecuting/orderExecuting.ts b/core/systems/orderExecuting/orderExecuting.ts index a9a18b88..0092aeff 100644 --- a/core/systems/orderExecuting/orderExecuting.ts +++ b/core/systems/orderExecuting/orderExecuting.ts @@ -249,23 +249,17 @@ export class OrderExecutingSystem extends System { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.removeEntity.subscribe( - "OrderExecutingSystem-allocations", - ({ entity }) => cleanupAllocations(entity) + sim.hooks.subscribe("removeEntity", ({ entity }) => + cleanupAllocations(entity) ); - sim.hooks.removeEntity.subscribe( - "OrderExecutingSystem-orders", - ({ entity }) => cleanupOrders(entity) + sim.hooks.subscribe("removeEntity", ({ entity }) => cleanupOrders(entity)); + sim.hooks.subscribe("removeEntity", ({ entity }) => + cleanupChildren(entity) ); - sim.hooks.removeEntity.subscribe( - "OrderExecutingSystem-children", - ({ entity }) => cleanupChildren(entity) - ); - sim.hooks.removeEntity.subscribe( - "OrderExecutingSystem-docks", - ({ entity }) => cleanupDocks(entity) - ); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("removeEntity", ({ entity }) => cleanupDocks(entity)); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") this.exec(); + }); }; exec = () => { diff --git a/core/systems/pathPlanning.ts b/core/systems/pathPlanning.ts index 80783271..a194c1d2 100644 --- a/core/systems/pathPlanning.ts +++ b/core/systems/pathPlanning.ts @@ -33,7 +33,11 @@ export class PathPlanningSystem extends System<"regen"> { apply = (sim: Sim): void => { super.apply(sim); - sim.hooks.phase.init.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "init") { + this.exec(); + } + }); regen(this.sim); }; diff --git a/core/systems/pirateSpawning.ts b/core/systems/pirateSpawning.ts index 9ee4347e..59ec6c0c 100644 --- a/core/systems/pirateSpawning.ts +++ b/core/systems/pirateSpawning.ts @@ -239,15 +239,13 @@ export class PirateSpawningSystem extends System< apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.start.subscribe(this.constructor.name, () => { - if (!this.faction) { + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "start" && !this.faction) { this.faction = sim.index.ai .get() .find((f) => f.cp.name.slug === "PIR")!; - } + } else if (phase === "update") this.exec(); }); - - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); }; } diff --git a/core/systems/producing.ts b/core/systems/producing.ts index e5e6db57..014cb2c5 100644 --- a/core/systems/producing.ts +++ b/core/systems/producing.ts @@ -43,7 +43,7 @@ export class ProducingSystem extends System<"exec"> { apply = (sim: Sim): void => { super.apply(sim); - this.sim.hooks.removeEntity.subscribe("ProducingSystem", ({ entity }) => { + this.sim.hooks.subscribe("removeEntity", ({ entity }) => { if (entity.cp.modules) { entity.cp.modules.ids.forEach((id) => this.sim.getOrThrow(id).unregister("parent destroyed") @@ -55,7 +55,11 @@ export class ProducingSystem extends System<"exec"> { const offset = Math.floor(sim.getTime() / gameDay) + 1 - sim.getTime() / gameDay; this.cooldowns.use("exec", offset); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; static isAbleToProduce = ( diff --git a/core/systems/reporting/avgFrameReporting.ts b/core/systems/reporting/avgFrameReporting.ts index aa33f366..f762fa9d 100644 --- a/core/systems/reporting/avgFrameReporting.ts +++ b/core/systems/reporting/avgFrameReporting.ts @@ -32,26 +32,30 @@ export class AvgFrameReportingSystem extends System { this.constructor.name ); - sim.hooks.phase.start.subscribe(this.constructor.name, () => { - this.start = performance.now(); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "start") { + this.start = performance.now(); + } }); - sim.hooks.phase.end.subscribe(this.constructor.name, (delta) => { - if (!this.reporting || delta === 0) return; + sim.hooks.subscribe("phase", ({ phase, delta }) => { + if (phase === "end") { + if (!this.reporting || delta === 0) return; - this.accumulator += performance.now() - this.start; - this.iterations++; + this.accumulator += performance.now() - this.start; + this.iterations++; - if (this.iterations === 60) { - // eslint-disable-next-line no-console - const newData = frameData.value.slice(); - newData.unshift(this.accumulator / 60); - if (newData.length > 31) { - newData.pop(); + if (this.iterations === 60) { + // eslint-disable-next-line no-console + const newData = frameData.value.slice(); + newData.unshift(this.accumulator / 60); + if (newData.length > 31) { + newData.pop(); + } + frameData.notify(newData); + this.iterations = 0; + this.accumulator = 0; } - frameData.notify(newData); - this.iterations = 0; - this.accumulator = 0; } }); } diff --git a/core/systems/sectorClaiming.ts b/core/systems/sectorClaiming.ts index 3bc6e2d4..3c9c9c4a 100644 --- a/core/systems/sectorClaiming.ts +++ b/core/systems/sectorClaiming.ts @@ -8,7 +8,9 @@ export class SectorClaimingSystem extends System<"exec"> { super.apply(sim); this.cooldowns.timers.exec = 2; - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", (event) => { + if (event.phase === "update") this.exec(); + }); }; exec = (): void => { diff --git a/core/systems/sectorStatisticGathering.ts b/core/systems/sectorStatisticGathering.ts index e9f2324f..59f8919d 100644 --- a/core/systems/sectorStatisticGathering.ts +++ b/core/systems/sectorStatisticGathering.ts @@ -9,7 +9,9 @@ export class SectorStatisticGatheringSystem extends System { apply(sim: Sim): void { super.apply(sim); - sim.hooks.phase.end.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", (event) => { + if (event.phase === "end") this.exec(); + }); } exec = (): void => { diff --git a/core/systems/shipBuilding.ts b/core/systems/shipBuilding.ts index 4a2c6767..c1571f46 100644 --- a/core/systems/shipBuilding.ts +++ b/core/systems/shipBuilding.ts @@ -13,7 +13,9 @@ export class ShipBuildingSystem extends System<"exec"> { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", (event) => { + if (event.phase === "update") this.exec(); + }); }; exec = (): void => { diff --git a/core/systems/storageQuotaPlanning.ts b/core/systems/storageQuotaPlanning.ts index 61c74fbe..32d16368 100644 --- a/core/systems/storageQuotaPlanning.ts +++ b/core/systems/storageQuotaPlanning.ts @@ -56,7 +56,9 @@ export class StorageQuotaPlanningSystem extends System<"settle"> { apply = (sim: Sim): void => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", (event) => { + if (event.phase === "update") this.exec(); + }); }; exec = (): void => { diff --git a/core/systems/storageTransferring.ts b/core/systems/storageTransferring.ts index 3fd3641d..33b26fc8 100644 --- a/core/systems/storageTransferring.ts +++ b/core/systems/storageTransferring.ts @@ -9,7 +9,9 @@ export class StorageTransferringSystem extends System<"exec"> { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", (event) => { + if (event.phase === "update") this.exec(event.delta); + }); }; exec = (delta: number): void => { diff --git a/core/systems/system.ts b/core/systems/system.ts index 02c46d59..6090e722 100644 --- a/core/systems/system.ts +++ b/core/systems/system.ts @@ -23,9 +23,9 @@ export abstract class System { this.sim = sim; this.logger.log("Applying system"); - sim.hooks.phase.start.subscribe(this.constructor.name, (delta) => - this.cooldowns.update(delta) - ); - sim.hooks.destroy.subscribe(this.constructor.name, this.destroy); + sim.hooks.subscribe("phase", ({ phase, delta }) => { + if (phase === "start") this.cooldowns.update(delta); + }); + sim.hooks.subscribe("destroy", this.destroy); } } diff --git a/core/systems/trading.ts b/core/systems/trading.ts index c13b25a9..5b94c086 100644 --- a/core/systems/trading.ts +++ b/core/systems/trading.ts @@ -383,7 +383,11 @@ export class TradingSystem extends System<"adjustPrices" | "createOffers"> { this.collect(sim); } - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; destroy = (): void => { diff --git a/core/systems/undeploying.ts b/core/systems/undeploying.ts index 304af9ae..b2f352ed 100644 --- a/core/systems/undeploying.ts +++ b/core/systems/undeploying.ts @@ -50,7 +50,11 @@ export class UndeployingSystem extends System<"exec"> { apply = (sim: Sim) => { super.apply(sim); - sim.hooks.phase.update.subscribe(this.constructor.name, this.exec); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + this.exec(); + } + }); }; exec = (): void => { diff --git a/core/utils/trading.test.ts b/core/utils/trading.test.ts index 96214ab8..58dbb3e5 100644 --- a/core/utils/trading.test.ts +++ b/core/utils/trading.test.ts @@ -1,4 +1,3 @@ -import { matrix } from "mathjs"; import { Vec2 } from "ogl"; import type { Facility } from "../archetypes/facility"; import { facilityComponents } from "../archetypes/facility"; @@ -39,16 +38,16 @@ describe("Trading module", () => { systems: [new PathPlanningSystem(), new OrderExecutingSystem()], }); // Run path planning - sim.hooks.phase.init.notify(0); + sim.hooks.publish("phase", { phase: "init", delta: 0 }); sector = createSector(sim, { name: "", slug: "", - position: matrix([0, 0, 0]), + position: new Vec2(0, 0), }); facility = createFarm( { - position: matrix([0, 0]), + position: new Vec2(0, 0), owner: createFaction("F", sim), sector, }, @@ -90,7 +89,7 @@ describe("Trading module", () => { const ship = createShip(sim, { ...shipClasses[0], owner: shipFaction, - position: matrix([0, 0, 0]), + position: new Vec2(0, 0), sector, }); ship.cp.storage.max = 1000; @@ -139,7 +138,7 @@ describe("Trading module", () => { const ship = createShip(sim, { ...shipClasses[0], owner: shipFaction, - position: matrix([0, 0, 0]), + position: new Vec2(0, 0), sector, }); ship.cp.storage.max = 1000; @@ -194,7 +193,7 @@ describe("Trading module", () => { const ship = createShip(sim, { ...shipClasses[0], owner: shipFaction, - position: matrix([0, 0, 0]), + position: new Vec2(0, 0), sector, }); ship.cp.storage.max = 1000; @@ -250,7 +249,7 @@ describe("Trading module", () => { createOffers(facility); const waterFacility = createWaterFacility( - { owner: createFaction("F2", sim), position: matrix([0, 0]), sector }, + { owner: createFaction("F2", sim), position: new Vec2(0, 0), sector }, sim ).requireComponents([...facilityComponents, "compoundProduction", "owner"]); settleStorageQuota(waterFacility); @@ -271,7 +270,7 @@ describe("Trading module", () => { const ship = createShip(sim, { ...shipClasses[0], owner: shipFaction, - position: matrix([0, 0, 0]), + position: new Vec2(0, 0), sector, }); @@ -358,7 +357,7 @@ describe("Trade flow", () => { sim = new Sim({ systems: [new PathPlanningSystem(), new OrderExecutingSystem()], }); - sim.hooks.phase.init.notify(0); + sim.hooks.publish("phase", { phase: "init", delta: 0 }); createFaction("Player", sim) .addComponent({ @@ -373,11 +372,11 @@ describe("Trade flow", () => { sector = createSector(sim, { name: "", slug: "", - position: matrix([0, 0, 0]), + position: new Vec2(0, 0), }); farm = createFarm( { - position: matrix([0, 0]), + position: new Vec2(0, 0), owner: createFaction("F", sim), sector, }, @@ -390,7 +389,7 @@ describe("Trade flow", () => { farm.cp.trade.offers.water.price = 110; waterFacility = createWaterFacility( - { owner: createFaction("F2", sim), position: matrix([0, 0]), sector }, + { owner: createFaction("F2", sim), position: new Vec2(0, 0), sector }, sim ).requireComponents([...facilityComponents, "compoundProduction", "owner"]); waterFacility.cp.name.value = "Water facility"; @@ -402,7 +401,7 @@ describe("Trade flow", () => { ship = createShip(sim, { ...shipClasses[0], owner: shipFaction, - position: matrix([0, 0, 0]), + position: new Vec2(0, 0), sector, }); }); diff --git a/ui/components/MapOverlay/StrategicMap.tsx b/ui/components/MapOverlay/StrategicMap.tsx index 2430be3e..94e7f902 100644 --- a/ui/components/MapOverlay/StrategicMap.tsx +++ b/ui/components/MapOverlay/StrategicMap.tsx @@ -133,12 +133,12 @@ export class StrategicMap extends React.PureComponent { this.control.onPointerUp = this.onClick.bind(this); this.control.onKeyDown = this.onKeyDown.bind(this); - this.sim.hooks.removeEntity.subscribe("TacticalMap", ({ entity }) => { + this.sim.hooks.subscribe("removeEntity", ({ entity }) => { const mesh = this.engine.scene.getEntity(entity.id); - if (mesh) { - if (isDestroyable(mesh)) mesh.destroy(); - this.engine.scene.removeEntity(entity.id); + if (mesh && isDestroyable(mesh)) { + mesh.destroy(); } + if (mesh) this.engine.scene.removeEntity(entity.id); }); } diff --git a/ui/components/TacticalMap/TacticalMap.tsx b/ui/components/TacticalMap/TacticalMap.tsx index 2498fe6a..2ec00ccb 100644 --- a/ui/components/TacticalMap/TacticalMap.tsx +++ b/ui/components/TacticalMap/TacticalMap.tsx @@ -83,10 +83,16 @@ export class TacticalMap extends React.PureComponent { const onSpeedChange = (speed: number) => { this.engine.setDeltaMultiplier(speed); }; - this.sim.hooks.onSpeedChange.subscribe("TacticalMap", onSpeedChange); - this.onUnmountCallbacks.push(transport3D.reset.bind(transport3D), () => { - this.sim.hooks.onSpeedChange.unsubscribe(onSpeedChange); - }); + const unsubscribe = this.sim.hooks.subscribe( + "speedChange", + ({ newSpeed }) => { + onSpeedChange(newSpeed); + } + ); + this.onUnmountCallbacks.push( + transport3D.reset.bind(transport3D), + unsubscribe + ); } componentDidMount(): void { @@ -120,7 +126,7 @@ export class TacticalMap extends React.PureComponent { if (key !== "gameSettings") return; this.updateEngineSettings(); }); - this.sim.hooks.removeEntity.subscribe("TacticalMap", ({ entity }) => { + this.sim.hooks.subscribe("removeEntity", ({ entity }) => { if (this.meshes.has(entity)) { const mesh = this.meshes.get(entity)!; mesh.destroy(); diff --git a/ui/scenarios/shooting.stories.tsx b/ui/scenarios/shooting.stories.tsx index 0527f8dc..6699bfe7 100644 --- a/ui/scenarios/shooting.stories.tsx +++ b/ui/scenarios/shooting.stories.tsx @@ -24,22 +24,24 @@ class MovingSystem extends System { apply(sim: Sim) { this.sim = sim; - sim.hooks.phase.update.subscribe(this.constructor.name, () => { - const shipsNum = defaultIndexer.ships.get().length; - - for (let i = 0; i < shipsNum; i++) { - const ship = defaultIndexer.ships.get()[i]; - const t = sim.getTime() / 10; - - const angle = t + (i * Math.PI * 2) / shipsNum; - const r = 0.1; - - ship.cp.position.coord.copy(fromPolar(angle, r)); - const nextAngle = t + ((i + 1) * Math.PI * 2) / shipsNum; - const nextPos = fromPolar(nextAngle, r); - const vec = nextPos.sub(ship.cp.position.coord); - ship.cp.position.angle = Math.atan2(vec.y, vec.x); - applyPositionToChildren(ship); + sim.hooks.subscribe("phase", ({ phase }) => { + if (phase === "update") { + const shipsNum = defaultIndexer.ships.get().length; + + for (let i = 0; i < shipsNum; i++) { + const ship = defaultIndexer.ships.get()[i]; + const t = sim.getTime() / 10; + + const angle = t + (i * Math.PI * 2) / shipsNum; + const r = 0.1; + + ship.cp.position.coord.copy(fromPolar(angle, r)); + const nextAngle = t + ((i + 1) * Math.PI * 2) / shipsNum; + const nextPos = fromPolar(nextAngle, r); + const vec = nextPos.sub(ship.cp.position.coord); + ship.cp.position.angle = Math.atan2(vec.y, vec.x); + applyPositionToChildren(ship); + } } }); } diff --git a/ui/views/Game.tsx b/ui/views/Game.tsx index 32925638..39e4c073 100644 --- a/ui/views/Game.tsx +++ b/ui/views/Game.tsx @@ -63,15 +63,11 @@ const Game: React.FC = () => { setSim(undefined!); }; - sim.hooks.removeEntity.subscribe("Game", ({ entity }) => { - if ( - entity.hasComponents(["position"]) && - selectedUnits.includes(entity) - ) { + sim.hooks.subscribe("removeEntity", ({ entity }) => { + if (entity.hasComponents(["position"]) && selectedUnits.includes(entity)) gameStore.unselectUnit(entity); - } }); - sim.hooks.destroy.subscribe("Game", unmount); + sim.hooks.subscribe("destroy", unmount); window.sim = sim; From a04bbcf13081b1f84f1a8e0825231c4f834745a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20=C5=BBegle=C5=84?= Date: Sat, 30 Aug 2025 03:49:01 +0200 Subject: [PATCH 2/4] Fix speed change hook --- core/sim/Sim.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/sim/Sim.ts b/core/sim/Sim.ts index 8511420f..9278ba07 100644 --- a/core/sim/Sim.ts +++ b/core/sim/Sim.ts @@ -168,7 +168,7 @@ export class Sim extends BaseSim { override setSpeed(value: number) { super.setSpeed(value); - this.hooks.publish({ type: "speedChange", newSpeed: this.speed }); + this.hooks.publish({ type: "speedChange", newSpeed: value }); } init = () => { From e410513ff6537294a3deee88b15e0f5204bbe8aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20=C5=BBegle=C5=84?= Date: Sat, 30 Aug 2025 03:49:29 +0200 Subject: [PATCH 3/4] Improve hp bars opaque with distance --- .../materials/entityIndicator/shader.frag.glsl | 17 ++++++++++++++++- .../materials/entityIndicator/shader.vert.glsl | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ogl-engine/materials/entityIndicator/shader.frag.glsl b/ogl-engine/materials/entityIndicator/shader.frag.glsl index 284bff81..50d008c9 100644 --- a/ogl-engine/materials/entityIndicator/shader.frag.glsl +++ b/ogl-engine/materials/entityIndicator/shader.frag.glsl @@ -2,6 +2,7 @@ precision highp float; in vec2 vUv; +in float vDist; out vec4 fragData[3]; @@ -85,5 +86,19 @@ void main() { discard; } - fragData[2] = vec4(color, clamp(alpha, 0.f, 1.f) * (0.75f + sign(float(uSelected)) * 0.22f + sign(float(uHovered)) * 0.1f)); + float distanceModifier = clamp(2.f - log(vDist) / 8.f, 0.25f, 1.f); + if(shield <= 0.f || hp <= 0.f) { + distanceModifier = min(1.f, 2.f - log(vDist) / 4.f); + } + alpha *= 0.65f * distanceModifier; + + if(uHovered > 0) { + alpha += 0.2f; + } + + if(uSelected > 0) { + alpha = 1.f; + } + + fragData[2] = vec4(color, clamp(alpha, 0.f, 1.f)); } \ No newline at end of file diff --git a/ogl-engine/materials/entityIndicator/shader.vert.glsl b/ogl-engine/materials/entityIndicator/shader.vert.glsl index 8f0a0753..5d8612e5 100644 --- a/ogl-engine/materials/entityIndicator/shader.vert.glsl +++ b/ogl-engine/materials/entityIndicator/shader.vert.glsl @@ -20,6 +20,7 @@ void main() { vec4 viewPosition = viewMatrix * vec4(worldPosition, 1.0f); vUv = uv; + vDist = distance(cameraPosition, modelMatrix[3].xyz); gl_Position = projectionMatrix * viewPosition; } \ No newline at end of file From e70ef82b3456cbdbb088a86f88db1460ca8226c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20=C5=BBegle=C5=84?= Date: Sat, 30 Aug 2025 21:55:29 +0200 Subject: [PATCH 4/4] Use 3d engine pubsub events --- core/systems/disposableUnregistering.ts | 2 +- core/systems/navigating.ts | 10 ++++----- core/utils/pubsub.ts | 25 ++++++++++++++++++++++- devtools/scenarios/dogfight.tsx | 2 +- devtools/scenarios/frigateUnderAttack.tsx | 2 +- devtools/scenarios/stationUnderAttack.tsx | 2 +- ogl-engine/OglCanvas.tsx | 4 ++-- ogl-engine/engine/engine.ts | 24 +++++++++------------- ogl-engine/engine/engine2d.ts | 2 +- ogl-engine/engine/engine3d.ts | 2 +- ogl-engine/stories/Story3d.tsx | 6 +++--- ogl-engine/stories/billboard.stories.tsx | 4 ++-- ogl-engine/stories/lighting.stories.tsx | 4 ++-- ogl-engine/stories/mapControl.stories.tsx | 4 ++-- ogl-engine/stories/path.stories.tsx | 4 ++-- ui/components/MapOverlay/StrategicMap.tsx | 16 +++++---------- ui/components/TacticalMap/TacticalMap.tsx | 6 ++---- 17 files changed, 65 insertions(+), 54 deletions(-) diff --git a/core/systems/disposableUnregistering.ts b/core/systems/disposableUnregistering.ts index c5a23e4a..4d15fc5c 100644 --- a/core/systems/disposableUnregistering.ts +++ b/core/systems/disposableUnregistering.ts @@ -12,7 +12,7 @@ export class DisposableUnregisteringSystem extends System<"exec"> { this.exec(delta); } }); - NavigatingSystem.onTargetReached(this.constructor.name, (entity) => { + NavigatingSystem.onTargetReached((entity) => { if (entity.hasComponents(["disposable"])) { DisposableUnregisteringSystem.dispose(entity); } diff --git a/core/systems/navigating.ts b/core/systems/navigating.ts index 612c4860..c7efa983 100644 --- a/core/systems/navigating.ts +++ b/core/systems/navigating.ts @@ -3,8 +3,8 @@ import type { Driveable } from "@core/utils/moving"; import { clearTarget, startCruise, stopCruise } from "@core/utils/moving"; import { Vec2 } from "ogl"; import { entityIndexer } from "@core/entityIndexer/entityIndexer"; -import { Observable } from "@core/utils/observer"; import { random } from "mathjs"; +import { PubSubHook } from "@core/utils/pubsub"; import { defaultDriveLimit } from "../components/drive"; import type { Sim } from "../sim"; import type { RequireComponent } from "../tsHelpers"; @@ -69,7 +69,7 @@ const cruiseTimer = "cruise"; let navigatingSystem: NavigatingSystem; export class NavigatingSystem extends System { entities: Navigable[]; - hook: Observable = new Observable("onTargetReached"); + hook = new PubSubHook(); constructor() { super(); @@ -194,7 +194,7 @@ export class NavigatingSystem extends System { if (phase === "update") this.exec(delta); }); sim.hooks.subscribe("destroy", () => { - this.hook.observers.clear(); + this.hook.reset(); }); } @@ -213,7 +213,7 @@ export class NavigatingSystem extends System { return navigatingSystem; } - static onTargetReached(origin: string, fn: (_entity: Navigable) => void) { - return NavigatingSystem.getInstance().hook.subscribe(origin, fn); + static onTargetReached(fn: (_entity: Navigable) => void) { + return NavigatingSystem.getInstance().hook.subscribe(fn); } } diff --git a/core/utils/pubsub.ts b/core/utils/pubsub.ts index cf514a3b..fad3dc79 100644 --- a/core/utils/pubsub.ts +++ b/core/utils/pubsub.ts @@ -1,4 +1,4 @@ -export type EventHandler = (_data: T) => void; +export type EventHandler = (_data: T) => void; export class PubSub { private subscribers: Map>> = new Map(); @@ -30,3 +30,26 @@ export class PubSub { this.subscribers.clear(); } } + +export class PubSubHook { + private subscribers = new Set>(); + + subscribe(callback: EventHandler): () => void { + this.subscribers.add(callback); + return () => { + this.unsubscribe(callback); + }; + } + + unsubscribe(callback: EventHandler): void { + this.subscribers.delete(callback); + } + + publish(data: T): void { + this.subscribers.forEach((callback) => callback(data)); + } + + reset(): void { + this.subscribers.clear(); + } +} diff --git a/devtools/scenarios/dogfight.tsx b/devtools/scenarios/dogfight.tsx index 305cb855..059d18c4 100644 --- a/devtools/scenarios/dogfight.tsx +++ b/devtools/scenarios/dogfight.tsx @@ -39,7 +39,7 @@ export const Dogfight = () => { const [sim, setSim] = React.useState(null); const engine = React.useMemo(() => { const e = new Engine3D(); - e.hooks.onInit.subscribe("Dogfight", () => { + e.hooks.subscribe("init", () => { const asteroids = new Asteroids( window.renderer, 10, diff --git a/devtools/scenarios/frigateUnderAttack.tsx b/devtools/scenarios/frigateUnderAttack.tsx index e461ceff..895fd68d 100644 --- a/devtools/scenarios/frigateUnderAttack.tsx +++ b/devtools/scenarios/frigateUnderAttack.tsx @@ -36,7 +36,7 @@ export const FrigateUnderAttack = () => { const [sim, setSim] = React.useState(null); const engine = React.useMemo(() => { const e = new Engine3D(); - e.hooks.onInit.subscribe("FrigateUnderAttack", () => { + e.hooks.subscribe("init", () => { const asteroids = new Asteroids( window.renderer, 10, diff --git a/devtools/scenarios/stationUnderAttack.tsx b/devtools/scenarios/stationUnderAttack.tsx index 0b14edc9..c2bc480c 100644 --- a/devtools/scenarios/stationUnderAttack.tsx +++ b/devtools/scenarios/stationUnderAttack.tsx @@ -39,7 +39,7 @@ export const StationUnderAttack = () => { const [sim, setSim] = React.useState(null); const engine = React.useMemo(() => { const e = new Engine3D(); - e.hooks.onInit.subscribe("StationUnderAttack", () => { + e.hooks.subscribe("init", () => { const asteroids = new Asteroids( window.renderer, 10, diff --git a/ogl-engine/OglCanvas.tsx b/ogl-engine/OglCanvas.tsx index 2ab3ce86..d4c59931 100644 --- a/ogl-engine/OglCanvas.tsx +++ b/ogl-engine/OglCanvas.tsx @@ -19,10 +19,10 @@ export const OglCanvas: React.FC = React.memo(({ engine }) => { }; if (!canvas) return cleanup; - engine.hooks.onUpdate.subscribe("OglCanvas", () => { + engine.hooks.subscribe("update", () => { frameIdRef.current = requestAnimationFrame(engine.update.bind(engine)); }); - engine.hooks.onError.subscribe("OglCanvas", () => { + engine.hooks.subscribe("error", () => { setErrorCount((count) => count + 1); }); diff --git a/ogl-engine/engine/engine.ts b/ogl-engine/engine/engine.ts index 028b7baf..5b29b4b8 100644 --- a/ogl-engine/engine/engine.ts +++ b/ogl-engine/engine/engine.ts @@ -1,14 +1,15 @@ -import { Observable } from "@core/utils/observer"; import { Renderer } from "ogl"; +import { PubSub } from "@core/utils/pubsub"; import type { Scene } from "./Scene"; import type { Camera } from "./Camera"; +type InitEvent = { type: "init" }; +type UpdateEvent = { type: "update"; delta: number }; +type ErrorEvent = { type: "error"; error: Error }; +type EngineEvent = InitEvent | UpdateEvent | ErrorEvent; + export abstract class Engine { - public hooks: { - onInit: Observable; - onUpdate: Observable; - onError: Observable; - }; + public hooks: PubSub; public camera: Camera; protected canvas: HTMLCanvasElement | OffscreenCanvas; @@ -23,11 +24,7 @@ export abstract class Engine { protected deltaMultiplier = 1; constructor() { - this.hooks = { - onInit: new Observable("onInit"), - onUpdate: new Observable("onUpdate"), - onError: new Observable("onError"), - }; + this.hooks = new PubSub(); this.lastFrameTime = performance.now(); } @@ -62,17 +59,16 @@ export abstract class Engine { if (!this.initialized) { throw new Error("Engine not initialized"); } - const now = performance.now(); this.originalDelta = (now - this.lastFrameTime) / 1000; - this.hooks.onUpdate.notify(this.delta); + this.hooks.publish({ type: "update", delta: this.delta }); try { if (this.isFocused()) { this.render(); } } catch (err) { - this.hooks.onError.notify(err); + this.hooks.publish({ type: "error", error: err }); throw err; } diff --git a/ogl-engine/engine/engine2d.ts b/ogl-engine/engine/engine2d.ts index 8c8a45cd..fcfc372f 100644 --- a/ogl-engine/engine/engine2d.ts +++ b/ogl-engine/engine/engine2d.ts @@ -26,7 +26,7 @@ export class Engine2D extends Engine { this.camera.near = settings.camera.near; this.camera.far = settings.camera.far; - this.hooks.onInit.notify(); + this.hooks.publish({ type: "init" }); this.initialized = true; } diff --git a/ogl-engine/engine/engine3d.ts b/ogl-engine/engine/engine3d.ts index 214274c1..3f7ce96f 100644 --- a/ogl-engine/engine/engine3d.ts +++ b/ogl-engine/engine/engine3d.ts @@ -127,7 +127,7 @@ export class Engine3D extends Engine { this.initPostProcessing(); window.renderer = this; - this.hooks.onInit.notify(); + this.hooks.publish({ type: "init" }); this.initialized = true; }; diff --git a/ogl-engine/stories/Story3d.tsx b/ogl-engine/stories/Story3d.tsx index 9b6b826e..274f9801 100644 --- a/ogl-engine/stories/Story3d.tsx +++ b/ogl-engine/stories/Story3d.tsx @@ -44,7 +44,7 @@ export const Story3d: React.FC = ({ }, [pane]); React.useEffect(() => { - engine.hooks.onInit.subscribe("Story3d", () => { + engine.hooks.subscribe("init", () => { onEngineInit(engine); controlRef.current = @@ -55,9 +55,9 @@ export const Story3d: React.FC = ({ skyboxRef.current.setParent(engine.scene); }); - engine.hooks.onUpdate.subscribe("Story3d", (time) => { + engine.hooks.subscribe("update", ({ delta }) => { controlRef.current?.update(engine.originalDelta); - onEngineUpdate(engine, time); + onEngineUpdate(engine, delta); }); }, [engine]); diff --git a/ogl-engine/stories/billboard.stories.tsx b/ogl-engine/stories/billboard.stories.tsx index 486ea547..b4c619d2 100644 --- a/ogl-engine/stories/billboard.stories.tsx +++ b/ogl-engine/stories/billboard.stories.tsx @@ -19,7 +19,7 @@ const BillboardStory: React.FC<{ const billboardRef = React.useRef>(); React.useEffect(() => { - engine.hooks.onInit.subscribe("BillboardStory", async () => { + engine.hooks.subscribe("init", async () => { controlRef.current = new Orbit(engine.camera); skyboxRef.current = new Skybox(engine, "example"); @@ -39,7 +39,7 @@ const BillboardStory: React.FC<{ img.src = arrowDownFat; }); - engine.hooks.onUpdate.subscribe("BillboardStory", () => { + engine.hooks.subscribe("update", () => { controlRef.current!.update(); }); }, [engine]); diff --git a/ogl-engine/stories/lighting.stories.tsx b/ogl-engine/stories/lighting.stories.tsx index fe262b7f..6c5e8e21 100644 --- a/ogl-engine/stories/lighting.stories.tsx +++ b/ogl-engine/stories/lighting.stories.tsx @@ -28,7 +28,7 @@ const LightingStory: React.FC<{ const radiusRef = React.useRef(radius); React.useEffect(() => { - engine.hooks.onInit.subscribe("LightingStory", async () => { + engine.hooks.subscribe("init", async () => { engine.setScene(new Scene(engine)); engine.camera.position.set(2); controlRef.current = new Orbit(engine.camera); @@ -58,7 +58,7 @@ const LightingStory: React.FC<{ img.src = arrowDownFat; }); - engine.hooks.onUpdate.subscribe("LightingStory", () => { + engine.hooks.subscribe("update", () => { for (let i = 0; i < lightsRef.current.length; i++) { lightsRef.current[i].position.x = Math.sin(engine.uniforms.uTime.value + i * Math.PI) * diff --git a/ogl-engine/stories/mapControl.stories.tsx b/ogl-engine/stories/mapControl.stories.tsx index 801d31e8..1657f748 100644 --- a/ogl-engine/stories/mapControl.stories.tsx +++ b/ogl-engine/stories/mapControl.stories.tsx @@ -17,7 +17,7 @@ const ModelStory: React.FC = () => { const skyboxRef = React.useRef(); React.useEffect(() => { - engine.hooks.onInit.subscribe("MapControlStory", async () => { + engine.hooks.subscribe("init", async () => { engine.setScene(new TacticalMapScene(engine)); controlRef.current = new MapControl(engine.camera, engine.canvas); const helper = new AxesHelper(engine.gl, {}); @@ -33,7 +33,7 @@ const ModelStory: React.FC = () => { engine.scene.addChild(mesh); }); - engine.hooks.onUpdate.subscribe("MapControlStory", () => { + engine.hooks.subscribe("update", () => { controlRef.current!.update(engine.originalDelta); }); }, [engine]); diff --git a/ogl-engine/stories/path.stories.tsx b/ogl-engine/stories/path.stories.tsx index 4144282a..36f60e9f 100644 --- a/ogl-engine/stories/path.stories.tsx +++ b/ogl-engine/stories/path.stories.tsx @@ -25,7 +25,7 @@ const PathStory: React.FC = () => { const pathRef = React.useRef(); React.useEffect(() => { - engine.hooks.onInit.subscribe("MapControlStory", async () => { + engine.hooks.subscribe("init", async () => { engine.setScene(new Scene(engine)); controlRef.current = new MapControl(engine.camera, engine.canvas); skyboxRef.current = new Skybox(engine, "example"); @@ -36,7 +36,7 @@ const PathStory: React.FC = () => { engine.scene.addChild(pathRef.current); }); - engine.hooks.onUpdate.subscribe("MapControlStory", () => { + engine.hooks.subscribe("update", () => { pathRef.current?.update(waypoints); controlRef.current!.update(engine.originalDelta); }); diff --git a/ui/components/MapOverlay/StrategicMap.tsx b/ui/components/MapOverlay/StrategicMap.tsx index 94e7f902..5540ea8f 100644 --- a/ui/components/MapOverlay/StrategicMap.tsx +++ b/ui/components/MapOverlay/StrategicMap.tsx @@ -23,7 +23,6 @@ function isDestroyable(mesh: Transform): mesh is Transform & Destroyable { } const tempVec2 = new Vec2(); -// const tempVec3 = new Vec3(); function sign(p1: Vec2, p2: Vec2, p3: Vec2) { return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); @@ -77,7 +76,6 @@ export class StrategicMap extends React.PureComponent { mouseWorldPos = new Vec2(); lastClick: number; sectorSizes: Record = {}; - // raycastHits: BaseMesh2D[]; onUnmountCallbacks: (() => void)[] = []; @@ -85,15 +83,16 @@ export class StrategicMap extends React.PureComponent { super(props); this.engine = new StrategicMapEngine(); this.sim = props.sim; - // this.raycastHits = []; } componentDidMount(): void { const onInit = this.onInit.bind(this); const onUpdate = this.onUpdate.bind(this); - this.engine.hooks.onInit.subscribe("StrategicMap", onInit); - this.engine.hooks.onUpdate.subscribe("StrategicMap", onUpdate); + this.onUnmountCallbacks.push( + this.engine.hooks.subscribe("init", onInit), + this.engine.hooks.subscribe("update", onUpdate) + ); this.updateSectorSizes(); const sectorSizeInterval = setInterval( @@ -101,12 +100,7 @@ export class StrategicMap extends React.PureComponent { 1000 ); - this.onUnmountCallbacks.push(() => { - this.engine.hooks.onInit.unsubscribe(onInit); - this.engine.hooks.onUpdate.unsubscribe(onUpdate); - - clearInterval(sectorSizeInterval); - }); + this.onUnmountCallbacks.push(() => clearInterval(sectorSizeInterval)); } componentWillUnmount(): void { diff --git a/ui/components/TacticalMap/TacticalMap.tsx b/ui/components/TacticalMap/TacticalMap.tsx index 2ec00ccb..6c7f0cf6 100644 --- a/ui/components/TacticalMap/TacticalMap.tsx +++ b/ui/components/TacticalMap/TacticalMap.tsx @@ -103,12 +103,10 @@ export class TacticalMap extends React.PureComponent { ) ?? this.sim.index.sectors.get()[0] ); - this.engine.hooks.onInit.subscribe("TacticalMap", () => { + this.engine.hooks.subscribe("init", () => { this.onEngineInit(); }); - this.engine.hooks.onUpdate.subscribe("TacticalMap", () => - this.onEngineUpdate() - ); + this.engine.hooks.subscribe("update", () => this.onEngineUpdate()); this.onUnmountCallbacks.push( reaction( () => gameStore.sector,