Sped up large battles and trimmed snapshot traffic

- The bombardment overlay was rebuilt every animation frame, and each barrage
  scanned the whole army to find its target (O(strikes x units)). It now
  redraws at most at the renderer's 24 fps, reuses its SVG/DOM nodes, and
  resolves garrisons from one per-snapshot tile index, so a high-refresh
  display no longer makes a big battle worse.
- Route truncation had the same shape: every step of every route rescanned
  every unit. The snapshot now buckets the army by tile once.
- Idle units no longer have identical left/top rewritten every frame, and the
  whole-map fog buffer is rebuilt less often.
- Snapshot traffic: cityStats and visible are delta-encoded like territory and
  population, city stats carry a content-hash version, tile ethnicity is
  memoised, and a city's economy is derived once per broadcast instead of
  twice. A per-peer civ change now forces a full snapshot, since the visible
  set is per-viewer.
This commit is contained in:
2026-09-21 02:23:04 +02:00
parent dc0432a60a
commit b2ab4d76eb
11 changed files with 419 additions and 77 deletions
+36 -3
View File
@@ -12,6 +12,10 @@ export const serializationMethods = {
},
serializeShared() {
// Built once and shared by every viewer. Its content hash doubles as the
// version that lets the server omit it from a delta when nothing moved, so
// a city panel is not re-sent on every 10 Hz broadcast.
const cityStats = this._serializeCityStats();
return {
seed: this.seed,
// The world-generation config the seed was generated with. The browser
@@ -48,7 +52,7 @@ export const serializationMethods = {
tileGdpPenalties: this._serializeTileValues(this.tileGdpPenalty),
tileBattleGdpDeficits: this._serializeTileValues(this.tileBattleGdpDeficit),
civStats: this._serializeCivStats(),
cityStats: this._serializeCityStats(),
cityStats: cityStats,
budgets: this._serializeCivValues(this.budgets),
culture: this._serializeCivValues(this.culture),
// Each nation's share of the world's cultural output (zero-sum) and the
@@ -102,6 +106,8 @@ export const serializationMethods = {
ethnicity: this._ethnicityVersion,
improvements: this._improvementVersion,
warfare: this._tileImprovementVersion,
cityStats: hashCityStats(cityStats),
visible: this._visibleVersion,
},
};
},
@@ -319,7 +325,7 @@ export const serializationMethods = {
approval: economy.approval,
income: economy.income,
// The region's own income and building upkeep, for the city budget tab.
budget: this.cityBudgetBreakdown(city),
budget: this.cityBudgetBreakdown(city, economy),
// The ethnicity of the region's people, for the circle graph.
ethnicMakeup: this.getCityEthnicMakeup(city),
};
@@ -509,8 +515,17 @@ export const serializationMethods = {
},
// Sparse [x, y, [[ethnicity, share, trend], ...]] for every populated tile,
// where trend is the net migration over the last day.
// where trend is the net migration over the last day. Memoised against the
// ethnicity version and the current migration delta store: the shares only
// move when politics does, so re-walking every populated tile on every 10 Hz
// broadcast was pure waste (the delta wire format drops it anyway).
_serializeTileEthnicity() {
const cache = this._serializedTileEthnicityCache;
if (cache &&
cache.version === this._ethnicityVersion &&
cache.deltas === this._migrationDeltas.tile) {
return cache.value;
}
const result = [];
for (const [k, shares] of this.tileEthnicity) {
const coords = parseKey(k);
@@ -526,6 +541,11 @@ export const serializationMethods = {
if (entries.length === 0) continue;
result.push([coords.x, coords.y, entries]);
}
this._serializedTileEthnicityCache = {
version: this._ethnicityVersion,
deltas: this._migrationDeltas.tile,
value: result,
};
return result;
},
@@ -606,6 +626,19 @@ function roundTo(value, digits) {
return Math.round(value * factor) / factor;
}
// A cheap, stable fingerprint of the serialised city stats. Comparing it lets
// the server treat cityStats like the other versioned collections without
// having to hook every economy-affecting mutation (pillage, battle scars,
// approval drift, research) individually.
function hashCityStats(stats) {
const text = JSON.stringify(stats);
let hash = 2166136261;
for (let i = 0; i < text.length; i++) {
hash = Math.imul(hash ^ text.charCodeAt(i), 16777619);
}
return `${text.length}:${(hash >>> 0).toString(36)}`;
}
// A Map<ethnicity, number> as sorted [ethnicity, roundedNumber] pairs, dropping
// zero entries.
function mapToEntries(map) {