Refactoring and clean-up
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
// Camera, zoom/LOD switching, pointer input, selection and screen-to-tile
|
||||
// hit-testing.
|
||||
|
||||
import { mapToLocal, key, COL_STEP, HEX_W, HEX_H } from "../../../shared/hex.js";
|
||||
import { mapToLocal, key, parity, COL_STEP, HEX_W, HEX_H } from "../../../shared/hex.js";
|
||||
import { DRAG_THRESHOLD, ZOOM_MIN, ZOOM_MAX } from "./constants.js";
|
||||
import { clamp, parity } from "./utils.js";
|
||||
import { clamp } from "./utils.js";
|
||||
|
||||
export const inputMethods = {
|
||||
// ------------------------------------------------------------- camera --
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import { mapToLocal, key } from "../../../shared/hex.js";
|
||||
import { HexPathfinder } from "../../../shared/hex_pathfinder.js";
|
||||
import { CONTROLLED_SPEED_MULTIPLIER } from "../../../shared/game_state/constants.js";
|
||||
import { RESYNC_HOURS, PREDICTION_GRACE_HOURS } from "./constants.js";
|
||||
import { hoursAlong, samePath, pathProgress, makeSegment } from "./utils.js";
|
||||
|
||||
@@ -247,7 +248,8 @@ export const motionMethods = {
|
||||
const costCache = new Map();
|
||||
const heuristicCache = new Map();
|
||||
const bestSpeed = Math.max(speed || proto.speed, 0.0001);
|
||||
const fastestHours = this.terrainStats.minMovementCostMultiplier() / (bestSpeed * 1.5);
|
||||
const fastestHours =
|
||||
this.terrainStats.minMovementCostMultiplier() / (bestSpeed * CONTROLLED_SPEED_MULTIPLIER);
|
||||
const iterator = new HexPathfinder().findPathIter(
|
||||
start,
|
||||
goal,
|
||||
@@ -398,7 +400,7 @@ export const motionMethods = {
|
||||
_stepHours(proto, civ, coords, speed = null) {
|
||||
const k = key(coords.x, coords.y);
|
||||
let effective = Math.max(speed || proto.speed, 0.0001);
|
||||
if (this.territory.get(k) === civ) effective *= 1.5;
|
||||
if (this.territory.get(k) === civ) effective *= CONTROLLED_SPEED_MULTIPLIER;
|
||||
return this.terrainStats.movementCostMultiplier(coords) / effective;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
import { HEX_W, HEX_H } from "../../../shared/hex.js";
|
||||
import { OVERVIEW_COLOURS, OVERVIEW_SEA } from "./constants.js";
|
||||
|
||||
export function parity(x) {
|
||||
return x & 1;
|
||||
}
|
||||
|
||||
// Order-independent FNV-style hash of an [x, y, ...] entry array, used to skip
|
||||
// rebuilding a layer when the snapshot did not actually change it.
|
||||
export function hashEntries(entries) {
|
||||
|
||||
+12
-18
@@ -120,14 +120,8 @@ export class GameServer extends EventEmitter {
|
||||
}
|
||||
|
||||
_handleMove(civ, order) {
|
||||
const ids = Array.isArray(order.units) ? order.units : [order.unit];
|
||||
const units = [];
|
||||
for (const id of ids) {
|
||||
const unit = this.state.findUnit(Number(id));
|
||||
if (!unit || unit.civ !== civ) return false;
|
||||
units.push(unit.id);
|
||||
}
|
||||
if (units.length === 0) return false;
|
||||
const units = this._ownUnitIds(civ, order, true);
|
||||
if (!units) return false;
|
||||
return this.state.requestGroupMove(units, coordsOf(order.coords));
|
||||
}
|
||||
|
||||
@@ -146,26 +140,26 @@ export class GameServer extends EventEmitter {
|
||||
return this.state.requestScheduledMove(units, goals, !!order.append);
|
||||
}
|
||||
|
||||
_ownUnitIds(civ, order) {
|
||||
// The caller's own unit ids. With `strict`, a single id that is unknown or
|
||||
// owned by another civilisation rejects the whole order (null); otherwise the
|
||||
// foreign ids are simply dropped.
|
||||
_ownUnitIds(civ, order, strict = false) {
|
||||
const ids = Array.isArray(order.units) ? order.units : [order.unit];
|
||||
const units = [];
|
||||
for (const id of ids) {
|
||||
const unit = this.state.findUnit(Number(id));
|
||||
if (!unit || unit.civ !== civ) continue;
|
||||
if (!unit || unit.civ !== civ) {
|
||||
if (strict) return null;
|
||||
continue;
|
||||
}
|
||||
units.push(unit.id);
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
_handlePillage(civ, order) {
|
||||
const ids = Array.isArray(order.units) ? order.units : [order.unit];
|
||||
const units = [];
|
||||
for (const id of ids) {
|
||||
const unit = this.state.findUnit(Number(id));
|
||||
if (!unit || unit.civ !== civ) return false;
|
||||
units.push(unit.id);
|
||||
}
|
||||
if (units.length === 0) return false;
|
||||
const units = this._ownUnitIds(civ, order, true);
|
||||
if (!units) return false;
|
||||
return this.state.requestPillage(units);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
export const RELATION_PEACE = "peace";
|
||||
export const RELATION_WAR = "war";
|
||||
export const RELATIONS = [RELATION_PEACE, RELATION_WAR];
|
||||
|
||||
// Kinds of news item shown in the feed. Newsworthy events are recorded in
|
||||
// GameState with their in-game hour so every client can render them.
|
||||
|
||||
+16
-43
@@ -73,7 +73,22 @@ export class GameState {
|
||||
this.tiles = {};
|
||||
this.terrainStats = null;
|
||||
this.landCells = [];
|
||||
this.totalHours = 0;
|
||||
|
||||
this._configured = false;
|
||||
this._generator = null;
|
||||
this._pathfinder = new HexPathfinder();
|
||||
this._minMovementCost = 1.0;
|
||||
this._maxStepLength = 1.0;
|
||||
this._listeners = [];
|
||||
|
||||
this._resetCollections();
|
||||
}
|
||||
|
||||
// Recreates every per-game collection, index and counter. Shared by the
|
||||
// constructor and `configure`, so a fresh instance and a restarted game
|
||||
// always start from exactly the same shape.
|
||||
_resetCollections() {
|
||||
this.cities = [];
|
||||
this.units = [];
|
||||
this.territory = new Map();
|
||||
@@ -100,13 +115,6 @@ export class GameState {
|
||||
this.conflicts = new Map();
|
||||
this.news = [];
|
||||
|
||||
this.totalHours = 0;
|
||||
|
||||
this._configured = false;
|
||||
this._generator = null;
|
||||
this._pathfinder = new HexPathfinder();
|
||||
this._minMovementCost = 1.0;
|
||||
this._maxStepLength = 1.0;
|
||||
this._nextUnitId = 1;
|
||||
this._nextCityId = 1;
|
||||
this._nextNewsId = 1;
|
||||
@@ -126,8 +134,6 @@ export class GameState {
|
||||
this._serializedExplored = new Map();
|
||||
this._neighbourCache = new Map();
|
||||
this._visibilityDirty = true;
|
||||
|
||||
this._listeners = [];
|
||||
}
|
||||
|
||||
onChanged(fn) {
|
||||
@@ -157,40 +163,7 @@ export class GameState {
|
||||
this._minMovementCost = this.terrainStats.minMovementCostMultiplier();
|
||||
this._maxStepLength = this._computeMaxStepLength();
|
||||
|
||||
this.cities = [];
|
||||
this.units = [];
|
||||
this.territory = new Map();
|
||||
this.tilePopulation = new Map();
|
||||
this.tileGdpPenalty = new Map();
|
||||
this.tileBattleGdpDeficit = new Map();
|
||||
this.tileBattleRecoveryStart = new Map();
|
||||
this.budgets = new Map();
|
||||
this.training = new Map();
|
||||
this.research = new Map();
|
||||
this.culture = new Map();
|
||||
this.government = new Map();
|
||||
this.researched = new Map();
|
||||
this.wars = new Set();
|
||||
this.conflicts = new Map();
|
||||
this.news = [];
|
||||
this._nextNewsId = 1;
|
||||
this._nextUnitId = 1;
|
||||
this._nextCityId = 1;
|
||||
this._unitById = new Map();
|
||||
this._cityById = new Map();
|
||||
this._cityNamesUsed = new Map();
|
||||
this._usedCityNames = new Set();
|
||||
this._cityByCoords = new Map();
|
||||
this._cityAdjacent = new Map();
|
||||
this._territoryByCiv = new Map();
|
||||
this._gdpPerCapitaCache = new Map();
|
||||
this._populationBalance = new Map();
|
||||
this._perCapitaBalance = new Map();
|
||||
this._serializedTerritory = [];
|
||||
this._serializedExplored = new Map();
|
||||
this._neighbourCache = new Map();
|
||||
this.explored = new Map();
|
||||
this.visible = new Map();
|
||||
this._resetCollections();
|
||||
for (let i = 0; i < this.civilisations.length; i++) {
|
||||
this.research.set(i, 0);
|
||||
this.culture.set(i, 0);
|
||||
|
||||
@@ -7,9 +7,9 @@ export const MAX_NEWS = 100;
|
||||
|
||||
export const UNIT_VISION = 2;
|
||||
export const TERRITORY_VISION = 1;
|
||||
export const HOURS_PER_DAY = 24;
|
||||
export const DAYS_PER_YEAR = 365;
|
||||
export const HOURS_PER_YEAR = HOURS_PER_DAY * DAYS_PER_YEAR;
|
||||
// The calendar lives in one place: re-exported so model code can keep
|
||||
// importing every simulation constant from this module.
|
||||
export { HOURS_PER_DAY, DAYS_PER_YEAR, HOURS_PER_YEAR } from "../game_clock.js";
|
||||
export const CONTROLLED_SPEED_MULTIPLIER = 1.5;
|
||||
export const DEFAULT_UNIT_SPEED = 0.5;
|
||||
export const MIN_STEP_COST = 0.0001;
|
||||
|
||||
@@ -9,7 +9,12 @@ import {
|
||||
EFFECT_CULTURE,
|
||||
} from "../data/effects.js";
|
||||
import { CITY_GARRISON_HEAL_PER_HOUR } from "../data/combat.js";
|
||||
import { buildingUpkeep, prerequisitesMet } from "../rules.js";
|
||||
import {
|
||||
buildingUpkeep,
|
||||
governmentEffects,
|
||||
prerequisitesMet,
|
||||
technologyEffects,
|
||||
} from "../rules.js";
|
||||
import { key } from "../hex.js";
|
||||
import { HOURS_PER_DAY, HOURS_PER_YEAR } from "./constants.js";
|
||||
import { effectAt, mergeModifiers } from "./helpers.js";
|
||||
@@ -232,13 +237,8 @@ export const economyMethods = {
|
||||
},
|
||||
|
||||
getCivGovernmentModifiers(civ) {
|
||||
const totals = {};
|
||||
const resource = this.governments[this.getGovernment(civ)];
|
||||
if (!resource) return totals;
|
||||
for (const e of resource.effects) {
|
||||
totals[e.stat] = (totals[e.stat] || 0) + effectAt(e, 1);
|
||||
}
|
||||
return totals;
|
||||
return resource ? governmentEffects(resource) : {};
|
||||
},
|
||||
|
||||
getCivModifiers(civ) {
|
||||
@@ -293,9 +293,7 @@ export const economyMethods = {
|
||||
const totals = {};
|
||||
const set = this.researched.get(civ) || new Set();
|
||||
for (const index of set) {
|
||||
for (const e of this.technologies[index].effects) {
|
||||
totals[e.stat] = (totals[e.stat] || 0) + effectAt(e, 1);
|
||||
}
|
||||
mergeModifiers(totals, technologyEffects(this.technologies[index]));
|
||||
}
|
||||
return totals;
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Serialisation: turning the live model into the snapshots the server ships to
|
||||
// each viewer, with fog-of-war applied per civilisation.
|
||||
|
||||
import { key, parseKey } from "../hex.js";
|
||||
import { parseKey } from "../hex.js";
|
||||
import { serializeSet } from "./helpers.js";
|
||||
|
||||
export const serializationMethods = {
|
||||
@@ -23,10 +23,10 @@ export const serializationMethods = {
|
||||
territory: this._serializedTerritory,
|
||||
population: this._serializePopulation(),
|
||||
civStats: this._serializeCivStats(),
|
||||
budgets: this._serializeCivFloats(this.budgets),
|
||||
research: this._serializeCivFloats(this.research),
|
||||
culture: this._serializeCivFloats(this.culture),
|
||||
government: this._serializeCivInts(this.government),
|
||||
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(),
|
||||
@@ -58,15 +58,11 @@ export const serializationMethods = {
|
||||
breakdown: null,
|
||||
};
|
||||
}
|
||||
const aggregates = this._playerAggregates(viewerCiv);
|
||||
const population = Math.round(aggregates.population);
|
||||
const gdp = aggregates.gdp;
|
||||
const headline = this._civHeadline(viewerCiv);
|
||||
return {
|
||||
population,
|
||||
gdp,
|
||||
gdpPerCapita: population > 0 ? gdp / population : 0,
|
||||
...headline,
|
||||
budget: this.getBudget(viewerCiv),
|
||||
upkeep: this.getPlayerUpkeep(viewerCiv, gdp),
|
||||
upkeep: this.getPlayerUpkeep(viewerCiv, headline.gdp),
|
||||
research: this.getResearch(viewerCiv),
|
||||
culture: this.getCulture(viewerCiv),
|
||||
government: this.getGovernment(viewerCiv),
|
||||
@@ -132,27 +128,19 @@ export const serializationMethods = {
|
||||
// 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((civ, index) => {
|
||||
const aggregates = this._playerAggregates(index);
|
||||
const population = Math.round(aggregates.population);
|
||||
const gdp = aggregates.gdp;
|
||||
return {
|
||||
population,
|
||||
gdp,
|
||||
gdpPerCapita: population > 0 ? gdp / population : 0,
|
||||
};
|
||||
});
|
||||
return this.civilisations.map((_, index) => this._civHeadline(index));
|
||||
},
|
||||
|
||||
_serializeCivFloats(values) {
|
||||
const result = [];
|
||||
for (let i = 0; i < this.civilisations.length; i++) {
|
||||
result.push(values.has(i) ? values.get(i) : 0);
|
||||
}
|
||||
return result;
|
||||
// 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 };
|
||||
},
|
||||
|
||||
_serializeCivInts(values) {
|
||||
_serializeCivValues(values) {
|
||||
const result = [];
|
||||
for (let i = 0; i < this.civilisations.length; i++) {
|
||||
result.push(values.has(i) ? values.get(i) : 0);
|
||||
|
||||
@@ -35,15 +35,17 @@ export const worldMethods = {
|
||||
const civ = this.civilisations[civIndex];
|
||||
const coords = site.coords;
|
||||
const isCapital = site.isCapital;
|
||||
const description = this._cityDescriptionFor(civ, isCapital, coords);
|
||||
const name = this._nextCityName(civ, civIndex);
|
||||
const population = 500000 +
|
||||
(Math.abs((coords.x * 92837111) ^ (coords.y * 689287499)) % 3500000);
|
||||
const city = {
|
||||
id: this._nextCityId,
|
||||
civ: civIndex,
|
||||
coords,
|
||||
name: description.name,
|
||||
name,
|
||||
isCapital,
|
||||
population: description.population,
|
||||
improvements: description.improvements.slice(),
|
||||
population,
|
||||
improvements: [],
|
||||
buildings: {},
|
||||
statuses: [],
|
||||
};
|
||||
@@ -52,10 +54,6 @@ export const worldMethods = {
|
||||
this._nextCityId += 1;
|
||||
},
|
||||
|
||||
_cityDescriptionFor(civ, _isCapital, center) {
|
||||
return this._generatedCityDescription(civ, center);
|
||||
},
|
||||
|
||||
_rebuildCityIndex() {
|
||||
this._cityByCoords = new Map();
|
||||
this._cityAdjacent = new Map();
|
||||
@@ -70,14 +68,6 @@ export const worldMethods = {
|
||||
}
|
||||
},
|
||||
|
||||
_generatedCityDescription(civ, center) {
|
||||
const index = this.civilisations.indexOf(civ);
|
||||
const name = this._nextCityName(civ, index);
|
||||
const population = 500000 +
|
||||
(Math.abs((center.x * 92837111) ^ (center.y * 689287499)) % 3500000);
|
||||
return { name, population, improvements: [] };
|
||||
},
|
||||
|
||||
// Picks the civilisation's next city name, skipping any a city elsewhere has
|
||||
// already taken, and falls back to a numbered name so the whole world never
|
||||
// has two cities called the same thing.
|
||||
|
||||
+8
-6
@@ -30,7 +30,9 @@ export function governmentChangeCost(proto, gdp) {
|
||||
return proto.baseCost + proto.costGdpFraction * Math.max(gdp, 0);
|
||||
}
|
||||
|
||||
export function governmentEffects(proto) {
|
||||
// Combined effect totals for a catalogue entry whose effects do not scale
|
||||
// (governments and technologies are always applied at level 1).
|
||||
function effectsTotal(proto) {
|
||||
const totals = {};
|
||||
for (const e of proto.effects) {
|
||||
totals[e.stat] = (totals[e.stat] || 0) + effectValue(e, 1);
|
||||
@@ -38,12 +40,12 @@ export function governmentEffects(proto) {
|
||||
return totals;
|
||||
}
|
||||
|
||||
export function governmentEffects(proto) {
|
||||
return effectsTotal(proto);
|
||||
}
|
||||
|
||||
export function technologyEffects(proto) {
|
||||
const totals = {};
|
||||
for (const e of proto.effects) {
|
||||
totals[e.stat] = (totals[e.stat] || 0) + effectValue(e, 1);
|
||||
}
|
||||
return totals;
|
||||
return effectsTotal(proto);
|
||||
}
|
||||
|
||||
// True when every prerequisite technology is in the researched set. The set
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// movement multiplier of 0 (an unauthored layer, or a non-land tile) falls back
|
||||
// to a normal cost so a unit is never frozen.
|
||||
|
||||
import { key, parseKey } from "./hex.js";
|
||||
|
||||
export const DEFAULT_MOVEMENT_COST_MULTIPLIER = 1.0;
|
||||
|
||||
export class TerrainStats {
|
||||
@@ -11,7 +13,7 @@ export class TerrainStats {
|
||||
}
|
||||
|
||||
_value(coords, field) {
|
||||
const tile = this._tiles[coords.x + "," + coords.y];
|
||||
const tile = this._tiles[key(coords.x, coords.y)];
|
||||
return tile ? (tile[field] || 0) : 0;
|
||||
}
|
||||
|
||||
@@ -35,10 +37,7 @@ export class TerrainStats {
|
||||
if (this._minMovementCost !== undefined) return this._minMovementCost;
|
||||
let cheapest = Infinity;
|
||||
for (const k in this._tiles) {
|
||||
const value = this.movementCostMultiplier({
|
||||
x: Number(k.slice(0, k.indexOf(","))),
|
||||
y: Number(k.slice(k.indexOf(",") + 1)),
|
||||
});
|
||||
const value = this.movementCostMultiplier(parseKey(k));
|
||||
if (value < cheapest) cheapest = value;
|
||||
}
|
||||
this._minMovementCost = cheapest < Infinity ? cheapest : DEFAULT_MOVEMENT_COST_MULTIPLIER;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// throughout so tests are fast and their results stable.
|
||||
|
||||
import { GameState } from "../../shared/game_state.js";
|
||||
import { parseKey } from "../../shared/hex.js";
|
||||
import { MAP_CONFIG, CIVILISATIONS } from "../../shared/data.js";
|
||||
import { startServer } from "../../server/server.js";
|
||||
|
||||
@@ -18,7 +19,6 @@ export async function withServer(fn) {
|
||||
}
|
||||
|
||||
export const SEED = 12345;
|
||||
export const LAND = "Land";
|
||||
export const SEA = "Sea";
|
||||
|
||||
export function smallConfig(overrides = {}) {
|
||||
@@ -72,10 +72,7 @@ export function adjacentLand(state, unit) {
|
||||
|
||||
export function seaTile(state) {
|
||||
for (const k in state.tiles) {
|
||||
if (state.tiles[k].terrainClass === SEA) {
|
||||
const i = k.indexOf(",");
|
||||
return { x: Number(k.slice(0, i)), y: Number(k.slice(i + 1)) };
|
||||
}
|
||||
if (state.tiles[k].terrainClass === SEA) return parseKey(k);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TestCase } from "./framework/test_case.js";
|
||||
import { GameServer } from "../server/game_server.js";
|
||||
import { CIVILISATIONS } from "../shared/data.js";
|
||||
import { smallConfig, adjacentLand, SEED } from "./framework/helpers.js";
|
||||
import { adjacentLand, SEED } from "./framework/helpers.js";
|
||||
|
||||
function fakeNetwork() {
|
||||
return {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
cityOf,
|
||||
unitsOf,
|
||||
snapshotUnit,
|
||||
SEED,
|
||||
} from "./framework/helpers.js";
|
||||
|
||||
export class GameStateTest extends TestCase {
|
||||
|
||||
Reference in New Issue
Block a user