diff --git a/client/js/devlog_data.js b/client/js/devlog_data.js index 662c8aa..087b370 100644 --- a/client/js/devlog_data.js +++ b/client/js/devlog_data.js @@ -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": "80d2e22", + "date": "2026-09-25", + "subject": "Sped up simple construction and showed resource costs on build cards", + "body": "" + }, { "hash": "4e11af2", "date": "2026-09-25", @@ -54,11 +60,5 @@ export const DEVLOG = [ "date": "2026-09-24", "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." - }, - { - "hash": "822c26f", - "date": "2026-09-23", - "subject": "Founded every nation with a standing army and a year of reserves", - "body": "" } ]; diff --git a/docs/simple_economy/03-construction.md b/docs/simple_economy/03-construction.md index c61ab92..0b614d6 100644 --- a/docs/simple_economy/03-construction.md +++ b/docs/simple_economy/03-construction.md @@ -42,8 +42,11 @@ Every region must start with one power plant. (`constructionSpeedMultiplier`, `EFFECT_CONSTRUCTION_SPEED`). - Put the tuning constants (`energyPerConstructionHour`, a minimum speed floor, 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 - stalled, not free. A floor avoids a divide-by-zero and a hard lock. + - Define what happens when a region has no power: a guaranteed baseline + 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 region in the simple scenario so regions open with energy and the speed rule 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 `"materials"` phase and no `_nearbySuppliers`/trade-graph lookups. -- Construction time scales with the region's available energy, and a - power-less region builds clearly slower than a powered one (never faster, and - never instantaneous at zero energy). +- Construction time scales with the region's available energy: spare power + above the guaranteed baseline shortens the clock, while a region at or below + 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. - Hard economy construction is untouched. diff --git a/shared/data/simple_economy.js b/shared/data/simple_economy.js index 450648c..5597134 100644 --- a/shared/data/simple_economy.js +++ b/shared/data/simple_economy.js @@ -51,14 +51,15 @@ export const SIMPLE_ECONOMY = { // max(available, minEnergy)`, then the building/training ratio below and the // usual approval and technology multipliers. A tighter grid raises the same // 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, - // The least spare power a region is credited with, so a blacked-out region - // still builds -- slowly -- instead of dividing by zero or finishing at - // once. A tenth of a solar plant's daily output. - minEnergy: 30_000_000, + // The construction power every region is credited with even when its grid + // has nothing to spare. This is the guarantee behind the opening pace: a + // modern-infantry unit (cost 100M) always trains in 45 game hours -- 45 real + // 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 // cost; the simple economy keeps that ratio. 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 // spare regional power. `kind` is "unit" for training, anything else for a // building, tile work or upgrade, which the figures in `construction` run -// faster. Energy is floored so a blacked-out region still builds -- slowly -- -// rather than dividing by zero or finishing at once. Shared by the server's -// clock and the browser's quoted times, so the two can never disagree. +// faster. Energy is floored at the region's guaranteed construction power, so a +// blacked-out starter region still raises the reference unit in 45 hours and +// 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) { const available = Math.max( Number.isFinite(energy) ? energy : 0, diff --git a/shared/game_state/simple_economy.js b/shared/game_state/simple_economy.js index d2b8051..a229894 100644 --- a/shared/game_state/simple_economy.js +++ b/shared/game_state/simple_economy.js @@ -25,8 +25,11 @@ // 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 // work stands in: `regionAvailableEnergy` is the region's plant output less what -// its cities, buildings and converters already draw, floored so a blacked-out -// region still builds slowly instead of dividing by zero. Money is not spent +// its cities, buildings and converters already draw. A guaranteed baseline +// 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 // `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 -- @@ -811,9 +814,10 @@ export const simpleEconomyMethods = { // 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 - // work or upgrade -- which the hard economy runs ten times quicker. The energy - // is floored so a blacked-out region still builds, slowly. The arithmetic - // lives in the data module so the browser quotes the same figure. + // work or upgrade -- which the hard economy runs ten times quicker. The + // arithmetic lives in the data module, where the guaranteed baseline keeps + // even a blacked-out starter region at the reference time, and so the browser + // quotes the same figure. _simpleConstructionHours(city, cost, kind) { return simpleConstructionHours(cost, this.regionAvailableEnergy(city), kind); }, diff --git a/tests/simple_construction_test.js b/tests/simple_construction_test.js index 6c3e1eb..74719d8 100644 --- a/tests/simple_construction_test.js +++ b/tests/simple_construction_test.js @@ -134,46 +134,54 @@ export class SimpleConstructionTest extends TestCase { ); } - // The reference calibration: a modern-infantry unit (cost 100M) trains in - // about 45 in-game hours -- 45 real seconds -- in a starter region with the - // typical spare power. The rate lives in the shared data module so the server - // clock and the browser's quoted times derive it the same way. - test_the_reference_unit_trains_in_45_hours_in_a_starter_region() { - const energy = 3_500_000_000; - const hours = simpleConstructionHours(100_000_000, energy, "unit"); - this.assertApprox(hours, 45, 1, "modern infantry is about 45 hours at starter power"); + // The reference calibration: a modern-infantry unit (cost 100M) trains in 45 + // in-game hours -- 45 real seconds -- in a starter region on any map, however + // little spare power that region has. The rate and the guaranteed baseline + // live in the shared data module so the server clock and the browser's quoted + // times derive them the same way. + test_the_reference_unit_always_trains_within_45_hours() { + const cost = 100_000_000; + 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( - simpleConstructionHours(100_000_000, energy, "building") * 10, - hours, - 0.5, + simpleConstructionHours(cost, 3_500_000_000, "building") * 10, + typical, + 1e-6, "a building of the same cost runs ten times quicker" ); - const dead = simpleConstructionHours(100_000_000, 0, "unit"); - this.assertGreater(dead, hours, "a blacked-out region builds slower"); - this.assertGreaterOrEqual(dead, SIMPLE_ECONOMY.construction.minHours, "but not instantly"); + // The guarantee: no level of spare power makes the reference work slower + // than 45 hours, and none makes it instant. + 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() { const state = simpleState(); const city = cityOf(state, 0); 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"); - state.regionAvailableEnergy = () => 1_000_000_000; + state.regionAvailableEnergy = () => floor * 4; const high = state._simpleConstructionHours(city, cost, "building"); this.assertLess(high, low, "more spare power builds faster"); 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 city = cityOf(state, 0); const cost = 100_000_000; + const floor = SIMPLE_ECONOMY.construction.minEnergy; state.regionAvailableEnergy = () => 0; const dead = state._simpleConstructionHours(city, cost, "building"); - state.regionAvailableEnergy = () => 100_000_000; + state.regionAvailableEnergy = () => floor * 4; 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.assertGreaterOrEqual(dead, SIMPLE_ECONOMY.construction.minHours, "the floor holds"); }