Made queued buildings gather their materials and rise ten times faster

City building orders now keep their production-queue slot but open in a materials phase: the order reserves the market value of its steel and high-tech, buys from the nation's own stores and reachable suppliers once a day, and only joins a construction line once the bill is settled, so a city with no supplier simply waits. The queue, the city panel and the quick view show the gathering progress instead of a stalled bar.

Buildings and tile improvements now take a tenth of the old time; unit training is untouched. Any material money left over is returned when the work completes.
This commit is contained in:
2026-09-22 21:43:31 +02:00
parent c8260b2108
commit aa726ff431
11 changed files with 309 additions and 49 deletions
+6 -6
View File
@@ -1,6 +1,12 @@
// Generated by scripts/generate-devlog.js from `git log`; do not edit.
// The pre-commit hook refreshes it so the main menu shows the latest commits.
export const DEVLOG = [
{
"hash": "c8260b2",
"date": "2026-09-22",
"subject": "Better tile building management",
"body": ""
},
{
"hash": "7c28c45",
"date": "2026-09-22",
@@ -54,11 +60,5 @@ export const DEVLOG = [
"date": "2026-09-22",
"subject": "Gave regions their own economies and removed the global market",
"body": "- Each nation has a currency and each region a private-sector cash basket, borrowing from its central bank at the bank's rate; the government may always pay, going into central-bank debt when its budget runs red.\n- Resources are bought from reachable stores only: the region itself first, then same-nation regions, then foreign ones, paid for out of private cash or, for government orders, the treasury. Drawn national stores are paid to their owning region.\n- A grid in deficit imports power from a reachable surplus grid instead of a market fallback; downgrading a building no longer refunds anything.\n- The monthly budget lists each resource's economic value, expands to a resource-to-source tree and keeps a cash figure beside it.\n- Regions report what their people went short of over the day; starved research buildings and power-starved works raise a private daily notice.\n- The city panel gains a Resources tab: reserves per resource, their market value and the private sector's daily consumption."
},
{
"hash": "e7ee5db",
"date": "2026-09-22",
"subject": "Added the resource economy, its works and their icons",
"body": "- Five resources (energy, steel, food, luxury, high-tech) with per-city stores, a world market, delivery and material upkeep.\n- Resource-producing tile works built outside cities; every nation opens with the works its economy needs.\n- Real game-icons.net art replacing the dummy icons for the new resources and works, with the credits updated."
}
];
+33 -3
View File
@@ -1023,7 +1023,7 @@ export const panelMethods = {
$el.append(
$("<div class='site-material-label stalled'></div>").text(
"The market is above the budgeted prices. " +
"The budget rises 10% a day until the materials can be bought."
"The budget rises 1% a day, doubling each day, until the materials can be bought."
)
);
} else {
@@ -1094,6 +1094,15 @@ export const panelMethods = {
const building = entry.kind === "building";
const proto = building ? BUILDINGS[entry.protoIndex] : this.protoUnits[entry.protoIndex];
const name = proto ? proto.name : (building ? "Building" : "Unit");
if (building && entry.phase === "materials") {
const $bar = $("<div class='progress city-production-bar'></div>");
$bar.append($("<div></div>").css("width", `${this._gatherFraction(entry) * 100}%`));
$el.append(
$("<div class='city-production-label'></div>").text(`Gathering materials for ${name}`),
$bar
);
return;
}
const verb = building ? "Building" : "Training";
const total = Math.max(entry.totalHours || 0, 0.0001);
const elapsed = entry.elapsedHours || 0;
@@ -1225,7 +1234,7 @@ export const panelMethods = {
for (const improvement of TILE_IMPROVEMENTS) {
if (improvement.coastal && !this._tileIsCoastal(coords)) continue;
const built = !!(standing && standing.id === improvement.id);
const buildHours = ECONOMY.trainingHours(gdp, improvement.buildCost) / (1 + speed);
const buildHours = ECONOMY.buildingHours(gdp, improvement.buildCost) / (1 + speed);
const title = standing && !built
? `${improvement.name} — another improvement already stands here`
: `${improvement.name} — ${this._improvementCostText(improvement, prices)} · ` +
@@ -1297,7 +1306,7 @@ export const panelMethods = {
for (const producer of RESOURCE_BUILDINGS) {
if (producer.coastal && !this._tileIsCoastal(coords)) continue;
const built = !!(standing && standing.id === producer.id);
const buildHours = ECONOMY.trainingHours(gdp, producer.buildCost) / (1 + speed);
const buildHours = ECONOMY.buildingHours(gdp, producer.buildCost) / (1 + speed);
// Some producers are locked behind a technology (the fusion plant).
const techIndex = producer.requiresTechnology
? TECHNOLOGIES.findIndex((tech) => tech.id === producer.requiresTechnology)
@@ -1847,6 +1856,13 @@ export const panelMethods = {
const building = entry.kind === "building";
const proto = building ? BUILDINGS[entry.protoIndex] : this.protoUnits[entry.protoIndex];
const name = proto ? proto.name : (building ? "building" : "unit");
// A building order spends its first days buying its steel and high-tech, so
// show that gathering rather than a construction clock that has not started.
if (building && entry.phase === "materials") {
this._trainingCityId = this.selectedCityId;
this.cityModal.setGatheringProgress(name, this._gatherFraction(entry));
return;
}
const verb = building ? "Building" : "Training";
if (this._trainingCityId !== this.selectedCityId) {
this._trainingCityId = this.selectedCityId;
@@ -1856,6 +1872,20 @@ export const panelMethods = {
}
},
// How far a building order has got through gathering its material bill, as the
// average of each storable material's share bought. 1 when nothing is needed.
_gatherFraction(entry) {
let sum = 0;
let count = 0;
for (const id of ["steel", "hightech"]) {
const need = (entry.needed && entry.needed[id]) || 0;
if (!(need > 0)) continue;
sum += Math.min(1, ((entry.bought && entry.bought[id]) || 0) / need);
count += 1;
}
return count > 0 ? sum / count : 1;
},
// How many levels of each building are already waiting in the city's shared
// production queue, so the buildings tab prices the next order correctly.
_queuedBuildingLevels(cityId) {
+21 -5
View File
@@ -391,10 +391,15 @@ export class CityModal {
for (const index of group.indices) {
const entry = queue[index];
const elapsed = index === 0 ? entry.elapsedHours || 0 : 0;
cumulative += Math.max(entry.totalHours - elapsed, 0);
// A building still gathering its materials has not started building, so
// it adds no time to the queue's readiness estimate.
const gather = entry.kind === "building" && entry.phase === "materials";
cumulative += gather ? 0 : Math.max(entry.totalHours - elapsed, 0);
if (index === group.indices[0]) firstReady = cumulative;
}
const count = group.indices.length;
const gathering = queue[group.indices[0]].kind === "building" &&
queue[group.indices[0]].phase === "materials";
const name = group.kind === "building"
? this.buildingNames.get(group.protoIndex) || "Building"
: this.protoNames.get(group.protoIndex) || "Unit";
@@ -404,9 +409,11 @@ export class CityModal {
$info.append($("<div class='name'></div>").text(`${groupIndex + 1}. ${label}`));
$info.append(
$("<div class='desc'></div>").text(
count > 1
? `Next in ${hours(firstReady)} · all ready in ${hours(cumulative)}`
: `Ready in ${hours(cumulative)}`
gathering
? "Gathering materials"
: count > 1
? `Next in ${hours(firstReady)} · all ready in ${hours(cumulative)}`
: `Ready in ${hours(cumulative)}`
)
);
const $actions = $("<div class='actions'></div>");
@@ -435,6 +442,15 @@ export class CityModal {
this.$trainProgress.css("width", "100%");
}
// The selected city's head of queue is a building still buying its steel and
// high-tech: show how far that gathering has got, with no hours figure since
// the construction clock has not started.
setGatheringProgress(name, fraction = 0) {
this.$trainLabel.text(`Gathering materials for ${name}…`);
this.$trainProgress.css("width", `${Math.min(100, Math.max(0, fraction) * 100)}%`);
this.$trainStatus.removeClass("hidden");
}
// ------------------------------------------------------------- buildings --
_makeBuildingRow(proto, index) {
@@ -495,7 +511,7 @@ export class CityModal {
const parts = [];
if (level > 0) parts.push(`Upkeep ${currencySymbol()}${groupDigits(Math.round(buildingUpkeep(row.proto, level)))}/h`);
const cost = buildingBuildCost(row.proto, nextLevel);
const duration = ECONOMY.trainingHours(gdp, cost) * this._constructionTimeFactor();
const duration = ECONOMY.buildingHours(gdp, cost) * this._constructionTimeFactor();
parts.push(`Build cost ${currencySymbol()}${groupDigits(this._quotedCost(cost, row.proto, nextLevel))} · ${hours(duration)}`);
const requirement = this._buildRequirement(row.proto);
if (requirement) parts.push(requirement);
+10
View File
@@ -86,6 +86,10 @@ export const ECONOMY = {
capitalTileProductionMultiplier: 2.5,
citySurroundingProductionBonus: 0.3,
gdpPerProductionPoint: 100_000,
// Building and tile-improvement construction runs this much faster than
// training: a building takes a tenth of the hours a unit of the same cost
// would. Unit training is untouched.
buildingTimeMultiplier: 0.1,
// A nation opens the game on 1 January 2000 with a tenth of its starting GDP.
startingBudgetFraction: 0.1,
startingBudget: 1_000_000_000,
@@ -106,6 +110,12 @@ export const ECONOMY = {
return cost / capacity;
},
// The hours a building or tile improvement of `cost` takes to rise, ten times
// quicker than training something of the same cost.
buildingHours(gdp, cost) {
return this.trainingHours(gdp, cost) * this.buildingTimeMultiplier;
},
populationGrowthFactor(hoursPerYear, hours = 1) {
if (this.basePopulationGrowthRate <= 0 || hoursPerYear <= 0 || hours <= 0) return 0;
return Math.pow(1 + this.basePopulationGrowthRate, hours / hoursPerYear) - 1;
+20 -2
View File
@@ -923,12 +923,22 @@ export const economyMethods = {
_tickTraining() {
const completed = [];
const day = Math.floor(this.totalHours / HOURS_PER_DAY);
for (const [cityId, queue] of this.training) {
const city = this.findCity(cityId);
if (!city) {
completed.push(cityId);
continue;
}
// A queued building first gathers its materials, once a day, exactly as a
// tile improvement's construction site does; only then does its
// construction time begin.
for (const entry of queue) {
if (entry.kind !== "building" || entry.phase !== "materials") continue;
if (entry.lastGatherDay === day) continue;
entry.lastGatherDay = day;
this._gatherBuildEntry(city, entry);
}
// A city runs one production line per kind plus whatever its Barracks
// (units) and Shipyards (buildings) add. Each line works one entry at full
// capacity, so a higher-level city produces several things at once; the
@@ -970,6 +980,12 @@ export const economyMethods = {
i += 1;
continue;
}
// A building still gathering its materials is not on a construction line
// yet, so it neither works nor blocks the ones that are.
if (entry.phase === "materials") {
i += 1;
continue;
}
const proto = this._queueEntryProto(entry);
if (!proto) {
queue.splice(i, 1);
@@ -1006,9 +1022,11 @@ export const economyMethods = {
return index >= 0 && index < this.protoUnits.length ? this.protoUnits[index] : null;
},
// Raises one level of a completed building. The cost was paid when the order
// was placed, so this only changes the city and invalidates the GDP cache.
// Raises one level of a completed building. The money cost was paid when the
// order was placed and the materials gathered as it ran, so this only returns
// any unspent material budget, changes the city and invalidates the GDP cache.
_completeBuilding(city, entry) {
this._releaseEntryBudget(city.civ, entry);
const level = this.getCityBuildingLevel(city, entry.protoIndex);
city.buildings[entry.protoIndex] = level + 1;
this._gdpPerCapitaCache.clear();
+28 -10
View File
@@ -7,7 +7,7 @@ import { GOVERNMENTS } from "../data/governments.js";
import { PILLAGE, pillageSteps } from "../data/combat.js";
import { transportImprovement } from "../data/improvements.js";
import { buildingBuildCost } from "../rules.js";
import { TRAINING_QUEUE_LIMIT } from "./constants.js";
import { TRAINING_QUEUE_LIMIT, HOURS_PER_DAY } from "./constants.js";
export const orderMethods = {
// Adds one unit to the city's production queue. The queue is shared with
@@ -73,10 +73,12 @@ export const orderMethods = {
return true;
},
// Queues one more level of a building in the city's production queue. Like a
// unit, the level is paid for up front from the budget and only rises once
// its production time has elapsed. Repeated orders stack levels: the second
// queued level is priced off the level the first will leave behind.
// Queues one more level of a building in the city's production queue. The
// level's money cost is paid up front, but its steel and high-tech are
// gathered over the days that follow -- exactly as a tile improvement does --
// and only then does the construction time begin. Repeated orders stack
// levels: the second queued level is priced off the level the first will
// leave behind.
requestBuild(cityId, protoIndex, free = false) {
const city = this.findCity(cityId);
if (!city || protoIndex < 0 || protoIndex >= this.protoBuildings.length) return false;
@@ -113,23 +115,39 @@ export const orderMethods = {
const capacity = ECONOMY.productionCapacity(this.getPlayerGdp(city.civ));
if (capacity <= 0) return false;
if (!this._spendBudget(city.civ, cost, "construction")) return false;
// The level's material bill is not bought up front any more: like a tile
// improvement, the order reserves a budget and gathers the steel and
// high-tech over the days before construction can begin.
const materials = this.constructionResourceCost(proto, level, cost);
if (!this._payConstructionResources(city.civ, city.coords, materials, "construction", proto.name)) {
this._refundBudget(city.civ, cost, "construction");
return false;
}
const needed = {
steel: Math.max(0, materials.steel || 0),
hightech: Math.max(0, materials.hightech || 0),
};
const reserved = needed.steel * this.getResourcePrice("steel") +
needed.hightech * this.getResourcePrice("hightech");
this._reserveSiteBudget(city.civ, reserved, "construction");
const entry = {
kind: "building",
protoIndex,
level,
capacity,
phase: "materials",
budget: reserved,
prices: this._lockedUnitPrices(),
needed,
bought: { steel: 0, hightech: 0 },
elapsedHours: 0,
totalHours: (cost / capacity) *
totalHours: ECONOMY.buildingHours(this.getPlayerGdp(city.civ), cost) *
this.constructionApprovalMultiplier(city) *
this.constructionSpeedMultiplier(city.civ),
lastGatherDay: -1,
};
if (queue) queue.push(entry);
else this.training.set(cityId, [entry]);
// Make the first purchase at once, so a well-supplied city starts building
// right away instead of waiting for the first day boundary.
entry.lastGatherDay = Math.floor(this.totalHours / HOURS_PER_DAY);
this._gatherBuildEntry(city, entry);
this._emitChanged();
return true;
},
+90 -17
View File
@@ -10,11 +10,12 @@
//
// A site stalls when a whole day passes without buying a needed material at the
// locked price (the market moved against it). Rather than waiting for a manual
// top-up, it raises its budgeted prices -- and the money behind them -- by a
// tenth a day until it can buy; only when no supplier is within reach at all
// does it keep a lasting warning. The owner may still cancel the work, losing
// everything already spent. Production buildings that finish become private
// agents and step through the same phases again when they upgrade.
// top-up, it raises its budgeted prices -- and the money behind them -- each
// day, 1% at first and doubling daily, until it can buy; only when no supplier
// is within reach at all does it keep a lasting warning. The owner may still
// cancel the work, losing everything already spent. Production buildings that
// finish become private agents and step through the same phases again when they
// upgrade.
import { key } from "../hex.js";
import { ECONOMY } from "../data/economy.js";
@@ -29,9 +30,10 @@ import { TILE_IMPROVEMENT_HP, HOURS_PER_DAY } from "./constants.js";
// locked price is not bought from: the site waits for the market or raises its
// own budget.
const PRICE_TOLERANCE = 0.01;
// Each day a stalled site cannot buy, its budgeted prices and the matching
// money rise by this factor, so it bids its way up to the market unaided.
const BUDGET_DAILY_GROWTH = 1.1;
// The first day a stalled site cannot buy, its budgeted prices and the matching
// money rise by 1%; the rise doubles each following day (2%, 4%, 8%, ...), so
// it bids its way up to the market unaided.
const BUDGET_DAILY_RISE = 0.01;
const MATERIAL_IDS = ["steel", "hightech"];
const RESOURCE_IDS = ["steel", "hightech", "energy"];
@@ -126,10 +128,8 @@ export const siteMethods = {
: tileImprovementResourceCost(proto, buildCost);
const needed = emptyResources();
for (const id of RESOURCE_IDS) needed[id] = Math.max(0, materials[id] || 0);
const capacity = ECONOMY.productionCapacity(this.getPlayerGdp(civ));
const baseHours = kind === "upgrade"
? (capacity > 0 ? Math.max(1, buildCost / capacity) : 1)
: ECONOMY.trainingHours(this.getPlayerGdp(civ), buildCost);
const gdp = this.getPlayerGdp(civ);
const baseHours = ECONOMY.buildingHours(gdp, buildCost);
const totalHours = Math.max(1, baseHours * this.constructionSpeedMultiplier(civ));
const route = this._siteCityPath(civ, coords);
const site = {
@@ -148,6 +148,8 @@ export const siteMethods = {
cityId: route.cityId,
path: route.path,
stalled: false,
// Consecutive stalled days, driving the doubling budget rise.
budgetRaises: 0,
lastBuyDay: -1,
};
this.constructionSites.set(k, site);
@@ -229,6 +231,7 @@ export const siteMethods = {
const stillMissing = MATERIAL_IDS.some((id) => site.needed[id] - site.bought[id] > 1e-9);
if (!stillMissing) {
site.stalled = false;
site.budgetRaises = 0;
site.phase = "construction";
site.elapsedHours = 0;
this._emitChanged();
@@ -239,17 +242,23 @@ export const siteMethods = {
site.stalled = stalled;
this._emitChanged();
}
if (!stalled) site.budgetRaises = 0;
},
// A day that bought nothing while a supplier is within reach raises the site's
// budgeted prices by a tenth and moves the matching money out of the treasury,
// then tries again. A supplier price above the raised cap simply stalls it
// once more and the budget climbs another tenth tomorrow. When no supplier can
// reach the site at any price the rise cannot help, so it is left alone.
// budgeted prices and moves the matching money out of the treasury, then tries
// again. The first day's rise is 1% and it doubles every day after (2%, 4%,
// 8%, ...), so a stubborn price is met within a few days. A supplier still
// above the raised cap simply stalls it once more; the count resets as soon as
// it buys. When no supplier can reach the site at any price the rise cannot
// help, so it is left alone.
_raiseSiteBudgetIfStalled(site) {
if (!site.stalled || site.phase !== "materials") return;
if (this._siteUnreachableMaterials(site).length > 0) return;
for (const id of RESOURCE_IDS) site.prices[id] *= BUDGET_DAILY_GROWTH;
const raises = site.budgetRaises || 0;
const factor = 1 + BUDGET_DAILY_RISE * 2 ** raises;
for (const id of RESOURCE_IDS) site.prices[id] *= factor;
site.budgetRaises = raises + 1;
const add = this._siteTopUp(site, site.prices);
this._reserveSiteBudget(site.civ, add);
site.budget += add;
@@ -325,6 +334,70 @@ export const siteMethods = {
return payer;
},
// ------------------------------------------------ city building orders --
// One day of a queued city building's material phase. Like a construction
// site it buys whatever of its steel and high-tech bill it still lacks, at the
// prices locked in when the order was placed: the nation's own stores first,
// then the reachable suppliers. It only flips to construction once the whole
// bill is gathered; a day that buys nothing leaves it stalled.
_gatherBuildEntry(city, entry) {
const payer = this._entryPayer(city, entry);
const remaining = { steel: 0, hightech: 0 };
for (const id of MATERIAL_IDS) {
remaining[id] = Math.max(0, (entry.needed[id] || 0) - (entry.bought[id] || 0));
}
// The nation's own cities are bought from first, nearest to the building.
const drawn = this._drawFromNearest(city.civ, city.coords, remaining, payer);
for (const id of MATERIAL_IDS) {
if (drawn[id] > 0) entry.bought[id] = (entry.bought[id] || 0) + drawn[id];
}
for (const id of MATERIAL_IDS) {
const missing = (entry.needed[id] || 0) - (entry.bought[id] || 0);
if (!(missing > 1e-9)) continue;
const temp = emptyResources();
this._procureFromNeighbours(city, id, missing, temp, this._resourceNodes(), {
payer,
allowSea: this._cityMarketAccess(city),
});
if (temp[id] > 0) entry.bought[id] = (entry.bought[id] || 0) + temp[id];
}
const stillMissing = MATERIAL_IDS.some(
(id) => (entry.needed[id] || 0) - (entry.bought[id] || 0) > 1e-9
);
if (!stillMissing) entry.phase = "construction";
entry.stalled = stillMissing;
return !stillMissing;
},
// A payer for a queued building: its reserved material budget first, then the
// public treasury, so an overrun is public money.
_entryPayer(city, entry) {
const payer = { total: 0, overflow: 0 };
payer.charge = (cost) => {
if (!(cost > 0)) return;
const fromBudget = Math.min(entry.budget || 0, cost);
entry.budget = (entry.budget || 0) - fromBudget;
const rest = cost - fromBudget;
if (rest > 0) {
this.budgets.set(city.civ, this.getBudget(city.civ) - rest);
this._recordBudgetCash(city.civ, "construction", -rest);
payer.overflow += rest;
}
payer.total += cost;
};
return payer;
},
// Hands a finished order's unspent material budget back to the treasury. The
// reserved money was never paid to the regions, so it is not clawed back.
_releaseEntryBudget(civ, entry) {
if (!(entry.budget > 0)) return;
this.budgets.set(civ, this.getBudget(civ) + entry.budget);
this._recordBudgetCash(civ, "construction", entry.budget);
entry.budget = 0;
},
// -------------------------------------------------- completion --
_completeConstructionSite(site) {
+37 -4
View File
@@ -37,6 +37,7 @@ function makeSite(state, coords, overrides = {}) {
cityId: null,
path: [],
stalled: false,
budgetRaises: 0,
lastBuyDay: -1,
...overrides,
};
@@ -161,14 +162,46 @@ export class ConstructionSiteTest extends TestCase {
state._raiseSiteBudgetIfStalled(site);
this.assertApprox(
site.prices.steel,
locked * 1.1,
locked * 1.01,
1e-6,
"the budgeted price rises a tenth"
"the first day's budgeted price rises 1%"
);
this.assertGreater(site.budget, 0, "the matching money moved into the site");
this.assertLess(state.getBudget(0), treasury, "and came from the treasury");
}
test_the_budget_rise_doubles_every_day() {
const state = smallState();
clearMaterials(state);
const city = cityOf(state, 0);
const coords = bareOwnedLand(state);
const site = makeSite(state, coords, {
cityId: city.id,
budget: 0,
needed: { steel: 100, hightech: 0, energy: 0 },
});
// A supplier far out of reach in price keeps the site stalling as it climbs.
site.prices.steel = state._marketUnitCost("steel") * 0.001;
for (const other of state.cities) state.getCityResourceStock(other).steel = 1e9;
const nodes = state._resourceNodes();
const suppliers = state._nearbySuppliers(city, "steel", nodes);
this.assertGreater(suppliers.length, 0, "a supplier is reachable");
for (const other of state.cities) state.getCityResourceStock(other).steel = 0;
suppliers[0].stock.steel = 1e9;
state.budgets.set(0, 1e15);
state._gatherSiteMaterials(site);
this.assertTrue(site.stalled, "the locked price is too low to buy at");
// The rises compound: 1%, then 2%, then 4%, ...
let prev = site.prices.steel;
for (let day = 0; day < 4 && site.phase === "materials"; day++) {
state._raiseSiteBudgetIfStalled(site);
const expected = prev * (1 + 0.01 * 2 ** day);
this.assertApprox(site.prices.steel, expected, 1e-6, `day ${day + 1} rise`);
prev = site.prices.steel;
}
}
test_a_stalled_budget_climbs_until_a_supplier_is_affordable() {
const state = smallState();
clearMaterials(state);
@@ -195,9 +228,9 @@ export class ConstructionSiteTest extends TestCase {
state._raiseSiteBudgetIfStalled(site);
this.assertApprox(
site.prices.steel,
locked * 1.1,
locked * 1.01,
1e-6,
"the budgeted price rose a tenth"
"the first day's budgeted price rises 1%"
);
// Keep letting the budget climb; it must eventually reach the supplier.
for (let day = 0; day < 60 && site.phase === "materials"; day++) {
+17
View File
@@ -44,6 +44,23 @@ export class EconomyTest extends TestCase {
this.assertEqual(config({ gdpPerProductionPoint: 0 }).trainingHours(1000, 50), 0);
}
test_building_hours_are_a_tenth_of_training() {
const cfg = config({ gdpPerProductionPoint: 100 });
this.assertApprox(cfg.buildingHours(1000, 50), 0.5);
this.assertApprox(
cfg.buildingHours(1000, 50),
cfg.trainingHours(1000, 50) / 10,
1e-9,
"construction is ten times faster than training the same cost"
);
}
test_building_hours_follow_the_multiplier() {
const cfg = config({ gdpPerProductionPoint: 100, buildingTimeMultiplier: 0.5 });
this.assertApprox(cfg.buildingHours(1000, 50), 2.5);
this.assertEqual(config({ gdpPerProductionPoint: 0 }).buildingHours(1000, 50), 0);
}
test_trade_tax_defaults() {
this.assertApprox(TAXES.defaultSales, 0.2);
this.assertApprox(TAXES.defaultExport, 0.05);
+1 -1
View File
@@ -1746,7 +1746,7 @@ export class GameScreenTest extends TestCase {
.map((_, el) => env.$(el).text())
.get()
.join("|");
this.assertTrue(labels.includes("10%"), `the automatic rise is explained: ${labels}`);
this.assertTrue(labels.includes("doubling"), `the automatic rise is explained: ${labels}`);
this.assertFalse(labels.includes("Increase budget"), `no raise is offered: ${labels}`);
this.assertEqual(
env.$("#tile-works .site-warning").length,
+46 -1
View File
@@ -3,7 +3,7 @@ import { smallState, cityOf, gdpBuilding, grantBuilding } from "./framework/help
import { key } from "../shared/hex.js";
import { EFFECT_RESEARCH } from "../shared/data.js";
import { tileFoodOutput } from "../shared/resources.js";
import { DAYS_PER_YEAR } from "../shared/game_state/constants.js";
import { DAYS_PER_YEAR, HOURS_PER_DAY } from "../shared/game_state/constants.js";
const BIG_BUDGET = 1.0e15;
@@ -182,6 +182,51 @@ export class GameStateBuildingsTest extends TestCase {
this.assertGreater(state.getPlayerGdp(0), beforeGdp, "GDP bonus applied");
}
test_a_building_order_gathers_its_materials_before_building() {
const state = smallState();
state.budgets.set(0, BIG_BUDGET);
const index = this.addBuilding(state, gdpBuilding({ baseCost: 1_000_000_000, costAlpha: 1.0 }));
const city = cityOf(state, 0);
// Drain every own store and cut the network, so nothing can be gathered.
for (const own of state.cities) {
if (own.civ !== 0) continue;
const store = state.getCityResourceStock(own);
store.steel = 0;
store.hightech = 0;
}
state.roads.clear();
state.railways.clear();
this.assertTrue(state.requestBuild(city.id, index));
const queue = state.getTraining(city.id);
this.assertEqual(queue[0].phase, "materials", "the order gathers before it builds");
this.assertEqual(queue[0].elapsedHours, 0, "the construction clock has not started");
// Supply the steel; the next day the order can buy and start building.
state.getCityResourceStock(city).steel = 1.0e9;
for (let i = 0; i < HOURS_PER_DAY; i++) state.advanceHour();
this.assertEqual(queue[0].phase, "construction", "the bill is gathered");
this.advanceUntilIdle(state);
this.assertEqual(state.getCityBuildingLevel(city, index), 1);
}
test_a_stalled_building_order_waits_for_materials() {
const state = smallState();
state.budgets.set(0, BIG_BUDGET);
const index = this.addBuilding(state, gdpBuilding({ baseCost: 1_000_000_000, costAlpha: 1.0 }));
const city = cityOf(state, 0);
// Nothing anywhere to buy: the order can only wait.
for (const own of state.cities) {
const store = state.getCityResourceStock(own);
store.steel = 0;
store.hightech = 0;
}
this.assertTrue(state.requestBuild(city.id, index));
for (let i = 0; i < HOURS_PER_DAY * 3; i++) state.advanceHour();
const entry = state.getTraining(city.id)[0];
this.assertEqual(entry.phase, "materials", "it is still gathering");
this.assertTrue(entry.stalled, "the entry reports being stalled");
this.assertEqual(state.getCityBuildingLevel(city, index), 0, "nothing was built");
}
test_building_and_unit_share_one_queue_and_are_produced_in_order() {
const state = smallState();
state.budgets.set(0, BIG_BUDGET);