Files
Battle-for-Tismo/shared/rules.js
T
adrien e7ee5dbf0e Added the resource economy, its works and their icons
- 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.
2026-09-22 05:27:11 +02:00

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;
}