// 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 GDP-per-capita 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 GDP) and the browser (which explains it), so // the numbers always agree. export function gdpPerCapitaFactors(tile, { cityKind = null, nextToCity = false, pillagePenalty = 1, battleDeficit = 0, trench = false, regionalMultiplier = 1, regionalLabel = null, nationalMultiplier = 1, portGdp = 0, naturalGrowth = 0, } = {}) { const factors = []; const terrainMultiplier = tile ? tile.gdpMultiplier || 0 : 0; factors.push({ label: tile ? tile.terrainType : "Unknown", multiplier: terrainMultiplier }); if (cityKind === "capital") { factors.push({ label: "Capital", multiplier: ECONOMY.capitalTileGdpMultiplier }); } else if (cityKind === "city") { factors.push({ label: "City", multiplier: ECONOMY.cityTileGdpMultiplier }); } else if (nextToCity) { factors.push({ label: "Near city", multiplier: 1 + ECONOMY.citySurroundingGdpBonus }); } if (pillagePenalty !== 1) { factors.push({ label: "Pillaging", multiplier: pillagePenalty }); } if (trench) { factors.push({ label: "Trenches", multiplier: TRENCH.gdpMultiplier }); } // City improvements only shape the region their city controls, so a tile's // GDP per capita carries its region's multiplier plus the nation-wide one // from government and research. Both are factors on the per-capita 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 per-capita figure to its whole region, scaled // by the sea tiles beside the city, before war damage is taken off. if (portGdp > 0) { factors.push({ label: "Port", amount: portGdp }); } // Natural growth is a flat per-capita 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 `gdpPerCapitaFactors` list to `base`, in order, never falling // below zero. export function gdpPerCapitaFromFactors(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; }