- Five resources (energy, steel, food, luxury, high-tech) with per-city stores, a world market, delivery and material upkeep. - Resource-producing tile works built outside cities; every nation opens with the works its economy needs. - Real game-icons.net art replacing the dummy icons for the new resources and works, with the credits updated.
196 lines
7.4 KiB
JavaScript
196 lines
7.4 KiB
JavaScript
// Catalogue maths shared by the server (which charges costs) and the browser
|
|
// (which displays and disables buttons) so the numbers always agree.
|
|
|
|
import { effectValue, CITY_DEFENSE_BONUS, ECONOMY, POPULARITY, TRENCH } from "./data.js";
|
|
|
|
// The popularity scale shared by the server (which simulates it) and the
|
|
// browser (which shows it). Opinions live on the 0..1 scale; popularity is the
|
|
// same figure in integer points centred on neutral 0, and what the player sees
|
|
// is a logistic of those points.
|
|
export function popularityPoints(opinion) {
|
|
return Math.round((opinion - 0.5) / POPULARITY.opinionPerPoint);
|
|
}
|
|
|
|
export function popularityDisplay(points) {
|
|
return 1 / (1 + Math.exp(-POPULARITY.logisticScale * points));
|
|
}
|
|
|
|
// The hourly pace at which a population at `points` returns to neutral: one
|
|
// point an hour, so the estimate is simply the distance left.
|
|
export function hoursToNeutral(points) {
|
|
return Math.abs(points);
|
|
}
|
|
|
|
// Fixed cost of raising a building to `level` (0-indexed: the cost of the next
|
|
// level). It grows only with the level, never with the nation's GDP.
|
|
export function buildingBuildCost(proto, level) {
|
|
return proto.baseCost * Math.pow(proto.costAlpha, Math.max(level, 0));
|
|
}
|
|
|
|
export function buildingRefund(proto, level) {
|
|
return buildingBuildCost(proto, level - 1) * proto.refundFraction;
|
|
}
|
|
|
|
// Recurring upkeep of a building kept at `level`, as a fraction of the cost of
|
|
// that level. Like the build cost it follows the level alone, never the GDP.
|
|
export const BUILDING_UPKEEP_FRACTION = 0.01;
|
|
|
|
export function buildingUpkeep(proto, level) {
|
|
if (level <= 0) return 0;
|
|
return buildingBuildCost(proto, level - 1) * BUILDING_UPKEEP_FRACTION;
|
|
}
|
|
|
|
export function buildingEffectValue(proto, stat, level) {
|
|
let total = 0;
|
|
for (const e of proto.effects) {
|
|
if (e.stat === stat) total += effectValue(e, level);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
export function governmentChangeCost(proto, gdp) {
|
|
return proto.baseCost + proto.costGdpFraction * Math.max(gdp, 0);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
return totals;
|
|
}
|
|
|
|
export function governmentEffects(proto) {
|
|
return effectsTotal(proto);
|
|
}
|
|
|
|
export function technologyEffects(proto) {
|
|
return effectsTotal(proto);
|
|
}
|
|
|
|
// True when every prerequisite technology is in the researched set. The set
|
|
// holds catalogue indices, so each prerequisite id is first resolved against
|
|
// the catalogue.
|
|
export function prerequisitesMet(technology, researchedSet, catalogue) {
|
|
for (const id of technology.prerequisites) {
|
|
const index = catalogue ? catalogue.findIndex((t) => t.id === id) : -1;
|
|
if (index < 0 || !researchedSet.has(index)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Every defense modifier of the tile a unit stands on, in display order. The
|
|
// tile carries its own terrain bonus and a city adds another; each entry is a
|
|
// fraction of the unit's base defense. Shared by the server (which fights with
|
|
// them) and the browser (which shows them) so the numbers always agree.
|
|
export function defenseFactors(tile, hasCity) {
|
|
const factors = [];
|
|
if (tile && tile.defenseBonus) {
|
|
factors.push({ label: tile.terrainType, bonus: tile.defenseBonus });
|
|
}
|
|
if (hasCity) factors.push({ label: "City", bonus: CITY_DEFENSE_BONUS });
|
|
return factors;
|
|
}
|
|
|
|
// Multiplier applied to a unit's base defense for the tile it occupies.
|
|
export function defenseMultiplier(tile, hasCity) {
|
|
let total = 0;
|
|
for (const factor of defenseFactors(tile, hasCity)) total += factor.bonus;
|
|
return 1 + total;
|
|
}
|
|
|
|
// A unit's defense on a tile, before any role bonus (the attacker/defender
|
|
// distinction is a combat rule, not part of the stat shown to the player).
|
|
export function unitDefense(baseDefense, tile, hasCity) {
|
|
return baseDefense * defenseMultiplier(tile, hasCity);
|
|
}
|
|
|
|
// Every production modifier of a tile, in display order. `tile` supplies the
|
|
// terrain multiplier and its name; `cityKind` is "capital", "city" or null and
|
|
// `nextToCity` is true when the tile borders one of its owner's cities. The
|
|
// pillage penalty and battle deficit are the lasting marks of war. Shared by the
|
|
// server (which derives each tile's production) and the browser (which explains
|
|
// it), so the numbers always agree.
|
|
export function productionFactors(tile, {
|
|
cityKind = null,
|
|
nextToCity = false,
|
|
pillagePenalty = 1,
|
|
battleDeficit = 0,
|
|
trench = false,
|
|
regionalMultiplier = 1,
|
|
regionalLabel = null,
|
|
nationalMultiplier = 1,
|
|
portProduction = 0,
|
|
naturalGrowth = 0,
|
|
} = {}) {
|
|
const factors = [];
|
|
const terrainMultiplier = tile ? tile.productionMultiplier || 0 : 0;
|
|
factors.push({ label: tile ? tile.terrainType : "Unknown", multiplier: terrainMultiplier });
|
|
if (cityKind === "capital") {
|
|
factors.push({ label: "Capital", multiplier: ECONOMY.capitalTileProductionMultiplier });
|
|
} else if (cityKind === "city") {
|
|
factors.push({ label: "City", multiplier: ECONOMY.cityTileProductionMultiplier });
|
|
} else if (nextToCity) {
|
|
factors.push({ label: "Near city", multiplier: 1 + ECONOMY.citySurroundingProductionBonus });
|
|
}
|
|
if (pillagePenalty !== 1) {
|
|
factors.push({ label: "Pillaging", multiplier: pillagePenalty });
|
|
}
|
|
if (trench) {
|
|
factors.push({ label: "Trenches", multiplier: TRENCH.productionMultiplier });
|
|
}
|
|
// City improvements only shape the region their city controls, so a tile's
|
|
// production carries its region's multiplier plus the nation-wide one
|
|
// from government and research. Both are factors on the production figure,
|
|
// never an adjustment to the final GDP. They are applied before the absolute
|
|
// war damage below, so a scar stays proportional to the tile's productivity.
|
|
if (regionalMultiplier !== 1) {
|
|
factors.push({
|
|
label: regionalLabel ? `${regionalLabel} improvements` : "Regional improvements",
|
|
multiplier: regionalMultiplier,
|
|
});
|
|
}
|
|
if (nationalMultiplier !== 1) {
|
|
factors.push({ label: "National modifiers", multiplier: nationalMultiplier });
|
|
}
|
|
// A warm-water port adds a flat production-per-head figure to its whole
|
|
// region, scaled by the sea tiles beside the city, before war damage is taken
|
|
// off.
|
|
if (portProduction > 0) {
|
|
factors.push({ label: "Port", amount: portProduction });
|
|
}
|
|
// Natural growth is a flat production-per-head gain added on top of the
|
|
// tile's productivity, growing by a constant amount each day.
|
|
if (naturalGrowth > 0) {
|
|
factors.push({ label: "Natural growth", amount: naturalGrowth });
|
|
}
|
|
if (battleDeficit > 0) {
|
|
factors.push({ label: "Battle damage", amount: -battleDeficit });
|
|
}
|
|
return factors;
|
|
}
|
|
|
|
// Applies the `productionFactors` list to `base`, in order, never falling
|
|
// below zero.
|
|
export function productionFromFactors(base, factors) {
|
|
let total = base;
|
|
for (const factor of factors) {
|
|
if (factor.amount !== undefined) total += factor.amount;
|
|
else total *= factor.multiplier;
|
|
}
|
|
return total > 0 ? total : 0;
|
|
}
|
|
|
|
// The multiplier product of a `productionFactors` list, ignoring the absolute
|
|
// per-head terms (a port's flat production, natural growth, battle damage).
|
|
// This is what scales a tile's physical output.
|
|
export function productionMultiplier(factors) {
|
|
let total = 1;
|
|
for (const factor of factors) {
|
|
if (factor.multiplier !== undefined) total *= factor.multiplier;
|
|
}
|
|
return total > 0 ? total : 0;
|
|
}
|