refactor(civilization): pick best available land defender as city build default

- Replace hardcoded 'warriors' default with pickNextBuild() for both
  founded and captured cities
- Captured cities now adopt the new owner's tech level when selecting
  their default unit (e.g., Phalanx if Bronze Working is known)
- Add verification tests for tech-dependent defaults on founding and capture
This commit is contained in:
Brian Fertig 2026-07-18 09:08:44 -06:00
parent 30629380b4
commit 61924c439f
2 changed files with 27 additions and 2 deletions

View File

@ -449,13 +449,14 @@ export function foundCity(rules, state, unit, name) {
size: 1, size: 1,
foodBox: 0, foodBox: 0,
shieldBox: 0, shieldBox: 0,
build: { type: 'unit', id: 'warriors' }, build: { type: 'unit', id: 'warriors' }, // safe fallback; pickNextBuild overrides below
buildings: {}, buildings: {},
worked: [], worked: [],
emphasis: 'balanced', emphasis: 'balanced',
routes: [], routes: [],
boughtThisTurn: false, boughtThisTurn: false,
}; };
pickNextBuild(rules, state, city); // best available land defender, not always warriors
state.nextCityId += 1; state.nextCityId += 1;
if (civCities(state, unit.civ).length === 0) city.buildings.palace = true; if (civCities(state, unit.civ).length === 0) city.buildings.palace = true;
// Civ II treats the city square as having a road (its main early trade). // Civ II treats the city square as having a road (its main early trade).
@ -1102,7 +1103,7 @@ export function captureCity(rules, state, unit, city) {
city.size = Math.max(1, city.size - 1); city.size = Math.max(1, city.size - 1);
city.routes = []; city.routes = [];
city.shieldBox = 0; city.shieldBox = 0;
city.build = { type: 'unit', id: 'warriors' }; pickNextBuild(rules, state, city); // best available land defender for the new owner
for (const u of state.units.filter((un) => un.homeCity === city.id)) u.homeCity = null; for (const u of state.units.filter((un) => un.homeCity === city.id)) u.homeCity = null;
unit.x = city.x; unit.x = city.x;
unit.y = city.y; unit.y = city.y;

View File

@ -393,6 +393,30 @@ if (RULES) {
check('capital named for leader', city.name === 'Civ0 City'); check('capital named for leader', city.name === 'Civ0 City');
check('min distance blocks adjacent city', !Logic.canFoundCity(RULES, st, 6, 5)); check('min distance blocks adjacent city', !Logic.canFoundCity(RULES, st, 6, 5));
check('distance 2 allowed', Logic.canFoundCity(RULES, st, 7, 5)); check('distance 2 allowed', Logic.canFoundCity(RULES, st, 7, 5));
check('default build is the best available defender, not always warriors',
city.build.type === 'unit' && city.build.id === 'warriors');
}
// Founding/capturing picks the best available land defender, not a fixed
// 'warriors' default — a civ that already knows Bronze Working should
// found (and inherit captured cities) straight onto Phalanx.
{
const st = makeFlatState({ civs: 2 });
st.civs[0].known.bronzeworking = true;
const settler = Logic.spawnUnit(RULES, st, 0, 'settlers', 5, 5, null);
const city = Logic.foundCity(RULES, st, settler);
check('founded city with Bronze Working known defaults to Phalanx',
city.build.type === 'unit' && city.build.id === 'phalanx');
st.civs[1].known.bronzeworking = false;
const enemySettler = Logic.spawnUnit(RULES, st, 1, 'settlers', 9, 9, null);
const enemyCity = Logic.foundCity(RULES, st, enemySettler);
check('enemy without Bronze Working still defaults to Warriors',
enemyCity.build.type === 'unit' && enemyCity.build.id === 'warriors');
const attacker = Logic.spawnUnit(RULES, st, 0, 'legion', enemyCity.x, enemyCity.y, null);
Logic.captureCity(RULES, st, attacker, enemyCity);
check('captured city re-defaults using the new owner\'s tech (Phalanx)',
enemyCity.build.type === 'unit' && enemyCity.build.id === 'phalanx');
} }
// Growth on flat grassland: foodbox fills, granary keeps half. // Growth on flat grassland: foodbox fills, granary keeps half.