feat(civ): add shield waste mechanic and AI corruption triage

- Implement "waste" as corruption's production sibling: distant cities lose
  shields to waste (half the corruption decay rate), halved by courthouses.
- Update city yields, UI summaries, and government screen to display waste
  alongside corruption and support costs.
- AI now builds courthouses early when combined corruption + waste loss
  exceeds 4 per turn, preventing distant cities from becoming useless.
- Refresh building effect tooltips and palace description to reflect waste.
This commit is contained in:
Brian Fertig 2026-07-17 11:05:01 -06:00
parent aee3352efe
commit b214f6b8bd
5 changed files with 27 additions and 13 deletions

View File

@ -319,6 +319,13 @@ function manageCityBuild(rules, state, civIdx, city, strategy) {
setBuild(rules, state, city, 'unit', cvType);
return;
}
// Corruption triage: distant cities bleed trade to corruption and shields
// to waste — when the combined loss is worth a building's upkeep several
// times over, jump the chain straight to a courthouse (halves both).
if ((yields.corruption + yields.waste) >= 4 && avail.some((b) => b.id === 'courthouse')) {
setBuild(rules, state, city, 'building', 'courthouse');
return;
}
for (const id of DEVELOP_CHAIN) {
if (avail.some((b) => b.id === id)) {
// Skip aqueduct/sewer until the city is close to its cap.

View File

@ -61,7 +61,7 @@ export function openCityScreen(scene, rules, state, city, onClose) {
// Yields breakdown.
const lines = [
`Food ${y.food} (need ${y.foodNeed}) surplus ${y.foodSurplus >= 0 ? '+' : ''}${y.foodSurplus}`,
`Shields ${y.shield}${y.supportShields ? ` (support ${y.supportShields})` : ''}`,
`Shields ${y.shield}${y.waste ? ` waste ${y.waste}` : ''}${y.supportShields ? ` (support ${y.supportShields})` : ''}`,
`Trade ${y.trade} corruption ${y.corruption}${y.routeTrade ? ` routes +${y.routeTrade}` : ''}`,
`Gold +${y.gold} · Science +${y.science} · Upkeep ${y.upkeep}`,
];

View File

@ -380,14 +380,21 @@ export function cityYields(rules, state, city) {
}
const grossShield = Math.floor(shield * shieldMult);
const netShield = Math.max(0, grossShield - supportShields);
// Waste — corruption's production sibling: the same capital-distance decay
// applied to shields, at half strength (full strength would zero out
// distant cities' build boxes entirely under Despotism). Courthouse halves
// it, same as corruption.
let waste = Math.floor(grossShield * gov.corruptionFactor * Math.min(1, dist / 20) * 0.5);
if (city.buildings.courthouse) waste = Math.floor(waste * 0.5);
waste = Math.min(waste, grossShield);
const netShield = Math.max(0, grossShield - waste - supportShields);
const foodNeed = city.size * FOOD_PER_CITIZEN + settlerFood;
const upkeep = Object.keys(city.buildings)
.reduce((sum, id) => sum + (rules.buildings[id]?.upkeep ?? 0), 0);
return {
food, foodNeed, foodSurplus: food - foodNeed,
shield: netShield, grossShield, supportShields,
shield: netShield, grossShield, waste, supportShields,
trade, corruption, netTrade, routeTrade,
gold: Math.floor(baseGold * goldMult), science: Math.floor(baseScience * sciMult),
upkeep, supportGold,

View File

@ -466,12 +466,12 @@ function govBullets(g) {
const pro = (text) => out.push({ text, pro: true });
const con = (text) => out.push({ text, pro: false });
if (g.noScience) con('No research — science output is zero');
if (g.corruptionFactor === 0) pro('No corruption at all');
if (g.corruptionFactor === 0) pro('No corruption or waste at all');
else if (g.corruptionFactor <= 0.4) {
pro(g.flatCorruption ? 'Low corruption, the same across the whole empire' : 'Low corruption');
} else if (g.corruptionFactor <= 0.7) pro('Reduced corruption');
else if (g.corruptionFactor >= 1.5) con('Crippling corruption');
else con('Heavy corruption, worse far from the capital');
pro(g.flatCorruption ? 'Low corruption & waste, the same across the whole empire' : 'Low corruption & waste');
} else if (g.corruptionFactor <= 0.7) pro('Reduced corruption & waste');
else if (g.corruptionFactor >= 1.5) con('Crippling corruption & waste');
else con('Heavy corruption & waste, worse far from the capital');
if (g.tradeBonus) pro(`+${g.tradeBonus} trade on tiles already producing trade`);
if (g.despotPenalty) con('1 food/shield/trade on tiles yielding 3 or more');
if (g.freeUnits > 0) pro(`${g.freeUnits} free units per city, then 1 ${g.unitUpkeep}/unit each turn`);
@ -502,12 +502,12 @@ export function openGovernmentScreen(scene, rules, state, onClose) {
// aggregated from the same cityYields the cities themselves run on.
const cities = Logic.civCities(state, state.humanIndex);
const sum = {
gross: 0, support: 0, shield: 0, trade: 0, corruption: 0, netTrade: 0,
gross: 0, waste: 0, support: 0, shield: 0, trade: 0, corruption: 0, netTrade: 0,
gold: 0, upkeep: 0, supportGold: 0, science: 0, pop: 0,
};
for (const c of cities) {
const y = Logic.cityYields(rules, state, c);
sum.gross += y.grossShield; sum.support += y.supportShields; sum.shield += y.shield;
sum.gross += y.grossShield; sum.waste += y.waste; sum.support += y.supportShields; sum.shield += y.shield;
sum.trade += y.trade; sum.corruption += y.corruption; sum.netTrade += y.netTrade;
sum.gold += y.gold; sum.upkeep += y.upkeep; sum.supportGold += y.supportGold;
sum.science += y.science; sum.pop += c.size;
@ -521,7 +521,7 @@ export function openGovernmentScreen(scene, rules, state, onClose) {
}));
const netGold = sum.gold - sum.upkeep - sum.supportGold;
content.add(scene.add.text(left, top + 44,
`Production: ${sum.shield} shields/turn (${sum.gross} gross ${sum.support} unit support) `
`Production: ${sum.shield} shields/turn (${sum.gross} gross ${sum.waste} waste ${sum.support} unit support) `
+ `Trade: ${sum.netTrade}/turn (${sum.trade} ${sum.corruption} corruption)`, {
fontFamily: FONT, fontSize: '19px', color: COLORS.textHex,
}));

View File

@ -43,7 +43,7 @@ const EFFECT_TEXT = {
farmfood: (b) => `+${Math.round(b.value * 100)}% food from farmland-improved tiles worked by this city.`,
defenseair: (b) => `Multiplies this city's defense against air attacks by ${b.value}.`,
power: (b) => `+${Math.round(b.value * 100)}% shield output from the city's Factory (requires a Factory).`,
corruption: (b) => `Cuts this city's corruption by ${Math.round(b.value * 100)}%.`,
corruption: (b) => `Cuts this city's corruption and waste by ${Math.round(b.value * 100)}%.`,
gold: (b) => `+${Math.round(b.value * 100)}% gold income from this city's trade.`,
veterans: () => 'Units built here start as veterans, with a combat bonus.',
sizecap: (b) => (b.id === 'sewersystem'
@ -52,7 +52,7 @@ const EFFECT_TEXT = {
oceanfood: (b) => `+${b.value} food from every ocean tile this city works.`,
shields: (b) => `+${Math.round(b.value * 100)}% shield (production) output from this city.`,
oceanshield: (b) => `+${b.value} shield from every ocean tile this city works.`,
palace: () => 'Makes this your capital — sharply reduces corruption and stores the seat of government.',
palace: () => 'Makes this your capital — corruption and waste grow with distance from it.',
defensesea: (b) => `Multiplies this city's defense against naval attacks by ${b.value}.`,
};