feat(civilization): add coinage/public works special builds + fix AI fallback
- Add Coinage (1 shield → 1 gold) and Public Works (2 shields → 1 food) special build options for cities, with remainder carry logic in shieldBox - Update AI to fall back to producing gold or food instead of stockpiling redundant defenders when nothing else is available - Fix mid-build detection to check build.type before comparing shield progress - Add City Screen UI for special builds: display gain per turn, color-coded rows, and tooltips - Only apply class-switch penalty when switching between unit/building production types - Add verification tests for coinage payout, public works remainder carry, and AI special build fallback feat(superkart): fix player kart scale + add missing backdrop images - Compute PLAYER_KART_SCALE from MODE7 parameters instead of hardcoding, keeping player and AI karts consistent at the camera's follow distance - Wire up previously null backdrops: volcano, scrapyard, swamp, speedway, nightcity
This commit is contained in:
parent
65cc0a2c79
commit
30629380b4
Binary file not shown.
|
After Width: | Height: | Size: 357 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 362 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 391 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 470 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
|
|
@ -50,11 +50,11 @@
|
||||||
},
|
},
|
||||||
"backdrops": {
|
"backdrops": {
|
||||||
"beach": { "key": "superkart-backdrop-beach", "path": "assets/images/superkart/backdrop-beach.png" },
|
"beach": { "key": "superkart-backdrop-beach", "path": "assets/images/superkart/backdrop-beach.png" },
|
||||||
"volcano": { "key": "superkart-backdrop-volcano", "path": null },
|
"volcano": { "key": "superkart-backdrop-volcano", "path": "assets/images/superkart/backdrop-volcano.png" },
|
||||||
"scrapyard": { "key": "superkart-backdrop-scrapyard", "path": null },
|
"scrapyard": { "key": "superkart-backdrop-scrapyard", "path": "assets/images/superkart/backdrop-scrapyard.png" },
|
||||||
"swamp": { "key": "superkart-backdrop-swamp", "path": null },
|
"swamp": { "key": "superkart-backdrop-swamp", "path": "assets/images/superkart/backdrop-swamp.png" },
|
||||||
"speedway": { "key": "superkart-backdrop-speedway", "path": null },
|
"speedway": { "key": "superkart-backdrop-speedway", "path": "assets/images/superkart/backdrop-speedway.png" },
|
||||||
"nightcity": { "key": "superkart-backdrop-nightcity", "path": null }
|
"nightcity": { "key": "superkart-backdrop-nightcity", "path": "assets/images/superkart/backdrop-nightcity.png" }
|
||||||
},
|
},
|
||||||
"itemSheet": { "key": "superkart-items", "path": null, "frameWidth": 48, "frameHeight": 48 }
|
"itemSheet": { "key": "superkart-items", "path": null, "frameWidth": 48, "frameHeight": 48 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,8 @@ const DEVELOP_CHAIN = ['granary', 'library', 'marketplace', 'barracks', 'aqueduc
|
||||||
function manageCityBuild(rules, state, civIdx, city, strategy) {
|
function manageCityBuild(rules, state, civIdx, city, strategy) {
|
||||||
const civ = state.civs[civIdx];
|
const civ = state.civs[civIdx];
|
||||||
const def = rules.units[city.build?.id];
|
const def = rules.units[city.build?.id];
|
||||||
const midBuild = city.shieldBox > 0 && city.shieldBox < buildCost(rules, city) * 0.9;
|
const midBuild = (city.build.type === 'unit' || city.build.type === 'building')
|
||||||
|
&& city.shieldBox > 0 && city.shieldBox < buildCost(rules, city) * 0.9;
|
||||||
const defenders = unitsAt(state, city.x, city.y)
|
const defenders = unitsAt(state, city.x, city.y)
|
||||||
.filter((u) => u.civ === civIdx && rules.units[u.type].domain === 'land'
|
.filter((u) => u.civ === civIdx && rules.units[u.type].domain === 'land'
|
||||||
&& !rules.units[u.type].flags.includes('noncombat'));
|
&& !rules.units[u.type].flags.includes('noncombat'));
|
||||||
|
|
@ -335,8 +336,10 @@ function manageCityBuild(rules, state, civIdx, city, strategy) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fallback = bestDefender(rules, state, civ, city);
|
// Nothing left to build or develop — convert shields to gold or food
|
||||||
if (fallback) setBuild(rules, state, city, 'unit', fallback.id);
|
// instead of stockpiling redundant defenders.
|
||||||
|
const preferFood = yields.foodSurplus < 3;
|
||||||
|
setBuild(rules, state, city, preferFood ? 'food' : 'gold', preferFood ? 'publicworks' : 'coinage');
|
||||||
}
|
}
|
||||||
|
|
||||||
function bestDefender(rules, state, civ, city) {
|
function bestDefender(rules, state, civ, city) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
|
||||||
import { Button } from '../../ui/Button.js';
|
import { Button } from '../../ui/Button.js';
|
||||||
import { Tooltip } from '../../ui/Tooltip.js';
|
import { Tooltip } from '../../ui/Tooltip.js';
|
||||||
import * as Logic from './CivilizationLogic.js';
|
import * as Logic from './CivilizationLogic.js';
|
||||||
import { describeUnitTooltip, describeBuildingTooltip } from './CivilizationTooltips.js';
|
import { describeUnitTooltip, describeBuildingTooltip, describeSpecialBuildTooltip } from './CivilizationTooltips.js';
|
||||||
|
|
||||||
const FONT = '"Julius Sans One"';
|
const FONT = '"Julius Sans One"';
|
||||||
|
|
||||||
|
|
@ -117,6 +117,16 @@ export function openCityScreen(scene, rules, state, city, onClose) {
|
||||||
|
|
||||||
// --- Build picker (right column)
|
// --- Build picker (right column)
|
||||||
const rx = px + 60;
|
const rx = px + 60;
|
||||||
|
const isSpecialBuild = city.build.type === 'gold' || city.build.type === 'food';
|
||||||
|
if (isSpecialBuild) {
|
||||||
|
const spec = Logic.SPECIAL_BUILDS[city.build.type];
|
||||||
|
const gain = Math.floor((city.shieldBox + y.shield) / spec.den) * spec.num;
|
||||||
|
const unitLabel = city.build.type === 'gold' ? 'gold' : 'food';
|
||||||
|
dynamic.add(scene.add.text(rx, top + 64,
|
||||||
|
`Producing: ${spec.name} +${gain} ${unitLabel}/turn`, {
|
||||||
|
fontFamily: FONT, fontSize: '22px', color: COLORS.textHex,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
const cost = Logic.buildCost(rules, city);
|
const cost = Logic.buildCost(rules, city);
|
||||||
const cur = city.build.type === 'unit' ? rules.units[city.build.id] : rules.buildings[city.build.id];
|
const cur = city.build.type === 'unit' ? rules.units[city.build.id] : rules.buildings[city.build.id];
|
||||||
const shownShields = Math.min(city.shieldBox, cost);
|
const shownShields = Math.min(city.shieldBox, cost);
|
||||||
|
|
@ -130,16 +140,19 @@ export function openCityScreen(scene, rules, state, city, onClose) {
|
||||||
if (Logic.buyBuild(rules, state, city)) redraw();
|
if (Logic.buyBuild(rules, state, city)) redraw();
|
||||||
}, { width: 190, height: 46, fontSize: 18, variant: civ.gold >= buyPrice && !city.boughtThisTurn ? 'solid' : 'ghost' });
|
}, { width: 190, height: 46, fontSize: 18, variant: civ.gold >= buyPrice && !city.boughtThisTurn ? 'solid' : 'ghost' });
|
||||||
dynamic.add(buyBtn);
|
dynamic.add(buyBtn);
|
||||||
|
}
|
||||||
dynamic.add(scene.add.text(rx, top + 108, `Treasury: ${civ.gold} gold`, {
|
dynamic.add(scene.add.text(rx, top + 108, `Treasury: ${civ.gold} gold`, {
|
||||||
fontFamily: FONT, fontSize: '19px', color: COLORS.goldHex,
|
fontFamily: FONT, fontSize: '19px', color: COLORS.goldHex,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Choices: units then buildings, two columns of rows.
|
// Choices: Coinage/Public Works, then units, then buildings, two columns of rows.
|
||||||
|
const specialChoices = Object.entries(Logic.SPECIAL_BUILDS)
|
||||||
|
.map(([type, spec]) => ({ type, id: spec.id, label: spec.name }));
|
||||||
const unitChoices = Logic.availableUnits(rules, state, civ, city)
|
const unitChoices = Logic.availableUnits(rules, state, civ, city)
|
||||||
.map((u) => ({ type: 'unit', id: u.id, label: `${u.name} (${u.cost})`, ruleObj: u }));
|
.map((u) => ({ type: 'unit', id: u.id, label: `${u.name} (${u.cost})`, ruleObj: u }));
|
||||||
const bldChoices = Logic.availableBuildings(rules, state, civ, city)
|
const bldChoices = Logic.availableBuildings(rules, state, civ, city)
|
||||||
.map((b) => ({ type: 'building', id: b.id, label: `${b.name} (${b.cost})`, ruleObj: b }));
|
.map((b) => ({ type: 'building', id: b.id, label: `${b.name} (${b.cost})`, ruleObj: b }));
|
||||||
const choices = [...unitChoices, ...bldChoices];
|
const choices = [...specialChoices, ...unitChoices, ...bldChoices];
|
||||||
const colW = 330;
|
const colW = 330;
|
||||||
const rowH = 36;
|
const rowH = 36;
|
||||||
const perCol = 14; // leaves room below for the garrison strip
|
const perCol = 14; // leaves room below for the garrison strip
|
||||||
|
|
@ -150,18 +163,20 @@ export function openCityScreen(scene, rules, state, city, onClose) {
|
||||||
const rect = scene.add.rectangle(colX + colW / 2, rowY, colW, rowH - 4,
|
const rect = scene.add.rectangle(colX + colW / 2, rowY, colW, rowH - 4,
|
||||||
active ? 0x3a3222 : 0x181510)
|
active ? 0x3a3222 : 0x181510)
|
||||||
.setStrokeStyle(1, active ? COLORS.gold : COLORS.muted, active ? 1 : 0.5);
|
.setStrokeStyle(1, active ? COLORS.gold : COLORS.muted, active ? 1 : 0.5);
|
||||||
|
const rowColor = ch.type === 'unit' ? '#cfe3ff' : (ch.type === 'building' ? '#ffe9bd' : '#c9f2c0');
|
||||||
const txt = scene.add.text(colX + 10, rowY, ch.label, {
|
const txt = scene.add.text(colX + 10, rowY, ch.label, {
|
||||||
fontFamily: FONT, fontSize: '16px',
|
fontFamily: FONT, fontSize: '16px', color: rowColor,
|
||||||
color: ch.type === 'unit' ? '#cfe3ff' : '#ffe9bd',
|
|
||||||
}).setOrigin(0, 0.5);
|
}).setOrigin(0, 0.5);
|
||||||
rect.setInteractive({ useHandCursor: true });
|
rect.setInteractive({ useHandCursor: true });
|
||||||
rect.on('pointerdown', () => {
|
rect.on('pointerdown', () => {
|
||||||
Logic.setBuild(rules, state, city, ch.type, ch.id);
|
Logic.setBuild(rules, state, city, ch.type, ch.id);
|
||||||
redraw();
|
redraw();
|
||||||
});
|
});
|
||||||
tooltip.attachTo(rect, () => (ch.type === 'unit'
|
tooltip.attachTo(rect, () => {
|
||||||
? describeUnitTooltip(rules, ch.ruleObj)
|
if (ch.type === 'unit') return describeUnitTooltip(rules, ch.ruleObj);
|
||||||
: describeBuildingTooltip(rules, ch.ruleObj)));
|
if (ch.type === 'building') return describeBuildingTooltip(rules, ch.ruleObj);
|
||||||
|
return describeSpecialBuildTooltip(ch.type);
|
||||||
|
});
|
||||||
dynamic.add(rect);
|
dynamic.add(rect);
|
||||||
dynamic.add(txt);
|
dynamic.add(txt);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -468,8 +468,22 @@ export function foundCity(rules, state, unit, name) {
|
||||||
return city;
|
return city;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Coinage/Public Works: convert shields straight into gold/food every turn
|
||||||
|
// instead of banking them toward a unit/building. `den` lets food run at a
|
||||||
|
// worse ratio than gold without losing shields — see processCity, which
|
||||||
|
// reuses city.shieldBox as the carry register for the remainder.
|
||||||
|
export const SPECIAL_BUILDS = {
|
||||||
|
gold: { id: 'coinage', name: 'Coinage', num: 1, den: 1 },
|
||||||
|
food: { id: 'publicworks', name: 'Public Works', num: 1, den: 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
function isProductionClass(type) {
|
||||||
|
return type === 'unit' || type === 'building';
|
||||||
|
}
|
||||||
|
|
||||||
export function setBuild(rules, state, city, type, id) {
|
export function setBuild(rules, state, city, type, id) {
|
||||||
if (city.build && city.build.type !== type && city.shieldBox > 0) {
|
if (city.build && city.build.type !== type && city.shieldBox > 0
|
||||||
|
&& isProductionClass(city.build.type) && isProductionClass(type)) {
|
||||||
city.shieldBox = Math.floor(city.shieldBox / 2); // Civ II class-switch penalty
|
city.shieldBox = Math.floor(city.shieldBox / 2); // Civ II class-switch penalty
|
||||||
}
|
}
|
||||||
city.build = { type, id };
|
city.build = { type, id };
|
||||||
|
|
@ -574,12 +588,24 @@ function processCity(rules, state, city) {
|
||||||
if (city.size <= 0) { destroyCity(rules, state, city); return; }
|
if (city.size <= 0) { destroyCity(rules, state, city); return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shields. Capped at cost — a settler build stalled on city.size < 2
|
// Shields.
|
||||||
|
if (city.build.type === 'gold' || city.build.type === 'food') {
|
||||||
|
// Coinage/Public Works: shieldBox is repurposed as a carry register so
|
||||||
|
// the 2:1 food ratio never loses an odd shield across turns.
|
||||||
|
const spec = SPECIAL_BUILDS[city.build.type];
|
||||||
|
city.shieldBox += y.shield;
|
||||||
|
const gain = Math.floor(city.shieldBox / spec.den) * spec.num;
|
||||||
|
city.shieldBox -= Math.floor(gain / spec.num) * spec.den;
|
||||||
|
if (city.build.type === 'gold') civ.gold += gain;
|
||||||
|
else city.foodBox += gain;
|
||||||
|
} else {
|
||||||
|
// Capped at cost — a settler build stalled on city.size < 2
|
||||||
// (see completeBuild) would otherwise keep banking shields turn after
|
// (see completeBuild) would otherwise keep banking shields turn after
|
||||||
// turn with nowhere to go, showing e.g. "60/40 shields" and a negative
|
// turn with nowhere to go, showing e.g. "60/40 shields" and a negative
|
||||||
// turn count once shields overshot the cost.
|
// turn count once shields overshot the cost.
|
||||||
city.shieldBox = Math.min(city.shieldBox + y.shield, buildCost(rules, city));
|
city.shieldBox = Math.min(city.shieldBox + y.shield, buildCost(rules, city));
|
||||||
if (city.shieldBox >= buildCost(rules, city)) completeBuild(rules, state, city);
|
if (city.shieldBox >= buildCost(rules, city)) completeBuild(rules, state, city);
|
||||||
|
}
|
||||||
city.boughtThisTurn = false;
|
city.boughtThisTurn = false;
|
||||||
|
|
||||||
// Economy.
|
// Economy.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { COLORS } from '../../config.js';
|
import { COLORS } from '../../config.js';
|
||||||
import { cityById, cityAt, tileIndex, defenderStrength, unitsOnBoat, IMP } from './CivilizationLogic.js';
|
import {
|
||||||
|
cityById, cityAt, tileIndex, defenderStrength, unitsOnBoat, IMP, SPECIAL_BUILDS,
|
||||||
|
} from './CivilizationLogic.js';
|
||||||
|
|
||||||
// Flavor/ability text for unit flags — data/civilization-rules.json has no
|
// Flavor/ability text for unit flags — data/civilization-rules.json has no
|
||||||
// description field on units, so this is authored here. A few flags
|
// description field on units, so this is authored here. A few flags
|
||||||
|
|
@ -101,6 +103,15 @@ export function describeBuildingTooltip(rules, building) {
|
||||||
return { title: building.name, lines };
|
return { title: building.name, lines };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Coinage/Public Works: no cost/upkeep to report, just what the conversion does.
|
||||||
|
export function describeSpecialBuildTooltip(type) {
|
||||||
|
const spec = SPECIAL_BUILDS[type];
|
||||||
|
const text = type === 'gold'
|
||||||
|
? 'Converts this city\'s shield production directly into gold, 1 shield = 1 gold, every turn.'
|
||||||
|
: 'Converts this city\'s shield production directly into food, 2 shields = 1 food, every turn.';
|
||||||
|
return { title: spec.name, lines: [{ text }] };
|
||||||
|
}
|
||||||
|
|
||||||
// Combat is always a duel against the tile's single strongest defender
|
// Combat is always a duel against the tile's single strongest defender
|
||||||
// (pickDefender), so enemy tooltips surface that unit's effective defense —
|
// (pickDefender), so enemy tooltips surface that unit's effective defense —
|
||||||
// terrain, fortify, walls etc. included — instead of letting the raw stat
|
// terrain, fortify, walls etc. included — instead of letting the raw stat
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,11 @@ const D = {
|
||||||
};
|
};
|
||||||
const ITEM_FRAME = { bolt: 0, seeker: 1, oil: 2, turbo: 3, overdrive: 4, emp: 5, coins: 6 };
|
const ITEM_FRAME = { bolt: 0, seeker: 1, oil: 2, turbo: 3, overdrive: 4, emp: 5, coins: 6 };
|
||||||
const BOX_FRAME = 7;
|
const BOX_FRAME = 7;
|
||||||
|
// The player kart is a screen-fixed sprite rather than a projected world
|
||||||
|
// sprite, so its scale is pinned to what MODE7.project() would give an AI
|
||||||
|
// kart sitting at the camera's follow distance — keeps both the same size
|
||||||
|
// when a rival is drafting right alongside the player.
|
||||||
|
const PLAYER_KART_SCALE = (MODE7.spriteBase / 64) * (MODE7.focal / MODE7.followDist);
|
||||||
const COIN_FRAME = 8;
|
const COIN_FRAME = 8;
|
||||||
|
|
||||||
export default class SuperKartGame extends Phaser.Scene {
|
export default class SuperKartGame extends Phaser.Scene {
|
||||||
|
|
@ -520,7 +525,7 @@ export default class SuperKartGame extends Phaser.Scene {
|
||||||
// Player kart, fixed at the bottom center like the SNES original.
|
// Player kart, fixed at the bottom center like the SNES original.
|
||||||
const pk = this.playerKartState();
|
const pk = this.playerKartState();
|
||||||
this.playerSprite = this.add.sprite(GAME_WIDTH / 2, GAME_HEIGHT * 0.75,
|
this.playerSprite = this.add.sprite(GAME_WIDTH / 2, GAME_HEIGHT * 0.75,
|
||||||
this.kartTexKey(pk.racer.id), 0).setDepth(D.playerKart).setScale(3.1);
|
this.kartTexKey(pk.racer.id), 0).setDepth(D.playerKart).setScale(PLAYER_KART_SCALE);
|
||||||
|
|
||||||
this.debugG = this.add.graphics().setDepth(D.debug).setVisible(this.debugOn);
|
this.debugG = this.add.graphics().setDepth(D.debug).setVisible(this.debugOn);
|
||||||
}
|
}
|
||||||
|
|
@ -721,7 +726,7 @@ export default class SuperKartGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
s.setFrame(frame);
|
s.setFrame(frame);
|
||||||
s.setAngle(angle);
|
s.setAngle(angle);
|
||||||
let scale = 3.1;
|
let scale = PLAYER_KART_SCALE;
|
||||||
if (kart.empMs > 0) scale *= 0.55;
|
if (kart.empMs > 0) scale *= 0.55;
|
||||||
if (kart.squashMs > 0) s.setScale(scale * 1.2, scale * 0.5);
|
if (kart.squashMs > 0) s.setScale(scale * 1.2, scale * 0.5);
|
||||||
else s.setScale(scale);
|
else s.setScale(scale);
|
||||||
|
|
|
||||||
|
|
@ -622,6 +622,62 @@ if (RULES) {
|
||||||
check('revolution completes', civ.government === 'monarchy');
|
check('revolution completes', civ.government === 'monarchy');
|
||||||
check('cannot switch to unknown gov', !Logic.startRevolution(RULES, st, civ, 'democracy'));
|
check('cannot switch to unknown gov', !Logic.startRevolution(RULES, st, civ, 'democracy'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Coinage: shields convert straight to gold, 1:1, every turn, no shieldBox growth.
|
||||||
|
{
|
||||||
|
const st = makeFlatState();
|
||||||
|
const settler = Logic.spawnUnit(RULES, st, 0, 'settlers', 5, 5, null);
|
||||||
|
const city = Logic.foundCity(RULES, st, settler);
|
||||||
|
Logic.setBuild(RULES, st, city, 'gold', 'coinage');
|
||||||
|
const civ = st.civs[0];
|
||||||
|
civ.gold = 0;
|
||||||
|
const y1 = Logic.cityYields(RULES, st, city);
|
||||||
|
Logic.beginCivTurn(RULES, st, 0);
|
||||||
|
const expected = y1.gold - y1.upkeep - y1.supportGold + y1.shield;
|
||||||
|
check('coinage pays exactly y.shield gold on top of normal trade income', civ.gold === expected);
|
||||||
|
check('coinage leaves shieldBox at 0', city.shieldBox === 0);
|
||||||
|
const goldAfter1 = civ.gold;
|
||||||
|
Logic.beginCivTurn(RULES, st, 0);
|
||||||
|
check('coinage keeps paying out turn after turn', civ.gold > goldAfter1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public Works: shields convert to food at 2:1, with the odd shield carried
|
||||||
|
// in shieldBox instead of lost, and switching build types doesn't halve it.
|
||||||
|
{
|
||||||
|
const st = makeFlatState();
|
||||||
|
const settler = Logic.spawnUnit(RULES, st, 0, 'settlers', 5, 5, null);
|
||||||
|
const city = Logic.foundCity(RULES, st, settler);
|
||||||
|
Logic.setBuild(RULES, st, city, 'food', 'publicworks');
|
||||||
|
const y = Logic.cityYields(RULES, st, city);
|
||||||
|
const foodBoxBefore = city.foodBox;
|
||||||
|
Logic.beginCivTurn(RULES, st, 0);
|
||||||
|
const gain1 = Math.floor(y.shield / 2);
|
||||||
|
const remainder1 = y.shield - gain1 * 2; // 0 or 1, carried in shieldBox
|
||||||
|
check('public works food gain matches floor(shield/2)',
|
||||||
|
city.foodBox === foodBoxBefore + y.foodSurplus + gain1);
|
||||||
|
check('odd shield is carried in shieldBox rather than lost', city.shieldBox === remainder1);
|
||||||
|
const carry = city.shieldBox;
|
||||||
|
check('switching away from a special build does not halve the carry',
|
||||||
|
(() => { Logic.setBuild(RULES, st, city, 'building', 'granary'); return city.shieldBox === carry; })());
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI fallback: an idle city with nothing left to build picks gold or food
|
||||||
|
// instead of endlessly stacking defenders.
|
||||||
|
{
|
||||||
|
const st = makeFlatState();
|
||||||
|
const settler = Logic.spawnUnit(RULES, st, 0, 'settlers', 5, 5, null);
|
||||||
|
const city = Logic.foundCity(RULES, st, settler);
|
||||||
|
st.civs[0].human = false;
|
||||||
|
// Give it a defender so the garrison branch doesn't fire, and mark every
|
||||||
|
// building/unit tier as already handled so develop/expand/war all pass.
|
||||||
|
Logic.spawnUnit(RULES, st, 0, 'phalanx', 5, 5, city.id);
|
||||||
|
for (const b of RULES.buildingList) city.buildings[b.id] = true;
|
||||||
|
delete city.buildings.palace; // keep palace semantics untouched
|
||||||
|
city.buildings.palace = true;
|
||||||
|
AI.runAITurn(RULES, st, 0);
|
||||||
|
check('idle AI city settles on Coinage or Public Works',
|
||||||
|
city.build.type === 'gold' || city.build.type === 'food');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue