247 lines
8.5 KiB
JavaScript
247 lines
8.5 KiB
JavaScript
// Serialisation: turning the live model into the snapshots the server ships to
|
|
// each viewer, with fog-of-war applied per civilisation.
|
|
|
|
import { parseKey } from "../hex.js";
|
|
import { serializeSet } from "./helpers.js";
|
|
|
|
export const serializationMethods = {
|
|
snapshot(viewerCiv = -1) {
|
|
return this.viewerSnapshot(this.serializeShared(), viewerCiv);
|
|
},
|
|
|
|
serializeShared() {
|
|
return {
|
|
seed: this.seed,
|
|
totalHours: this.totalHours,
|
|
civs: this.civilisations.map((c) => ({ id: c.id, name: c.name })),
|
|
protos: this.protoUnits.map((p) => ({ id: p.id, name: p.name })),
|
|
buildings: this.protoBuildings.map((b) => ({ id: b.id, name: b.name })),
|
|
governments: this.governments.map((g) => ({ id: g.id, name: g.name })),
|
|
technologies: this.technologies.map((t) => ({ id: t.id, name: t.name })),
|
|
cities: this._serializeCities(),
|
|
units: this._serializeUnits(),
|
|
territory: this._serializedTerritory,
|
|
population: this._serializePopulation(),
|
|
// Sparse per-tile marks of war, so the browser can explain a tile's
|
|
// GDP per capita without shipping the whole breakdown for every tile.
|
|
tileGdpPenalties: this._serializeTileValues(this.tileGdpPenalty),
|
|
tileBattleGdpDeficits: this._serializeTileValues(this.tileBattleGdpDeficit),
|
|
civStats: this._serializeCivStats(),
|
|
budgets: this._serializeCivValues(this.budgets),
|
|
research: this._serializeCivValues(this.research),
|
|
culture: this._serializeCivValues(this.culture),
|
|
government: this._serializeCivValues(this.government),
|
|
researched: this._serializeResearched(),
|
|
diplomacy: this._serializeDiplomacy(),
|
|
conflicts: this._serializeConflicts(),
|
|
news: this._serializeNews(),
|
|
battles: this._serializeBattles(),
|
|
training: Object.fromEntries(
|
|
Array.from(this.training.entries()).map(([cityId, queue]) => [
|
|
cityId,
|
|
queue.map((entry) => ({ ...entry })),
|
|
])
|
|
),
|
|
};
|
|
},
|
|
|
|
viewerSnapshot(shared, viewerCiv, stats = null) {
|
|
const result = { ...shared };
|
|
result.viewer = viewerCiv;
|
|
result.viewerStats = stats || this.viewerStats(viewerCiv);
|
|
result.explored = this._cachedExplored(viewerCiv);
|
|
result.visible = serializeSet(this.visible.get(viewerCiv) || new Set());
|
|
return result;
|
|
},
|
|
|
|
viewerStats(viewerCiv) {
|
|
if (viewerCiv < 0) {
|
|
return {
|
|
population: 0, gdp: 0, gdpPerCapita: 0, budget: 0,
|
|
upkeep: 0, research: 0, culture: 0, government: 0,
|
|
breakdown: null,
|
|
};
|
|
}
|
|
const headline = this._civHeadline(viewerCiv);
|
|
return {
|
|
...headline,
|
|
budget: this.getBudget(viewerCiv),
|
|
upkeep: this.getPlayerUpkeep(viewerCiv),
|
|
research: this.getResearch(viewerCiv),
|
|
culture: this.getCulture(viewerCiv),
|
|
government: this.getGovernment(viewerCiv),
|
|
breakdown: this.budgetBreakdown(viewerCiv),
|
|
};
|
|
},
|
|
|
|
_serializeCities() {
|
|
return this.cities.map((city) => ({
|
|
id: city.id,
|
|
civ: city.civ,
|
|
coords: [city.coords.x, city.coords.y],
|
|
name: city.name,
|
|
isCapital: city.isCapital,
|
|
population: city.population,
|
|
improvements: city.improvements,
|
|
buildings: { ...city.buildings },
|
|
statuses: city.statuses || [],
|
|
}));
|
|
},
|
|
|
|
_serializeUnits() {
|
|
const battle = this._battleUnitIds();
|
|
return this.units.map((unit) => {
|
|
const rawPath = unit.path || [];
|
|
const pathIndex = unit.pathIndex;
|
|
const path = rawPath.map((coords) => [coords.x, coords.y]);
|
|
const segmentHours = [];
|
|
for (let i = 1; i < rawPath.length; i++) {
|
|
segmentHours.push(this._tileTravelHours(unit, rawPath[i]));
|
|
}
|
|
let stepHours = 0;
|
|
if (pathIndex + 1 < rawPath.length) {
|
|
stepHours = this._tileTravelHours(unit, rawPath[pathIndex + 1]);
|
|
}
|
|
return {
|
|
id: unit.id,
|
|
civ: unit.civ,
|
|
proto: unit.proto,
|
|
coords: [unit.coords.x, unit.coords.y],
|
|
hp: unit.hp,
|
|
maxHp: unit.maxHp,
|
|
statuses: this._effectiveStatuses(unit, battle.has(unit.id)),
|
|
path,
|
|
pathIndex,
|
|
progressHours: unit.progressHours,
|
|
stepHours,
|
|
segmentHours,
|
|
waypoints: (unit.waypoints || []).map((coords) => [coords.x, coords.y]),
|
|
};
|
|
});
|
|
},
|
|
|
|
_serializePopulation() {
|
|
const result = [];
|
|
for (const [k, value] of this.tilePopulation) {
|
|
const coords = parseKey(k);
|
|
result.push([coords.x, coords.y, Math.round(value)]);
|
|
}
|
|
return result;
|
|
},
|
|
|
|
// A sparse [x, y, value] list for a per-tile map, used for the war marks.
|
|
_serializeTileValues(values) {
|
|
const result = [];
|
|
for (const [k, value] of values) {
|
|
const coords = parseKey(k);
|
|
result.push([coords.x, coords.y, value]);
|
|
}
|
|
return result;
|
|
},
|
|
|
|
// One entry per civilisation with its headline economy figures, so the
|
|
// diplomacy panel can show every nation's standing without a second lookup.
|
|
_serializeCivStats() {
|
|
return this.civilisations.map((_, index) => this._civHeadline(index));
|
|
},
|
|
|
|
// Population, GDP and GDP per capita for one civilisation: the headline
|
|
// figures the diplomacy panel and the viewer's own stats both show.
|
|
_civHeadline(civ) {
|
|
const aggregates = this._playerAggregates(civ);
|
|
const population = Math.round(aggregates.population);
|
|
const gdp = aggregates.gdp;
|
|
return { population, gdp, gdpPerCapita: population > 0 ? gdp / population : 0 };
|
|
},
|
|
|
|
_serializeCivValues(values) {
|
|
const result = [];
|
|
for (let i = 0; i < this.civilisations.length; i++) {
|
|
result.push(values.has(i) ? values.get(i) : 0);
|
|
}
|
|
return result;
|
|
},
|
|
|
|
_serializeResearched() {
|
|
const result = [];
|
|
for (let i = 0; i < this.civilisations.length; i++) {
|
|
const set = this.researched.get(i) || new Set();
|
|
const indices = Array.from(set).sort((a, b) => a - b);
|
|
result.push(indices);
|
|
}
|
|
return result;
|
|
},
|
|
|
|
// Every unordered pair with its current status. With a handful of
|
|
// civilisations the whole matrix is tiny, and it lets the client show the
|
|
// status of every nation without a second lookup.
|
|
_serializeDiplomacy() {
|
|
const result = [];
|
|
for (let a = 0; a < this.civilisations.length; a++) {
|
|
for (let b = a + 1; b < this.civilisations.length; b++) {
|
|
result.push([a, b, this.getRelation(a, b)]);
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
// Every ongoing war with its start date and the casualties each side has
|
|
// suffered, broken down into military and civilian dead.
|
|
_serializeConflicts() {
|
|
const result = [];
|
|
for (const conflict of this.conflicts.values()) {
|
|
result.push({
|
|
a: conflict.a,
|
|
b: conflict.b,
|
|
started: conflict.startDate,
|
|
startHours: conflict.startHours,
|
|
sides: [conflict.a, conflict.b].map((civ) => ({
|
|
civ,
|
|
military: conflict.military.get(civ) || 0,
|
|
civilians: Math.round(conflict.civilians.get(civ) || 0),
|
|
})),
|
|
});
|
|
}
|
|
return result;
|
|
},
|
|
|
|
_serializeNews() {
|
|
return this.news.map((entry) => ({ ...entry }));
|
|
},
|
|
|
|
// One entry per battlefield: [x, y, defenderCiv, attackerCiv]. The tile's
|
|
// owner is the defender when present; otherwise the lowest-numbered civ on
|
|
// the tile defends. The browser uses this to draw the two stacks facing each
|
|
// other with the battle icon between them.
|
|
_serializeBattles() {
|
|
const result = [];
|
|
for (const battle of this._battleTiles()) {
|
|
const civs = Array.from(battle.civs).sort((a, b) => a - b);
|
|
const owner = this.civAt(battle.coords);
|
|
const defender = this._defenderCiv(battle, civs, owner);
|
|
const attacker = civs.find((civ) => civ !== defender);
|
|
if (attacker === undefined) continue;
|
|
result.push([battle.coords.x, battle.coords.y, defender, attacker]);
|
|
}
|
|
return result;
|
|
},
|
|
|
|
// The side that charged in is the attacker; the occupants defend. When that is
|
|
// ambiguous (a mutual charge, or a third party) fall back to the tile's owner
|
|
// and then to the lowest-numbered civ present.
|
|
_defenderCiv(battle, civs, owner) {
|
|
const invaders = new Set();
|
|
let hasDefender = false;
|
|
for (const unit of battle.units) {
|
|
if (unit.invader) invaders.add(unit.civ);
|
|
else hasDefender = true;
|
|
}
|
|
const defenders = civs.filter((civ) => !invaders.has(civ));
|
|
if (invaders.size > 0 && hasDefender && defenders.length > 0) {
|
|
if (defenders.length === 1) return defenders[0];
|
|
return defenders.includes(owner) ? owner : defenders[0];
|
|
}
|
|
return civs.includes(owner) ? owner : civs[0];
|
|
},
|
|
};
|