Production buildings are now independent private-sector agents: they keep what their output sells for, run only to the demand they can actually sell, and spend that cash on steel and high-tech upgrades when their demand grows. Grid power is a paid private flow between plants and their consumers, and regions synthesise the food their own harvest and the reachable market cannot supply. Island regions no longer famine: a region with no reachable supplier grows its whole shortfall from energy, and grid power now feeds cities and food before the converters, so a famine never waits on a factory. Bundles the in-progress tree cleanup that was already present in the working tree.
231 lines
9.6 KiB
JavaScript
231 lines
9.6 KiB
JavaScript
// 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,
|
||
};
|