From 4d864503f2d45f1c4b4545c109b937ed89a83d6a Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Wed, 15 Jul 2026 23:36:53 -0600 Subject: [PATCH] fix: cap city shields at build cost to prevent overflow display When a city cannot complete a build (e.g., size < 2 for settlers), shields were accumulating past the build cost, showing confusing values like "60/40 shields" or negative turn counts. - Cap shieldBox at buildCost each turn in processCity - Clamp displayed shields to min(shieldBox, cost) in city screen - Add tests verifying shield capping and stalled build completion --- .../civilization/CivilizationCityScreen.js | 5 ++-- src/games/civilization/CivilizationLogic.js | 7 +++-- tools/verifyCivilization.js | 27 +++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/games/civilization/CivilizationCityScreen.js b/src/games/civilization/CivilizationCityScreen.js index 3878505..0438baf 100644 --- a/src/games/civilization/CivilizationCityScreen.js +++ b/src/games/civilization/CivilizationCityScreen.js @@ -114,9 +114,10 @@ export function openCityScreen(scene, rules, state, city, onClose) { const rx = px + 60; const cost = Logic.buildCost(rules, city); const cur = city.build.type === 'unit' ? rules.units[city.build.id] : rules.buildings[city.build.id]; - const turns = y.shield > 0 ? Math.ceil((cost - city.shieldBox) / y.shield) : '∞'; + const shownShields = Math.min(city.shieldBox, cost); + const turns = y.shield > 0 ? Math.max(0, Math.ceil((cost - shownShields) / y.shield)) : '∞'; dynamic.add(scene.add.text(rx, top + 64, - `Building: ${cur.name} ${city.shieldBox}/${cost} shields (${turns} turns)`, { + `Building: ${cur.name} ${shownShields}/${cost} shields (${turns} turns)`, { fontFamily: FONT, fontSize: '22px', color: COLORS.textHex, })); const buyPrice = Logic.buyCost(rules, city); diff --git a/src/games/civilization/CivilizationLogic.js b/src/games/civilization/CivilizationLogic.js index 144b0dc..430a5bd 100644 --- a/src/games/civilization/CivilizationLogic.js +++ b/src/games/civilization/CivilizationLogic.js @@ -548,8 +548,11 @@ function processCity(rules, state, city) { if (city.size <= 0) { destroyCity(rules, state, city); return; } } - // Shields. - city.shieldBox += y.shield; + // Shields. Capped at cost — a settler build stalled on city.size < 2 + // (see completeBuild) would otherwise keep banking shields turn after + // turn with nowhere to go, showing e.g. "60/40 shields" and a negative + // turn count once shields overshot the cost. + city.shieldBox = Math.min(city.shieldBox + y.shield, buildCost(rules, city)); if (city.shieldBox >= buildCost(rules, city)) completeBuild(rules, state, city); city.boughtThisTurn = false; diff --git a/tools/verifyCivilization.js b/tools/verifyCivilization.js index 1e53fff..9ce65c2 100644 --- a/tools/verifyCivilization.js +++ b/tools/verifyCivilization.js @@ -336,6 +336,33 @@ if (RULES) { check('granary keeps half box', city.foodBox >= Math.floor(((sizeBefore + 1) * Logic.FOODBOX_PER_SIZE) / 2)); } + // Settler-hold: a size-1 city building a settler must not bank shields + // past cost turn after turn while it waits for the city to spare a + // citizen (used to show e.g. "60/40 shields (-5 turns)"). + { + const st = makeFlatState({ terrain: 'plains' }); + const city = Logic.foundCity(RULES, st, Logic.spawnUnit(RULES, st, 0, 'settlers', 5, 5, null)); + city.build = { type: 'unit', id: 'settlers' }; + const cost = Logic.buildCost(RULES, city); + city.size = 1; + city.shieldBox = cost + 20; // as if it had banked well past cost over many stalled turns + city.foodBox = 0; + Logic.beginCivTurn(RULES, st, 0); + check('shieldBox re-capped at cost on a stalled turn', city.shieldBox <= cost, + `${city.shieldBox}/${cost}`); + + // Once the city can spare a citizen, the stalled build completes + // immediately. foodBox=2 keeps this turn's -1 deficit (size-2 plains + // under despotism) from also triggering a famine shrink, which would + // confound the size assertion below. + const unitsBefore = st.units.length; + city.size = 2; + city.foodBox = 2; + Logic.beginCivTurn(RULES, st, 0); + check('stalled settler completes once city can spare population', + st.units.length === unitsBefore + 1 && city.size === 1 && city.shieldBox === 0); + } + // Despotism penalty & government trade bonus on tile yields. { const st = makeFlatState({ terrain: 'plains' });