// 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)); } // 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. A technology may also carry a `requiresBuilding` gate -- the // uranium enrichment centre that unlocks nuclear weapons -- checked through the // optional `hasBuilding` predicate, so the rule stays free of any world state. export function prerequisitesMet(technology, researchedSet, catalogue, hasBuilding = null) { for (const id of technology.prerequisites) { const index = catalogue ? catalogue.findIndex((t) => t.id === id) : -1; if (index < 0 || !researchedSet.has(index)) return false; } if (technology.requiresBuilding && hasBuilding && !hasBuilding(technology.requiresBuilding)) { 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, sabotaged = false, } = {}) { 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 }); } // A sabotaged tile produces nothing for a week. The zero multiplier is last, // so it zeroes the absolute terms above (port output, natural growth) too. if (sabotaged) { factors.push({ label: "Sabotaged", multiplier: 0 }); } 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; }