Implement tile state change handling in Game and Worker components

- Added an optional `onTileStateChanged` hook in the Game interface
- Refactored GameImpl to utilize the new hook for reporting tile state changes instead of emitting GameUpdateType.Tile updates.
- Updated Worker.worker.ts to integrate the new tile state change handling
This commit is contained in:
scamiv
2026-02-04 21:53:31 +01:00
parent 978b7fcd23
commit f6df2ccb18
4 changed files with 29 additions and 24 deletions
+2 -2
View File
@@ -108,8 +108,8 @@ export class TerritoryLayer implements Layer {
this.syncPaletteMaybe(now);
// Renderer tick and dirty-tile marking are driven in the worker from
// simulation-derived tile updates (tileUpdateSink). The main thread only
// drives render frames + view transforms.
// simulation-derived tile mutations (onTileStateChanged). The main thread
// only drives render frames + view transforms.
FrameProfiler.end("TerritoryLayer:tick", tickProfile);
}
+6
View File
@@ -733,6 +733,12 @@ export interface Game extends GameMap {
callback: (neighbor: TileRef) => void,
): void;
/**
* Optional hook for tile state changes. When set, tile mutations should call
* this instead of emitting GameUpdateType.Tile updates.
*/
onTileStateChanged?: (tile: TileRef) => void;
// Player Management
player(id: PlayerID): Player;
players(): Player[];
+18 -13
View File
@@ -84,6 +84,7 @@ export class GameImpl implements Game {
private updates: GameUpdates = createGameUpdatesMap();
private unitGrid: UnitGrid;
public onTileStateChanged?: (tile: TileRef) => void;
private playerTeams: Team[];
private botTeam: Team = ColoredTeams.Bot;
@@ -234,6 +235,17 @@ export class GameImpl implements Game {
(this.updates[update.type] as GameUpdate[]).push(update);
}
private reportTileStateChanged(tile: TileRef): void {
if (this.onTileStateChanged) {
this.onTileStateChanged(tile);
return;
}
this.addUpdate({
type: GameUpdateType.Tile,
update: this.toTileUpdate(tile),
});
}
nextUnitID(): number {
const old = this._nextUnitID;
this._nextUnitID++;
@@ -248,10 +260,7 @@ export class GameImpl implements Game {
return;
}
this._map.setFallout(tile, value);
this.addUpdate({
type: GameUpdateType.Tile,
update: this.toTileUpdate(tile),
});
this.reportTileStateChanged(tile);
}
units(...types: UnitType[]): Unit[] {
@@ -594,10 +603,7 @@ export class GameImpl implements Game {
owner._lastTileChange = this._ticks;
this.updateBorders(tile);
this._map.setFallout(tile, false);
this.addUpdate({
type: GameUpdateType.Tile,
update: this.toTileUpdate(tile),
});
this.reportTileStateChanged(tile);
}
relinquish(tile: TileRef) {
@@ -615,10 +621,7 @@ export class GameImpl implements Game {
this._map.setOwnerID(tile, 0);
this.updateBorders(tile);
this.addUpdate({
type: GameUpdateType.Tile,
update: this.toTileUpdate(tile),
});
this.reportTileStateChanged(tile);
}
private updateBorders(tile: TileRef) {
@@ -951,7 +954,8 @@ export class GameImpl implements Game {
return this._map.hasOwner(ref);
}
setOwnerID(ref: TileRef, playerId: number): void {
return this._map.setOwnerID(ref, playerId);
this._map.setOwnerID(ref, playerId);
this.reportTileStateChanged(ref);
}
hasFallout(ref: TileRef): boolean {
return this._map.hasFallout(ref);
@@ -961,6 +965,7 @@ export class GameImpl implements Game {
}
setDefended(ref: TileRef, value: boolean): void {
this._map.setDefended(ref, value);
this.reportTileStateChanged(ref);
}
isBorder(ref: TileRef): boolean {
return this._map.isBorder(ref);
+3 -9
View File
@@ -4,7 +4,6 @@ import { PastelTheme } from "../configuration/PastelTheme";
import { PastelThemeDark } from "../configuration/PastelThemeDark";
import { FetchGameMapLoader } from "../game/FetchGameMapLoader";
import { PlayerID } from "../game/Game";
import { TileRef } from "../game/GameMap";
import {
AllianceExpiredUpdate,
AllianceRequestReplyUpdate,
@@ -575,9 +574,9 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
// Capacity is bounded; on overflow we fall back to markAllDirty().
dirtyTiles = new DirtyTileQueue(numTiles, Math.max(4096, numTiles));
dirtyTilesOverflow = false;
renderTileState = new Uint16Array(gr.game.tileStateView());
renderTileState = gr.game.tileStateView();
gr.tileUpdateSink = (packedUpdate) => {
gr.game.onTileStateChanged = (tile) => {
if (!dirtyTiles) {
return;
}
@@ -585,11 +584,6 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
return;
}
const tile = Number(packedUpdate >> 16n) as TileRef;
const state = Number(packedUpdate & 0xffffn);
if (renderTileState) {
renderTileState[tile] = state;
}
const mark = (t: any) => {
if (!dirtyTiles!.mark(t)) {
dirtyTilesOverflow = true;
@@ -798,7 +792,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
? new WorkerCanvas2DRenderer()
: new WorkerTerritoryRenderer();
renderTileState ??= new Uint16Array(gr.game.tileStateView());
renderTileState ??= gr.game.tileStateView();
await renderer.init(
message.offscreenCanvas,
gr,