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

258 lines
11 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,
};
// An opinion shift as the integer popularity points the policies are quoted in,
// so the wizard's estimates come straight from the applied effect.
export function opinionToPoints(opinion) {
return Math.round(opinion / POPULARITY.opinionPerPoint);
}
// A signed popularity figure with the typographic minus the catalogue uses.
function popularityPointsText(opinion) {
const points = opinionToPoints(opinion);
return `${points < 0 ? "\u2212" : "+"}${Math.abs(points)}`;
}
// 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.",
get targetEffect() {
return `${popularityPointsText(POLICY_OPINION.championTarget)} popularity among the chosen people`;
},
get politicalCost() {
return `${popularityPointsText(POLICY_OPINION.championMajority)} 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.",
get targetEffect() {
return `${popularityPointsText(POLICY_OPINION.protectTarget)} popularity among the chosen people`;
},
get politicalCost() {
return `${popularityPointsText(POLICY_OPINION.protectMajority)} 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",
get politicalCost() {
return `${popularityPointsText(POLICY_OPINION.expelTarget)} 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",
get politicalCost() {
return `${popularityPointsText(POLICY_OPINION.immigrationOthers)} 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,
// No tile loses more than this share of its people to migration in one day,
// whichever mechanism carries them.
maxDailyOutShare: 0.0001,
// 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,
};