// 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"; // Diplomatic relations between two civilisations. Only peace and war exist for // now, but the catalogue is data-driven so further statuses (alliances, // truces, ...) can be slotted in without touching the simulation. export const RELATION_PEACE = "peace"; export const RELATION_WAR = "war"; export const RELATIONS = [RELATION_PEACE, RELATION_WAR]; // Kinds of news item shown in the feed. Newsworthy events are recorded in // GameState with their in-game hour so every client can render them. export const NEWS_WAR = "war"; export const NEWS_CAPTURE = "capture"; 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", demonym: "French", primaryColour: "#0d0d5c", accentColour: "#8c8c82", flag: "flag_france.png", cityNames: ["Lyon", "Marseille", "Toulouse", "Nice", "Nantes", "Montpellier", "Strasbourg", "Bordeaux", "Lille"], }, { id: "britain", name: "Britain", demonym: "British", primaryColour: "#a81818", accentColour: "#0722ff", flag: "flag_britain.png", cityNames: ["Birmingham", "Manchester", "Glasgow", "Liverpool", "Leeds", "Sheffield", "Edinburgh", "Bristol", "Cardiff"], }, { id: "slovenia", name: "Slovenia", demonym: "Slovenian", primaryColour: "#005da4", accentColour: "#ed1c24", flag: "flag_slovenia.png", cityNames: ["Ljubljana", "Maribor", "Celje", "Kranj", "Koper", "Velenje", "Novo Mesto", "Ptuj", "Kamnik", "Jesenice"], }, { id: "poland", name: "Poland", demonym: "Polish", 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", demonym: "Swiss", 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", demonym: "Ticinese", primaryColour: "#4646e0", accentColour: "#a6000b", flag: "flag_ticino.png", cityNames: ["Bellinzona", "Lugano", "Locarno", "Mendrisio", "Chiasso", "Airolo", "Biasca", "Ascona", "Faido", "Giubiasco"], }, { id: "galicia", name: "Galicia", demonym: "Galician", 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", demonym: "German", primaryColour: "#141414", accentColour: "#ffce00", flag: "flag_germany.png", cityNames: ["Berlin", "Hamburg", "Munich", "Cologne", "Frankfurt", "Stuttgart", "Düsseldorf", "Leipzig", "Dortmund", "Essen"], }, { id: "iran", name: "Iran", demonym: "Iranian", 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, applies their 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), a // `cityHpLossPerDay` override for when the unit stands in a city, and an // `upkeepMultiplier` applied to the unit's upkeep while it is active. export const STATUS_ENCIRCLED = "encircled"; export const STATUS_BESIEGED = "besieged"; export const STATUS_BATTLE = "battle"; 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.", }, { id: STATUS_BATTLE, label: "In battle", icon: "icon_battle.svg", description: "Fighting an enemy unit in range. Upkeep is tripled while the battle rages.", upkeepMultiplier: 3, }, ]; 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; }