Files
Battle-for-Tismo/shared/rules.js
T
adrien 3acd2487df Added roads, regional taxation and a reworked budget view
Roads are pre-generated between cities with a greedy geometric spanner and flatten unit movement cost. City improvements now only affect the tiles their region controls, with per-region tax collection and a Cities tab. The budget is a grouped, scrollable table with 25%/year tax and a 10%-of-GDP opening treasury. Also merged the tile info into the city panel, kept conflict and budget expansion state across snapshots, added join notifications, filled border corners, drew battle sides as stacks and made tab overflow scroll.
2026-09-18 07:11:59 +02:00

150 lines
5.6 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 } from "./data.js";
// 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,
regionalMultiplier = 1,
regionalLabel = null,
nationalMultiplier = 1,
} = {}) {
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 });
}
// 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 });
}
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;
}