// Politics tuning: public opinion, approval penalties, propaganda campaigns, // minority policies, migration, war crimes, uprisings and cultural impact. // // Every population (an ethnicity, indexed like the civilisations) holds an // opinion of every other ethnicity and of every government. Opinions run from 0 // (hatred) to 1 (devotion) and start at the neutral 50%. All of it lives in // shared/game_state/politics.js so the server simulates it and the browser only // reads it from the snapshot. // The opinion scale. `driftPerDay` pulls every opinion back toward the neutral // baseline; at this rate the full 0..1 span recovers over roughly 45 years, so // a campaign's goodwill lingers for years while a war crime is not forgotten // for a generation. export const OPINION = { baseline: 0.5, driftPerDay: 1.0 / (365.25 * 45), min: 0.0, max: 1.0, }; // A region whose people approve of their government by less than // `rebellionThreshold` suffers the penalties below. Construction slows // exponentially as approval falls and the tax take collapses toward // `taxFloorFraction` of its normal value. export const APPROVAL = { rebellionThreshold: 0.1, constructionExponent: 2.5, taxFloorFraction: 0.1, }; // Popularity is counted in integer points, centred on 0 (neutral). The whole // 0..1 opinion scale maps to -100..+100 points, one point being half a percent. // What the player sees is a logistic of those points: 0 points reads 50%, +100 // reads 75% and -100 reads 25%. Opinions drift one point an hour toward // neutrality, and war, captures and casualties move them in points. export const POPULARITY = { opinionPerPoint: 0.005, // Steepness chosen so display(0) = 0.5 and display(100) = 0.75. logisticScale: Math.log(3) / 100, // Points a population's opinion of its government moves toward neutral each // hour, in either direction. driftPerHour: 1, // Declaring war costs the aggressor this many points with every population. declareWar: 8, // Being declared on grants the victim this many points with its own people, // but only when the aggressor was already loathed there (below threshold). declaredOnBoost: 15, declaredOnThreshold: 0.15, // Points won or lost per military unit destroyed, and per city taken. unitLoss: 1, unitKill: 1, cityLoss: 10, cityCapture: 10, // Pillaging a tile costs the aggressor standing with the whole world, doubled // among the populations directly affected. pillage: 6, }; // A campaign costs nothing to start: it charges `defaultHourlyCost` for as long // as it runs, like any other upkeep and shown in the budget. The player may // raise or lower the spending up to `maxHourlyCost`; the opinion shift scales // with it. A campaign stays in force until cancelled. `dailyDelta` is the // opinion shift per day at the default spend and balanced cultural impact; // `culturalWeight` is how strongly the two nations' cultural impacts bend the // result. // // Convincing the last few points is the hardest: the daily shift is scaled by // how much room is left before the opinion reaches its maximum (warming) or // minimum (cooling), raised to `difficultyExponent`. The rate is unchanged at // or below the neutral midpoint, so only the final approach to a devoted or // hateful population suffers the diminishing returns that make the last // percentage points disproportionately expensive. // // Opinions otherwise drift one point an hour toward neutral (0.12 a day), so a // campaign must out-spend that drift to change minds; `dailyDelta` is set well // above it. export const PROPAGANDA = { defaultHourlyCost: 8_000, maxHourlyCost: 40_000, dailyDelta: 0.18, culturalWeight: 0.6, difficultyExponent: 2, }; export const CAMPAIGN_ETHNICITY = "ethnicity"; export const CAMPAIGN_GOVERNMENT = "government"; // Minority policies a government may proclaim. Any number may be active at // once, but the same policy may not be enacted twice for the same ethnicity. // `targetEffect` and `politicalCost` are the estimates the wizard shows; the // model applies them through POLICY_OPINION below. Effects are stated in // popularity points (the integer unit the model works in); only the final // approval figure is ever shown as a percentage. export const POLICY_CHAMPION = "champion"; export const POLICY_EXPEL = "expel"; export const POLICY_PROTECT = "protect"; export const POLICY_IMMIGRATION = "immigration"; export const POLICIES = [ { id: POLICY_CHAMPION, name: "Champion a minority", description: "Proclaim the state the protector of an ethnicity. Its people everywhere warm to you, but your own other populations resent the favouritism.", targetEffect: "+30 popularity among the chosen people", politicalCost: "−16 popularity among every other population in your country", }, { id: POLICY_PROTECT, name: "Protect a minority", description: "Shield an ethnicity inside your borders. It raises their view of you at home and abroad, at some cost with your other populations.", targetEffect: "+24 popularity among the chosen people", politicalCost: "−8 popularity among every other population in your country", }, { id: POLICY_EXPEL, name: "Expel a minority", description: "Force an ethnicity out of the country. Its people, in and outside your borders, will hate you for a long time, and they will press into your neighbours.", targetEffect: "The people begin to leave for neighbouring countries", politicalCost: "−80 popularity among the expelled people, remembered for a generation", }, { id: POLICY_IMMIGRATION, name: "Encourage immigration", description: "Invite an ethnicity in. They will come from lands where their government is unpopular, but every other population in your country dislikes the influx.", targetEffect: "The chosen people migrate into your country", politicalCost: "−10 popularity among every other population in your country", }, ]; export function policyById(id) { return POLICIES.find((policy) => policy.id === id) || null; } // Immediate opinion shifts of proclaiming each policy, addressed to the target // ethnicity and to the rest of the nation's people. export const POLICY_OPINION = { championTarget: 0.15, championMajority: -0.08, protectTarget: 0.12, protectMajority: -0.04, expelTarget: -0.4, immigrationOthers: -0.05, }; // Daily share of a minority that is expelled to neighbouring tiles, and the // rate at which an encouraged ethnicity is drawn into the country. Immigration // is scaled by how much the newcomer dislikes their current government: // a 5% approval government sheds people fast, a 90% one holds them. export const MIGRATION = { expelSharePerDay: 0.004, immigrationSharePerDay: 0.002, // People fly from any other airport city into a city with an airport. The // share is per destination airport level, per source city, per day. airportSharePerDay: 0.0005, // The approval at or above which nobody is inclined to leave. comfortableApproval: 0.9, // People move only to a region at least this much richer per head, and the // bar rises with the travel time: a destination `time` hours away must be // `incomeRatioBase * (1 + time * incomeTimePenalty)` times as rich. So the // closer and richer a place is, the more attractive. incomeRatioBase: 2, incomeTimePenalty: 0.02, // People travel the transport network; a tile of open terrain costs this many // hours at walking pace, a road or railway less, open water a flat sea rate. peopleSpeed: 1, seaHoursPerTile: 1.5, // Land routes are only traced this far; beyond it a destination is reachable // only by air. maxTravelHours: 120, // Of the people who qualify, this share of the poorer region moves per day, // scaled by how far the ratio clears the bar and capped. incomeSharePerDay: 0.0015, incomeGapCap: 1, // No single day moves more than this share of a region's people by land. incomeMaxSharePerDay: 0.005, // An airport flies people over the distance at a flat time cost, but moves at // most this many people per day for each airport level on the route. airHours: 6, airPeoplePerDayPerLevel: 1000, // At generation, regions of a nation trade people until their production per // head is level. `equalizeTolerance` is the residual spread accepted and // `equalizePasses` bounds the refinement; no region is emptied below // `minRegionPopulation`. equalizeTolerance: 0.005, equalizePasses: 30, minRegionPopulation: 20000, }; // A pillaged tile is a war crime. The people of the tile loathe the aggressor // immediately; the stain spreads to neighbours, the region, the victim nation // and the watching world, and is remembered for a generation. export const WAR_CRIME = { tileOpinion: 0.35, tileEthnicOpinion: 0.2, neighbourFactor: 0.5, regionalFactor: 0.3, nationalFactor: 0.2, worldFactor: 0.08, memoryYears: 25, }; // A mistreated minority rises up when it hates its government and adores the // one that shares its ethnicity, taking up arms for the latter. export const REBELLION = { approvalMax: 0.1, championApprovalMin: 0.7, populationShareMin: 0.12, cooldownDays: 60, maxUnitsPerEthnicity: 8, }; // Declaring war on a government its people like costs the aggressor dear; a war // on a loathed government can even be popular. The penalty scales with how far // the onlookers' opinion of the victim strays from neutral. export const WAR_DECLARATION = { penalty: 0.3, }; // Cultural impact is a nation's share of the world's cultural output, so it is // zero-sum by construction. A nation above the average impact slowly wins the // world over to its government. export const CULTURE_IMPACT = { dailyPopularityGain: 0.0004, };