Guaranteed the 45-second starter build even on an unpowered map
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
// Generated by scripts/generate-devlog.js from `git log`; do not edit.
|
// 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.
|
// The pre-commit hook refreshes it so the main menu shows the latest commits.
|
||||||
export const DEVLOG = [
|
export const DEVLOG = [
|
||||||
|
{
|
||||||
|
"hash": "80d2e22",
|
||||||
|
"date": "2026-09-25",
|
||||||
|
"subject": "Sped up simple construction and showed resource costs on build cards",
|
||||||
|
"body": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hash": "4e11af2",
|
"hash": "4e11af2",
|
||||||
"date": "2026-09-25",
|
"date": "2026-09-25",
|
||||||
@@ -54,11 +60,5 @@ export const DEVLOG = [
|
|||||||
"date": "2026-09-24",
|
"date": "2026-09-24",
|
||||||
"subject": "Cut the per-broadcast and per-frame cost on a full-sized map",
|
"subject": "Cut the per-broadcast and per-frame cost on a full-sized map",
|
||||||
"body": "The server sent the whole army on every snapshot and the client re-cloned\nthe whole icon layer whenever a unit moved, so a settled world with a\nstanding army drained the broadcast loop and the frame loop alike.\n\n- Ship only the units and city list that changed, keyed by a per-peer\n fingerprint and a serialised hash, and merge those deltas on the client.\n- Memoise the battlefield scan on a battle version bumped when a unit\n joins, leaves or changes side.\n- Skip the DOM writes for units whose snapshot object is unchanged, and\n cache the unit-view inner nodes.\n- Fold units, cities and tile markers onto the nearest world copy instead\n of mirroring the icon layers, which re-cloned thousands of views every\n frame; the small decoration layers still mirror, and only on a change.\n\nFrame work at full zoom-out with units moving fell from ~190ms to ~6.5ms."
|
"body": "The server sent the whole army on every snapshot and the client re-cloned\nthe whole icon layer whenever a unit moved, so a settled world with a\nstanding army drained the broadcast loop and the frame loop alike.\n\n- Ship only the units and city list that changed, keyed by a per-peer\n fingerprint and a serialised hash, and merge those deltas on the client.\n- Memoise the battlefield scan on a battle version bumped when a unit\n joins, leaves or changes side.\n- Skip the DOM writes for units whose snapshot object is unchanged, and\n cache the unit-view inner nodes.\n- Fold units, cities and tile markers onto the nearest world copy instead\n of mirroring the icon layers, which re-cloned thousands of views every\n frame; the small decoration layers still mirror, and only on a change.\n\nFrame work at full zoom-out with units moving fell from ~190ms to ~6.5ms."
|
||||||
},
|
|
||||||
{
|
|
||||||
"hash": "822c26f",
|
|
||||||
"date": "2026-09-23",
|
|
||||||
"subject": "Founded every nation with a standing army and a year of reserves",
|
|
||||||
"body": ""
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ Every region must start with one power plant.
|
|||||||
(`constructionSpeedMultiplier`, `EFFECT_CONSTRUCTION_SPEED`).
|
(`constructionSpeedMultiplier`, `EFFECT_CONSTRUCTION_SPEED`).
|
||||||
- Put the tuning constants (`energyPerConstructionHour`, a minimum speed floor,
|
- Put the tuning constants (`energyPerConstructionHour`, a minimum speed floor,
|
||||||
and how energy maps to hours) in the simple-economy data module.
|
and how energy maps to hours) in the simple-economy data module.
|
||||||
- Define what happens when a region has no power: the work should be slow or
|
- Define what happens when a region has no power: a guaranteed baseline
|
||||||
stalled, not free. A floor avoids a divide-by-zero and a hard lock.
|
construction power stands in for the grid, so the work is never free, never
|
||||||
|
instant and never slower than the reference target (modern infantry in 45
|
||||||
|
game hours). Spare power above the baseline only makes a work quicker, and
|
||||||
|
the floor avoids a divide-by-zero and a hard lock.
|
||||||
- **Every region starts with one power plant.** Seed one working plant per city
|
- **Every region starts with one power plant.** Seed one working plant per city
|
||||||
region in the simple scenario so regions open with energy and the speed rule
|
region in the simple scenario so regions open with energy and the speed rule
|
||||||
has a non-zero basis.
|
has a non-zero basis.
|
||||||
@@ -79,9 +82,10 @@ Every region must start with one power plant.
|
|||||||
|
|
||||||
- In simple mode, ordering a building raises it on the hourly tick with no
|
- In simple mode, ordering a building raises it on the hourly tick with no
|
||||||
`"materials"` phase and no `_nearbySuppliers`/trade-graph lookups.
|
`"materials"` phase and no `_nearbySuppliers`/trade-graph lookups.
|
||||||
- Construction time scales with the region's available energy, and a
|
- Construction time scales with the region's available energy: spare power
|
||||||
power-less region builds clearly slower than a powered one (never faster, and
|
above the guaranteed baseline shortens the clock, while a region at or below
|
||||||
never instantaneous at zero energy).
|
the baseline (including a powerless one) is held at the reference time -- never
|
||||||
|
slower, never instantaneous.
|
||||||
- Every starting region has at least one working power plant in simple mode.
|
- Every starting region has at least one working power plant in simple mode.
|
||||||
- Hard economy construction is untouched.
|
- Hard economy construction is untouched.
|
||||||
|
|
||||||
|
|||||||
@@ -51,14 +51,15 @@ export const SIMPLE_ECONOMY = {
|
|||||||
// max(available, minEnergy)`, then the building/training ratio below and the
|
// max(available, minEnergy)`, then the building/training ratio below and the
|
||||||
// usual approval and technology multipliers. A tighter grid raises the same
|
// usual approval and technology multipliers. A tighter grid raises the same
|
||||||
// work slower, the opposite of the hard economy's richer-nation-builds-faster.
|
// work slower, the opposite of the hard economy's richer-nation-builds-faster.
|
||||||
// Tuned so a modern-infantry unit (cost 100M) trains in 45 in-game hours --
|
|
||||||
// 45 real seconds, one hour a second -- in a starter region holding the
|
|
||||||
// typical ~3.5e9 kWh a day of spare power.
|
|
||||||
energyPerConstructionHour: 1_600,
|
energyPerConstructionHour: 1_600,
|
||||||
// The least spare power a region is credited with, so a blacked-out region
|
// The construction power every region is credited with even when its grid
|
||||||
// still builds -- slowly -- instead of dividing by zero or finishing at
|
// has nothing to spare. This is the guarantee behind the opening pace: a
|
||||||
// once. A tenth of a solar plant's daily output.
|
// modern-infantry unit (cost 100M) always trains in 45 game hours -- 45 real
|
||||||
minEnergy: 30_000_000,
|
// seconds, one hour a second -- in a starter city on any map, however
|
||||||
|
// starved its grid. A region with more spare power builds the same work
|
||||||
|
// faster; none builds it slower. Sized as `100M * energyPerConstructionHour
|
||||||
|
// / 45`, so the reference work lands exactly on 45 hours at the floor.
|
||||||
|
minEnergy: (100_000_000 * 1_600) / 45,
|
||||||
// The hard economy builds ten times faster than it trains a unit of the same
|
// The hard economy builds ten times faster than it trains a unit of the same
|
||||||
// cost; the simple economy keeps that ratio.
|
// cost; the simple economy keeps that ratio.
|
||||||
buildingTimeMultiplier: 0.1,
|
buildingTimeMultiplier: 0.1,
|
||||||
@@ -75,9 +76,11 @@ export const SIMPLE_ECONOMY = {
|
|||||||
// The base hours a simple-economy work of `cost` takes at `energy` kWh a day of
|
// The base hours a simple-economy work of `cost` takes at `energy` kWh a day of
|
||||||
// spare regional power. `kind` is "unit" for training, anything else for a
|
// spare regional power. `kind` is "unit" for training, anything else for a
|
||||||
// building, tile work or upgrade, which the figures in `construction` run
|
// building, tile work or upgrade, which the figures in `construction` run
|
||||||
// faster. Energy is floored so a blacked-out region still builds -- slowly --
|
// faster. Energy is floored at the region's guaranteed construction power, so a
|
||||||
// rather than dividing by zero or finishing at once. Shared by the server's
|
// blacked-out starter region still raises the reference unit in 45 hours and
|
||||||
// clock and the browser's quoted times, so the two can never disagree.
|
// divides by no zero; more power than the floor only makes a work quicker.
|
||||||
|
// Shared by the server's clock and the browser's quoted times, so the two can
|
||||||
|
// never disagree.
|
||||||
export function simpleConstructionHours(cost, energy, kind) {
|
export function simpleConstructionHours(cost, energy, kind) {
|
||||||
const available = Math.max(
|
const available = Math.max(
|
||||||
Number.isFinite(energy) ? energy : 0,
|
Number.isFinite(energy) ? energy : 0,
|
||||||
|
|||||||
@@ -25,8 +25,11 @@
|
|||||||
// is no materials phase, no energy-buy phase and no trade-graph lookup. The
|
// is no materials phase, no energy-buy phase and no trade-graph lookup. The
|
||||||
// construction or training clock is set from the spare power of the region the
|
// construction or training clock is set from the spare power of the region the
|
||||||
// work stands in: `regionAvailableEnergy` is the region's plant output less what
|
// work stands in: `regionAvailableEnergy` is the region's plant output less what
|
||||||
// its cities, buildings and converters already draw, floored so a blacked-out
|
// its cities, buildings and converters already draw. A guaranteed baseline
|
||||||
// region still builds slowly instead of dividing by zero. Money is not spent
|
// covers a region whose grid has nothing to spare, so a starter city on any map
|
||||||
|
// raises a modern-infantry unit in the 45-hour reference time and no work ever
|
||||||
|
// divides by zero; spare power above the baseline only makes a work quicker.
|
||||||
|
// Money is not spent
|
||||||
// internally on construction (the single tax and the market cover that in
|
// internally on construction (the single tax and the market cover that in
|
||||||
// `05-`/`06-`), so the treasury gates nothing. Every opening region is guaranteed
|
// `05-`/`06-`), so the treasury gates nothing. Every opening region is guaranteed
|
||||||
// at least one plant -- a fuel-free one where the hard seeding left it without --
|
// at least one plant -- a fuel-free one where the hard seeding left it without --
|
||||||
@@ -811,9 +814,10 @@ export const simpleEconomyMethods = {
|
|||||||
|
|
||||||
// The base hours a work of `cost` takes in `city`'s region at today's spare
|
// The base hours a work of `cost` takes in `city`'s region at today's spare
|
||||||
// power. `kind` is "unit" for training, anything else for a building, tile
|
// power. `kind` is "unit" for training, anything else for a building, tile
|
||||||
// work or upgrade -- which the hard economy runs ten times quicker. The energy
|
// work or upgrade -- which the hard economy runs ten times quicker. The
|
||||||
// is floored so a blacked-out region still builds, slowly. The arithmetic
|
// arithmetic lives in the data module, where the guaranteed baseline keeps
|
||||||
// lives in the data module so the browser quotes the same figure.
|
// even a blacked-out starter region at the reference time, and so the browser
|
||||||
|
// quotes the same figure.
|
||||||
_simpleConstructionHours(city, cost, kind) {
|
_simpleConstructionHours(city, cost, kind) {
|
||||||
return simpleConstructionHours(cost, this.regionAvailableEnergy(city), kind);
|
return simpleConstructionHours(cost, this.regionAvailableEnergy(city), kind);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -134,46 +134,54 @@ export class SimpleConstructionTest extends TestCase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The reference calibration: a modern-infantry unit (cost 100M) trains in
|
// The reference calibration: a modern-infantry unit (cost 100M) trains in 45
|
||||||
// about 45 in-game hours -- 45 real seconds -- in a starter region with the
|
// in-game hours -- 45 real seconds -- in a starter region on any map, however
|
||||||
// typical spare power. The rate lives in the shared data module so the server
|
// little spare power that region has. The rate and the guaranteed baseline
|
||||||
// clock and the browser's quoted times derive it the same way.
|
// live in the shared data module so the server clock and the browser's quoted
|
||||||
test_the_reference_unit_trains_in_45_hours_in_a_starter_region() {
|
// times derive them the same way.
|
||||||
const energy = 3_500_000_000;
|
test_the_reference_unit_always_trains_within_45_hours() {
|
||||||
const hours = simpleConstructionHours(100_000_000, energy, "unit");
|
const cost = 100_000_000;
|
||||||
this.assertApprox(hours, 45, 1, "modern infantry is about 45 hours at starter power");
|
const typical = simpleConstructionHours(cost, 3_500_000_000, "unit");
|
||||||
|
this.assertApprox(typical, 45, 1e-6, "a typical starter region lands on 45 hours");
|
||||||
this.assertApprox(
|
this.assertApprox(
|
||||||
simpleConstructionHours(100_000_000, energy, "building") * 10,
|
simpleConstructionHours(cost, 3_500_000_000, "building") * 10,
|
||||||
hours,
|
typical,
|
||||||
0.5,
|
1e-6,
|
||||||
"a building of the same cost runs ten times quicker"
|
"a building of the same cost runs ten times quicker"
|
||||||
);
|
);
|
||||||
const dead = simpleConstructionHours(100_000_000, 0, "unit");
|
// The guarantee: no level of spare power makes the reference work slower
|
||||||
this.assertGreater(dead, hours, "a blacked-out region builds slower");
|
// than 45 hours, and none makes it instant.
|
||||||
this.assertGreaterOrEqual(dead, SIMPLE_ECONOMY.construction.minHours, "but not instantly");
|
const dead = simpleConstructionHours(cost, 0, "unit");
|
||||||
|
this.assertApprox(dead, 45, 1e-6, "a blacked-out region still meets the target");
|
||||||
|
this.assertApprox(simpleConstructionHours(cost, 1, "unit"), 45, 1e-6, "so does a trickle");
|
||||||
|
const huge = simpleConstructionHours(cost, 64_000_000_000, "unit");
|
||||||
|
this.assertLess(huge, 45, "a rich grid only builds faster");
|
||||||
|
this.assertGreaterOrEqual(dead, SIMPLE_ECONOMY.construction.minHours, "and never instantly");
|
||||||
}
|
}
|
||||||
|
|
||||||
test_more_regional_energy_strictly_shortens_the_clock() {
|
test_more_regional_energy_strictly_shortens_the_clock() {
|
||||||
const state = simpleState();
|
const state = simpleState();
|
||||||
const city = cityOf(state, 0);
|
const city = cityOf(state, 0);
|
||||||
const cost = 100_000_000;
|
const cost = 100_000_000;
|
||||||
state.regionAvailableEnergy = () => 100_000_000;
|
const floor = SIMPLE_ECONOMY.construction.minEnergy;
|
||||||
|
state.regionAvailableEnergy = () => floor;
|
||||||
const low = state._simpleConstructionHours(city, cost, "building");
|
const low = state._simpleConstructionHours(city, cost, "building");
|
||||||
state.regionAvailableEnergy = () => 1_000_000_000;
|
state.regionAvailableEnergy = () => floor * 4;
|
||||||
const high = state._simpleConstructionHours(city, cost, "building");
|
const high = state._simpleConstructionHours(city, cost, "building");
|
||||||
this.assertLess(high, low, "more spare power builds faster");
|
this.assertLess(high, low, "more spare power builds faster");
|
||||||
this.assertGreater(high, 0, "and never instant");
|
this.assertGreater(high, 0, "and never instant");
|
||||||
}
|
}
|
||||||
|
|
||||||
test_a_blacked_out_region_builds_slowly_but_not_at_once() {
|
test_a_blacked_out_region_is_capped_at_the_baseline() {
|
||||||
const state = simpleState();
|
const state = simpleState();
|
||||||
const city = cityOf(state, 0);
|
const city = cityOf(state, 0);
|
||||||
const cost = 100_000_000;
|
const cost = 100_000_000;
|
||||||
|
const floor = SIMPLE_ECONOMY.construction.minEnergy;
|
||||||
state.regionAvailableEnergy = () => 0;
|
state.regionAvailableEnergy = () => 0;
|
||||||
const dead = state._simpleConstructionHours(city, cost, "building");
|
const dead = state._simpleConstructionHours(city, cost, "building");
|
||||||
state.regionAvailableEnergy = () => 100_000_000;
|
state.regionAvailableEnergy = () => floor * 4;
|
||||||
const lit = state._simpleConstructionHours(city, cost, "building");
|
const lit = state._simpleConstructionHours(city, cost, "building");
|
||||||
this.assertGreater(dead, lit, "a power-less region builds clearly slower");
|
this.assertGreater(dead, lit, "a power-rich region builds clearly faster");
|
||||||
this.assertTrue(Number.isFinite(dead), "the clock is finite, not a divide-by-zero");
|
this.assertTrue(Number.isFinite(dead), "the clock is finite, not a divide-by-zero");
|
||||||
this.assertGreaterOrEqual(dead, SIMPLE_ECONOMY.construction.minHours, "the floor holds");
|
this.assertGreaterOrEqual(dead, SIMPLE_ECONOMY.construction.minHours, "the floor holds");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user