Files
Battle-for-Tismo/shared/data.js
T

427 lines
14 KiB
JavaScript

// Data-driven content for civilisations, units, buildings, governments and
// technologies. The server and the browser both read these plain objects, so
// the catalogue text, costs and effects can never disagree between the two.
export const EFFECT_GDP = "gdp";
export const EFFECT_RESEARCH = "research";
export const EFFECT_CULTURE = "culture";
export const EFFECT_BUDGET_INCOME = "budget_income";
function effect(stat, baseValue, exponent = 1.0) {
return { stat, baseValue, exponent };
}
export function effectValue(e, level) {
if (level <= 0) return 0;
return e.baseValue * Math.pow(level, e.exponent);
}
export const CIVILISATIONS = [
{
id: "france",
name: "France",
primaryColour: "#0d0d5c",
accentColour: "#8c8c82",
flag: "flag_france.png",
cityNames: ["Lyon", "Marseille", "Toulouse", "Nice", "Nantes", "Montpellier", "Strasbourg", "Bordeaux", "Lille"],
},
{
id: "britain",
name: "Britain",
primaryColour: "#a81818",
accentColour: "#0722ff",
flag: "flag_britain.png",
cityNames: ["Birmingham", "Manchester", "Glasgow", "Liverpool", "Leeds", "Sheffield", "Edinburgh", "Bristol", "Cardiff"],
},
{
id: "slovenia",
name: "Slovenia",
primaryColour: "#005da4",
accentColour: "#ed1c24",
flag: "flag_slovenia.png",
cityNames: ["Maribor", "Celje", "Kranj", "Koper", "Velenje", "Novo Mesto", "Ptuj", "Kamnik", "Jesenice"],
},
{
id: "poland",
name: "Poland",
primaryColour: "#a6000b",
accentColour: "#ffffff",
flag: "flag_poland.png",
cityNames: ["Warsaw", "Kraków", "Łódź", "Wrocław", "Poznań", "Gdańsk", "Szczecin", "Bydgoszcz", "Lublin", "Katowice"],
},
{
id: "switzerland",
name: "Switzerland",
primaryColour: "#da291c",
accentColour: "#ffffff",
flag: "flag_switzerland.png",
cityNames: ["Bern", "Zürich", "Geneva", "Basel", "Lausanne", "Lucerne", "Winterthur", "St. Gallen", "Lugano", "Biel"],
},
{
id: "ticino",
name: "Ticino",
primaryColour: "#4646e0",
accentColour: "#a6000b",
flag: "flag_ticino.png",
cityNames: ["Bellinzona", "Lugano", "Locarno", "Mendrisio", "Chiasso", "Airolo", "Biasca", "Ascona", "Faido", "Giubiasco"],
},
{
id: "galicia",
name: "Galicia",
primaryColour: "#006dad",
accentColour: "#ffffff",
flag: "flag_galicia.png",
cityNames: ["Santiago de Compostela", "Vigo", "A Coruña", "Ourense", "Lugo", "Pontevedra", "Ferrol", "Vilagarcía de Arousa", "Ribeira", "Redondela"],
},
{
id: "germany",
name: "Germany",
primaryColour: "#141414",
accentColour: "#ffce00",
flag: "flag_germany.png",
cityNames: ["Berlin", "Hamburg", "Munich", "Cologne", "Frankfurt", "Stuttgart", "Düsseldorf", "Leipzig", "Dortmund", "Essen"],
},
{
id: "iran",
name: "Iran",
primaryColour: "#18a645",
accentColour: "#f01c21",
flag: "flag_iran.png",
cityNames: ["Tehran", "Mashhad", "Isfahan", "Karaj", "Shiraz", "Tabriz", "Qom", "Ahvaz", "Kermanshah", "Urmia"],
},
];
export const PROTO_UNITS = [
{
id: "modern_infantry",
name: "Modern infantry",
maxHp: 500,
cost: 100_000_000,
upkeep: 5000,
moveable: true,
speed: 0.5,
// Only military units fight, capture cities and may enter an enemy
// military unit's tile. `attack` is the damage dealt per combat round and
// `defense` is the damage absorbed per round; a defender's defense is
// doubled (the 100% defensive bonus). `range` is how many tiles away the
// unit can strike (1 is melee).
military: true,
attack: 100,
defense: 40,
range: 1,
traversableTerrains: ["Land"],
icon: "icon_modern_infantry.svg",
},
{
id: "worker",
name: "Worker",
maxHp: 100,
cost: 0,
upkeep: 0,
moveable: true,
speed: 0.5,
military: false,
attack: 0,
defense: 0,
range: 1,
traversableTerrains: ["Land"],
icon: "icon_settler.svg",
},
{
id: "artillery",
name: "Artillery",
maxHp: 400,
cost: 300_000_000,
upkeep: 12_000,
moveable: true,
speed: 0.25,
military: true,
attack: 250,
// Barely armoured: nearly every hit gets through.
defense: 5,
// Outranges infantry, so melee units cannot strike back from two tiles.
range: 2,
traversableTerrains: ["Land"],
icon: "icon_artillery.svg",
},
];
// Only units in this list can be trained from the city screen, matching
// GameState.TRAINABLE_UNITS.
export const TRAINABLE_UNIT_IDS = ["modern_infantry", "artillery"];
// Status effects a unit or city can carry. They are data-driven so new ones
// can be added without touching the simulation or the renderer: the server
// derives the active ids once a day, applies their daily fields, and ships the
// ids in snapshots; the browser draws one icon per id. A status may define
// `hpLossPerDay` (a fraction of maximum hit points taken each day) and a
// `cityHpLossPerDay` override for when the unit stands in a city.
export const STATUS_ENCIRCLED = "encircled";
export const STATUS_BESIEGED = "besieged";
export const STATUSES = [
{
id: STATUS_ENCIRCLED,
label: "Encircled",
icon: "encirclement.svg",
description: "Cut off from every allied city while a hostile unit is adjacent. Loses 10% of its maximum hit points each day, or 2% inside a city.",
hpLossPerDay: 0.10,
cityHpLossPerDay: 0.02,
},
{
id: STATUS_BESIEGED,
label: "Besieged",
icon: "encirclement.svg",
description: "Cut off from every other allied city while a hostile unit is adjacent.",
},
];
export function statusById(id) {
return STATUSES.find((status) => status.id === id) || null;
}
export const BUILDINGS = [
{
id: "market",
name: "Market",
description: "A hub of local trade. Raises national GDP, with diminishing returns.",
maxLevel: 10,
baseCost: 0,
costGdpFraction: 0.01,
costAlpha: 1.6,
baseUpkeep: 0,
upkeepGdpFraction: 0.00003,
upkeepAlpha: 1.5,
refundFraction: 0.5,
effects: [effect(EFFECT_GDP, 0.04, 0.75)],
},
{
id: "bank",
name: "Bank",
description: "Channels private wealth into the state coffers. Raises budget income.",
maxLevel: 10,
baseCost: 0,
costGdpFraction: 0.02,
costAlpha: 1.7,
baseUpkeep: 0,
upkeepGdpFraction: 0.00004,
upkeepAlpha: 1.6,
refundFraction: 0.5,
effects: [effect(EFFECT_BUDGET_INCOME, 0.05, 0.7)],
},
{
id: "research_lab",
name: "Research lab",
description: "Applies science to industry and government. Produces research points.",
maxLevel: 10,
baseCost: 0,
costGdpFraction: 0.015,
costAlpha: 1.65,
baseUpkeep: 0,
upkeepGdpFraction: 0.00005,
upkeepAlpha: 1.55,
refundFraction: 0.5,
effects: [effect(EFFECT_RESEARCH, 1.0, 0.9)],
},
{
id: "university",
name: "University",
description: "Higher education produces research and a little culture.",
maxLevel: 10,
baseCost: 0,
costGdpFraction: 0.025,
costAlpha: 1.7,
baseUpkeep: 0,
upkeepGdpFraction: 0.00008,
upkeepAlpha: 1.6,
refundFraction: 0.5,
effects: [effect(EFFECT_RESEARCH, 2.0, 0.8), effect(EFFECT_CULTURE, 0.5, 0.7)],
},
{
id: "museum",
name: "Museum",
description: "Preserves national heritage, strengthening culture against foreign ideas.",
maxLevel: 10,
baseCost: 0,
costGdpFraction: 0.02,
costAlpha: 1.6,
baseUpkeep: 0,
upkeepGdpFraction: 0.00005,
upkeepAlpha: 1.55,
refundFraction: 0.5,
effects: [effect(EFFECT_CULTURE, 1.5, 0.85)],
},
{
id: "cinema_studio",
name: "Cinema studio",
description: "Mass media spreads the nation's culture at home and abroad.",
maxLevel: 10,
baseCost: 0,
costGdpFraction: 0.03,
costAlpha: 1.75,
baseUpkeep: 0,
upkeepGdpFraction: 0.0001,
upkeepAlpha: 1.65,
refundFraction: 0.5,
effects: [effect(EFFECT_CULTURE, 3.0, 0.7)],
},
];
export const GOVERNMENTS = [
{
id: "republic",
name: "Republic",
description: "Rule by elected representatives. A balanced government that spreads its support across the economy, the treasury and the sciences.",
baseCost: 0,
costGdpFraction: 0.02,
effects: [effect(EFFECT_GDP, 0.05, 1.0), effect(EFFECT_BUDGET_INCOME, 0.1, 1.0), effect(EFFECT_RESEARCH, 1.0, 1.0)],
},
{
id: "monarchy",
name: "Monarchy",
description: "Rule by a hereditary sovereign. The crown favours commerce and tradition, lifting the economy and the nation's cultural output.",
baseCost: 0,
costGdpFraction: 0.03,
effects: [effect(EFFECT_GDP, 0.1, 1.0), effect(EFFECT_CULTURE, 1.0, 1.0)],
},
{
id: "technocracy",
name: "Technocracy",
description: "Rule by technical experts. A single-minded drive for progress pours resources into research while economic growth takes second place.",
baseCost: 0,
costGdpFraction: 0.04,
effects: [effect(EFFECT_RESEARCH, 3.0, 1.0), effect(EFFECT_GDP, 0.03, 1.0)],
},
];
export const TECHNOLOGIES = [
{
id: "scientific_method",
name: "Scientific Method",
description: "A disciplined approach to experiment and evidence. Lays the groundwork for every later technology.",
cost: 20,
prerequisites: [],
effects: [effect(EFFECT_RESEARCH, 1.0, 1.0), effect(EFFECT_GDP, 0.02, 1.0)],
},
{
id: "industrial_automation",
name: "Industrial Automation",
description: "Robotics and control systems take over the factory floor, lifting national output.",
cost: 60,
prerequisites: ["scientific_method"],
effects: [effect(EFFECT_GDP, 0.08, 1.0)],
},
{
id: "renewable_energy",
name: "Renewable Energy",
description: "Clean generation cuts waste and strengthens the public finances.",
cost: 80,
prerequisites: ["scientific_method"],
effects: [effect(EFFECT_GDP, 0.03, 1.0), effect(EFFECT_BUDGET_INCOME, 0.05, 1.0)],
},
{
id: "quantum_computing",
name: "Quantum Computing",
description: "Machines that reason in superposition. A step change in the pace of discovery.",
cost: 120,
prerequisites: ["scientific_method"],
effects: [effect(EFFECT_RESEARCH, 4.0, 1.0)],
},
{
id: "space_program",
name: "Space Program",
description: "Reaching orbit inspires the nation and spins off technology across the economy.",
cost: 300,
prerequisites: ["industrial_automation", "quantum_computing"],
effects: [effect(EFFECT_CULTURE, 3.0, 1.0), effect(EFFECT_GDP, 0.05, 1.0)],
},
];
export const ECONOMY = {
baseGdpPerCapita: 10_000,
basePopulation: 100_000,
cityTileGdpMultiplier: 2.0,
capitalTileGdpMultiplier: 2.5,
citySurroundingGdpBonus: 0.3,
gdpPerProductionPoint: 100_000,
budgetGdpRate: 0.001,
startingBudget: 0,
basePopulationGrowthRate: 0.005,
productionCapacity(gdp) {
if (this.gdpPerProductionPoint <= 0) return 0;
return gdp / this.gdpPerProductionPoint;
},
trainingHours(gdp, cost) {
const capacity = this.productionCapacity(gdp);
if (capacity <= 0) return 0;
return cost / capacity;
},
budgetIncome(gdp, hours = 1) {
return gdp * this.budgetGdpRate * hours;
},
populationGrowthFactor(hoursPerYear, hours = 1) {
if (this.basePopulationGrowthRate <= 0 || hoursPerYear <= 0 || hours <= 0) return 0;
return Math.pow(1 + this.basePopulationGrowthRate, hours / hoursPerYear) - 1;
},
};
export const MAP_CONFIG = {
mapSize: { x: 64, y: 40 },
topology: "cylindrical",
terrainSeed: 0,
seaLevel: -0.2,
falloffEnabled: true,
falloffStart: 0.55,
falloffStrength: 0.6,
iceLatitude: 0.9,
noise: {
frequency: 0.06,
octaves: 4,
lacunarity: 2.0,
gain: 0.5,
},
capitalMargin: 2,
minCapitalDistance: 8,
minCapitalLandmass: 40,
citiesPerCiv: 10,
citySpacing: 8,
};
// The terrain TileSet, flattened. `col` is the atlas column in terrain.png
// (one row, 7 tiles of 63x56); the class decides land/sea/ice and the
// minElevation bands place land tiles. `defenseBonus` is the fraction added to
// a unit's defense while it stands on the tile.
export const TERRAIN_TILES = [
{ col: 0, terrainType: "Forest", terrainClass: "Land", populationMultiplier: 0.3, gdpMultiplier: 0.5, movementCostMultiplier: 1.5, defenseBonus: 0.33, minElevation: 0.06 },
{ col: 1, terrainType: "Hills", terrainClass: "Land", populationMultiplier: 0.7, gdpMultiplier: 0.8, movementCostMultiplier: 1.8, defenseBonus: 0.33, minElevation: 0.2 },
{ col: 2, terrainType: "Ice", terrainClass: "Ice", populationMultiplier: 0, gdpMultiplier: 0, movementCostMultiplier: 4.0, defenseBonus: 0, minElevation: 0 },
{ col: 3, terrainType: "Land", terrainClass: "Land", populationMultiplier: 1.0, gdpMultiplier: 1.0, movementCostMultiplier: 1.0, defenseBonus: 0, minElevation: -0.08 },
{ col: 4, terrainType: "Land", terrainClass: "Land", populationMultiplier: 0.05, gdpMultiplier: 0.2, movementCostMultiplier: 3.0, defenseBonus: 0, minElevation: -0.2 },
{ col: 5, terrainType: "Sea", terrainClass: "Sea", populationMultiplier: 0, gdpMultiplier: 0, movementCostMultiplier: 0, defenseBonus: 0, minElevation: 0 },
{ col: 6, terrainType: "Mountain", terrainClass: "Land", populationMultiplier: 0.1, gdpMultiplier: 0.8, movementCostMultiplier: 5.0, defenseBonus: 1.0, minElevation: 0.34 },
];
// A city standing on a tile adds this fraction on top of the terrain's own
// defensive bonus.
export const CITY_DEFENSE_BONUS = 0.5;
export function protoUnitById(id) {
return PROTO_UNITS.find((u) => u.id === id) || null;
}
export function buildingById(id) {
return BUILDINGS.find((b) => b.id === id) || null;
}
export function governmentById(id) {
return GOVERNMENTS.find((g) => g.id === id) || null;
}
export function technologyById(id) {
return TECHNOLOGIES.find((t) => t.id === id) || null;
}