From 49613f2a1c2e2b4a320c0f840cd042d0cd63e86e Mon Sep 17 00:00:00 2001 From: Adrien Jaguenet Date: Thu, 24 Sep 2026 21:20:47 +0200 Subject: [PATCH] Made training gather materials, fixed the trade graph, and banked money gifts Unit orders now gather steel and high-tech over the days before training, exactly as buildings do, and clicking a queued order reveals what it is gathering, its reserved budget and the soldiers it will draw. The trade graph cached a producer-less node list under a stamp that already reflected the producers' stores, so every seeded mine stayed invisible to buyers and rare-good prices settled at a huge multiple of base; the storage nodes are now re-read after production. Money gifts draw the giver's central-bank reserves in the denomination first, buy the rest from the issuing bank at the market rate, convert into the receiver's currency, and cannot buy from a bank their nation is at war with. Pruned the finished design briefs (intelligence, stacks, air movement, politics performance, economy balance, training UI and FIXME) and refreshed the index and cross-references. --- AIR_MOVEMENT.md | 197 ----------- DESIGN.md | 10 - ECONOMY_BALANCE.md | 195 ----------- FIXME.md | 3 - ICONS.md | 19 +- INTELLIGENCE.md | 602 -------------------------------- POLITICS_PERFORMANCE.md | 163 --------- SIMPLIFICATIONS.md | 29 +- STACKS.md | 246 ------------- TRAINING_UI.md | 171 --------- client/assets/icon_spy.svg | 6 +- client/css/style.css | 9 + client/js/devlog_data.js | 12 +- client/js/game_screen/panels.js | 11 +- client/js/modals/city.js | 125 ++++++- shared/game_state/economy.js | 11 +- shared/game_state/orders.js | 25 +- shared/game_state/resources.js | 8 +- shared/game_state/sites.js | 2 +- shared/game_state/treaties.js | 53 ++- tests/construction_site_test.js | 51 +++ tests/modals_test.js | 39 ++- tests/treaty_test.js | 70 ++++ tests/warmup_test.js | 17 + 24 files changed, 407 insertions(+), 1667 deletions(-) delete mode 100644 AIR_MOVEMENT.md delete mode 100644 ECONOMY_BALANCE.md delete mode 100644 FIXME.md delete mode 100644 INTELLIGENCE.md delete mode 100644 POLITICS_PERFORMANCE.md delete mode 100644 STACKS.md delete mode 100644 TRAINING_UI.md diff --git a/AIR_MOVEMENT.md b/AIR_MOVEMENT.md deleted file mode 100644 index 4ce8efb..0000000 --- a/AIR_MOVEMENT.md +++ /dev/null @@ -1,197 +0,0 @@ -# Air movement — design brief - -A working document for a session on making **aircraft fly in straight lines** -instead of following the hex grid. - ---- - -## 1. Shared project context (read this first) - -*Battle for 'Tismo* is an HTML5 port of a turn-of-the-millennium strategy game. -Pure JavaScript, no build step, no runtime dependencies. - -- **Server-authoritative.** `shared/game_state.js` (`GameState`) is a - framework-free model; `server/game_server.js` owns one and validates orders; - `server/server.js` drives the clocks (movement every tick, an in-game hour per - `SECONDS_PER_HOUR`). -- **Shared logic runs in Node and the browser.** `shared/` stays framework-free - (no Node, no DOM). -- **GameState is mixins.** `shared/game_state.js` imports each - `shared/game_state/.js` and `Object.assign`s its methods on. A new - method file must be imported **and** added there. -- **Movement is continuous** and the browser **predicts** it: the client - extrapolates each unit along its snapshot path and mirrors `find_path` in - `client/js/map_view/motion.js`. **Any pathfinding or movement-cost change must - be mirrored on both sides**, or prediction fights the server. -- **Roads** (`shared/roads.js`, `GameState.roads`) flatten tiles' movement cost - on both sides via `_tileTravelHours` / `_stepHours`. - -### Commands - -- Install once: `npm install`. -- One suite: `node tests/run_tests.js --file .js`. **Never the full suite** - during development — the commit hook runs it. -- Server: `node server/server.js --port 27015 --bind 127.0.0.1`. - -### Conventions - -- 2-space indent; no new libraries; dummy icon `dummy icon - ` when art is - missing. -- Tests in `tests/_test.js` extending `TestCase`; fixtures in - `tests/framework/helpers.js`. Force RNG with `state._random = () => value`. -- Visual changes get a minimal standalone HTML preview linking - `client/css/style.css`, opened in Firefox, before commit. - ---- - -## 2. How air works today - -Aircraft are ordinary tile-bound units that happen to ignore terrain obstacles: - -- `shared/data/units.js`: `jet_fighter` and `bomber` have `air: true`, - `speed: 5`, `traversableTerrains: ["Land", "Sea", "Ice"]`, `enduranceHours` - (10 / 20) and `missionRange` (80 / 100), plus `requiresBuilding: "airport"`. - `bomber.strike = true`. Missiles (`missile`, `icbm`) are `strike` units too but - not `air`. -- `shared/game_state/air.js`: - - `_isAirProto` / `_isAirUnit`; `_isStrikeUnit` (any aircraft, or `strike`, or - range > 1). - - `_friendlyAirportAt`, `_returnAirport` (nearest friendly airport to the - heading), airport-only rebasing. - - `_pathHours`, `_canStrikeAndReturn` (whole round trip must fit remaining - fuel), `_strikeReady` (parked, fuelled, off cooldown `strikeReadyHour`). - - `requestAirStrike(unitIds, goal)`: a **sortie** — the plane flies to the - target, bombs once on arrival in `advanceMovement`, then returns to a - friendly airport, where landing arms the `AIR_STRIKE.cooldownHours` cooldown. - - Hourly `_tickAir` (or equivalent) burns one hour of endurance per airborne - aircraft, refuels/lands those parked at an airport, crashes those that ran - dry, and sends idle planes home. -- `shared/game_state/movement.js` `_canUnitEnter`: aircraft skip terrain-class - and foreign-territory checks (they fly over fog and borders); only the tile's - existence and terrain class constrain them. Ferry (rebasing) is bounded by - fuel, not `missionRange`. -- Client: `client/js/map_view/motion.js` extrapolates the snapshot path and - predicts local move orders; `map_view/motion.js` already has `_nearestCopy` - for the cylindrical wrap and strike-target line drawing. - -The path is still a list of tile-centre waypoints (from `find_path`), so on the -map a plane visibly zig-zags along hexes, and its fuel/time come from summed -per-tile `_stepHours`. - ---- - -## 3. Requirement (from `ROADMAP.md`) - -> - [ ] Airplanes move in straight lines as they are not bound by tiles. - ---- - -## 4. Design space (settle before coding) - -1. **A straight-line path representation.** The simplest change: produce, for an - air move/sortie, a path of **two world-pixel points** — origin centre to - destination centre — instead of tile waypoints. The continuous mover already - advances along path segments, so a 2-point path is a straight line for free. - Where is the helper? It must be shared so the client prediction draws the - same line: a pure function (e.g. `airPath(from, to, topology)` returning pixel - points, using `topology.pixelDelta` for the shortest wrapped copy) in - `shared/` (hex.js or air.js), called by both `GameState` and `motion.js`. -2. **Distance and time on a straight line.** Fuel is in hours; convert pixel - distance to tiles (`hypot(delta) / maxStepLength`) then to hours at the unit's - speed, matching `_pathHours`'s units. Decide whether air ignores roads and - terrain multipliers entirely (yes — it already effectively does, since it - flies) and whether the speed is tiles/hour directly. -3. **Destination semantics.** A straight line ends at the destination tile - centre. For a move order, the plane ends on that tile (then `_tickAir` sends - it home if it is not an airport — or must a move order only target an - airport? Today rebasing is airport-only; confirm). For a sortie, the plane - arrives at the target tile centre, bombs, turns for the nearest airport. -4. **Interception / air defense.** Combat is tile-based: `AIR_DEFENSE` fires on - aircraft "near" a tile. With a straight line, does air defense trigger when - the segment passes within range of an AA/`aa_building`, or only when the - endpoint is on/near it? This is the biggest open question — it changes both - the model and the client's strike preview. Options: (a) check the tiles the - segment crosses (sample the line at tile centres); (b) treat a strike as - still resolved at the target tile only; (c) add a continuous "flak corridor". -5. **Strike radius and range.** `missionRange` and `range` are tile counts; - keep them, but measure them along the straight line (tile distance between - endpoints) rather than path length. Round-trip fuel uses the straight-line - distance to target plus straight-line distance home. -6. **The cylindrical map.** A straight line must take the shortest wrapped copy - and be drawn across the seam. `topology.pixelDelta` gives the shortest offset; - the renderer needs the same wrapped copy (`_nearestCopy`) so the plane icon - does not jump. A "straight line" across the seam may be a line that leaves one - edge and re-enters the other. -7. **Rendering.** The icon layer positions units along the path; a 2-point path - is trivial, but the strike/bombard arc drawing and the dashed target line may - need to follow the wrapped copy. Check `client/js/map_view/motion.js` and the - entity icon code. -8. **Zone of control / territory.** Irrelevant to aircraft (they already ignore - both). Make sure the new path code does not accidentally reintroduce the - tile-based `_canUnitEnter` checks for air. -9. **Missiles.** `missile`/`icbm` are `strike` but not `air`; decide whether the - straight-line rule applies to them too (they are "fired, not marched", so - likely yes for their strike flight). -10. **Endurance drift.** The hourly fuel burn is one hour per airborne plane - regardless of distance. With straight lines the time and the fuel must agree; - keep `_pathHours` (or its replacement) the single source for both. - ---- - -## 5. Suggested order of work - -1. Add the shared straight-line path helper (with wrap) and unit tests for the - distance/time/wrap maths. -2. Use it for **ferrying** (rebasing) first — the simplest case, no combat. -3. Use it for **sorties** and keep the round-trip fuel check. -4. Decide and implement interception/air-defense semantics (question 4). -5. Mirror in the client prediction and the icon/arc rendering. -6. Tick the roadmap line. - ---- - -## 6. Tests to add - -- A straight-line flight between two tiles has the expected time/fuel, including - a flight across the wrap seam (shortest copy). -- A sortie whose round trip fits the fuel is accepted; one that does not is - refused (server and client prediction agree). -- The plane arrives and bombs at the target's hour (headless `advanceMovement`). -- Client `planMove`/prediction yields the same endpoint hour as the server. -- Air defense still triggers under the chosen rule (question 4). - ---- - -## 7. Gotchas - -- **Mirror server and client.** The plane's position is extrapolated by the - browser; if the client keeps the old tile path the icon will lag or jump. -- **Fuel units.** `_pathHours` returns in-game hours; keep new distance maths in - the same units or planes will run dry mid-flight. -- **Wrap seam.** Any new geometry must use `topology.pixelDelta` / `_nearestCopy`, - not raw `x2 - x1`. -- **Snapshot path.** Paths are shipped to the client; a 2-point pixel path is - smaller, not larger, but confirm the serialiser accepts non-integer points. -- **`_tickAir` idle-return.** Planes that are not at an airport are sent home - each hour; a straight-line move to a non-airport tile may be immediately - reversed. Confirm the intended behaviour. - ---- - -## 8. Relevant files - -- `shared/data/units.js` — aircraft/`strike`/`air`/missile protos. -- `shared/game_state/air.js` — endurance, strikes, airports, cooldown. -- `shared/game_state/movement.js` — `advanceMovement`, `_pathHours`, - `_stepHours`, air exemption in `_canUnitEnter`. -- `shared/hex.js` — `mapToLocal`, `COL_STEP`, `HEX_H`, `MapTopology.pixelDelta`. -- `client/js/map_view/motion.js` — prediction, `_nearestCopy`, strike drawing. -- `server/server.js` — the movement tick. -- `shared/data/combat.js` — `AIR_DEFENSE`, `AIR_STRIKE`. -- Tests: `tests/air_test.js`, `tests/bombing_test.js`, `tests/movement`-related - suites, `tests/map_view_test.js`. - -## 9. Related design docs - -`INTELLIGENCE.md`, `STACKS.md`, `ECONOMY_BALANCE.md`, `POLITICS_PERFORMANCE.md`, -`TRAINING_UI.md`, `ICONS.md` (repo root). diff --git a/DESIGN.md b/DESIGN.md index 82a5068..47d1c4b 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -6,18 +6,8 @@ context on purpose. | Doc | Area | | --- | --- | -| `INTELLIGENCE.md` | Intelligence tab, spy unit, sabotage, city/comms spying, counter-intelligence, spy satellites, the uranium-enrichment warning. | -| `STACKS.md` | Splitting unit stacks ergonomically; stacks pathfinding as one body with allied/at-war territory walkable. | -| `AIR_MOVEMENT.md` | Airplanes moving in straight lines instead of along hex tiles. | -| `ECONOMY_BALANCE.md` | Making luxury and high-tech world prices settle instead of plateauing. | -| `POLITICS_PERFORMANCE.md` | Speeding up the hourly politics/migration tick with version-keyed memoisation. | -| `TRAINING_UI.md` | Showing what a unit being trained is gathering when the player clicks it. | | `ICONS.md` | Replacing the placeholder "dummy icon" SVGs with real art. | | `SIMPLIFICATIONS.md` | The `[~]` roadmap items: deliberate simplifications worth revisiting. | `ROADMAP.md` remains the authoritative task list; these docs add the *why* and the design space around the open items. - -**Done but not yet ticked in `ROADMAP.md`** (as of writing): the ranged-attack -button for a stack (roadmap §5) and the resource-cost line in the budget table -(roadmap §7) are implemented in the working tree. diff --git a/ECONOMY_BALANCE.md b/ECONOMY_BALANCE.md deleted file mode 100644 index fdfef9e..0000000 --- a/ECONOMY_BALANCE.md +++ /dev/null @@ -1,195 +0,0 @@ -# Economy balance — design brief - -A working document for a session on the two commodities whose world-market -prices fail to settle: **luxury** and **high-tech**. - ---- - -## 1. Shared project context (read this first) - -*Battle for 'Tismo* is an HTML5 port of a turn-of-the-millennium strategy game. -Pure JavaScript, no build step, no runtime dependencies. - -- **Server-authoritative.** `shared/game_state.js` (`GameState`) is a - framework-free model; `server/game_server.js` owns one and validates orders. -- **Shared logic runs in Node and the browser.** `shared/` stays framework-free - (no Node, no DOM). -- **GameState is mixins.** `shared/game_state.js` imports each - `shared/game_state/.js` and adds its methods to the prototype. A new - method file must be imported **and** added there. -- **The economy ticks hourly** (`shared/game_state/resources.js` - `_tickResources`); the world can be **settled** without players via - `GameState.warmUp(days)` and `warmUpToStability({ maxDays })`, which the - server calls from `configureGame` (the `--warmup` flag). "Steady" means every - commodity within 10% over the trailing seven days (`pricesAreSteady`). -- **Snapshot performance.** The trade graph is cached across ticks and days - under a stamp built from road/railway, territory and storage-node counters; - `getCityEconomy` must not be memoised outside a snapshot. New per-day walks - should be memoised against the counters that actually change them. -- **Data** lives in `shared/data/` (barrel: `shared/data.js`). - -### Commands - -- Install once: `npm install`. -- One suite: `node tests/run_tests.js --file .js`. **Never the full suite** - during development — the commit hook runs it. -- A quick way to observe prices is the server's startup settle: - `node server/server.js --port 27015 --bind 127.0.0.1` (logs how many days it - settles, `--warmup 0` skips it). There is also `tests/warmup_test.js`. - -### Conventions - -- 2-space indent; no new libraries. -- Tests in `tests/_test.js` extending `TestCase`. Force RNG with - `state._random = () => value`. -- Commit only when asked; messages are one imperative sentence; the pre-commit - hook runs the whole suite. - ---- - -## 2. The problem (from `ROADMAP.md`) - -> - [ ] Luxury and high-tech prices climb to a plateau far above their base (the -> economy simulator's index reaches about 500-650 for luxury and 200-230 -> for high-tech) instead of settling; revisit the supply and demand balance -> of the two rarer goods. - -(Index 100 = the base price.) The two rare goods are in permanent structural -shortage, so the equilibrium price is high and the easing never reaches steady. - ---- - -## 3. How the market works today - -- **Base prices** (`shared/data/resources.js` `RESOURCE_MARKET_BASE`): - energy 0.051 /kWh, steel 10,200 /t, food 5,100 /t, **luxury 1,020 /carat**, - **high-tech 13,600 /unit**. -- **Market model** (`RESOURCE_MARKET`): each day the imbalance sets an - *equilibrium* — the price that clears supply against demand — and the price - eases toward it by `adjustment` (0.2). `maxRatio` (10) bounds how sharp a - shortage may read (and is also the ratio a zero-supply day is treated as). -- **Demand for the rare goods**: - - luxury: `luxuryPerPersonPerDay: 0.01` carat per person per day. - - high-tech: science buildings spend `highTechPerResearchPoint: 1` unit per - research point, advanced buildings/units pay high-tech upkeep - (`upkeepHighTechPerEuro`), and construction of science/advanced buildings - costs `constructionHighTechPerLevel: 100` per level. -- **Supply**: gold/diamond mines (`energyPerLuxuryCarat`), chip foundries - (`energyPerHighTechUnit`). The opening works are generated from the world seed - and are *fixed* per map; nothing in normal play adds supply except a player - raising more works. -- **Energy gates supply**: every unit of luxury/high-tech costs a large amount of - electricity, and food synthesis competes for the same power - (`foodSynthesisQuadratic`). So a tight electricity balance throttles rare-good - supply from above. -- **Food** is the one good a region synthesises from energy, with a reserve-based - price signal (`_tickResources` grows toward - `need + (stockpileTarget - store) - harvest`; `_consumeCityResources` bids the - empty-pantry appetite). Rare goods have no such synthesis. - ---- - -## 4. Why it plateaus (hypotheses to test) - -1. **Demand scales with population/research, supply does not.** Luxury demand - grows with every person; high-tech demand grows with research points and the - advanced-building stock. The seeded mine/foundry supply is a fixed number, so - a mid-game world is structurally short and `maxRatio` caps the price high. -2. **The equilibrium is computed, but the price only eases 20% a day**, so if the - equilibrium itself is far above base the plateau is simply the equilibrium. -3. **No elasticity on demand.** A very high rare-good price should tempt someone - to build more mines/foundries; the AI may not, and the player may not either, - so supply never answers the signal. -4. **Energy competition.** The power needed for more supply may be going to food - synthesis, pinning supply. -5. **Stockpile behaviour.** Cities aim to hold `stockpileDays: 30` of luxury and - high-tech, so a one-off demand spike is absorbed then restocked, keeping - demand elevated. - ---- - -## 5. Design space (settle before coding) - -1. **What is the target?** Should luxury/high-tech settle near their base (index - ~100), or is a moderate premium intended (they *are* rare)? The roadmap says - "settle", implying they should stop climbing; a stable plateau near, say, - 120-200 may be acceptable. Decide the target band first, then tune. -2. **Make the equilibrium self-correcting.** Options: relax `maxRatio` so a - shortage cannot pin the read; make the equilibrium elastic in supply - (higher price → modelled extra production) so the price cannot run away; - or add a slow mean-reversion term toward base when demand is met. -3. **Add synthesis for the rare goods** (like food) so energy can always buy - more at a rising marginal cost — the food model is the template - (`foodSynthesisQuadratic`). -4. **Retune the physical ratios** (`energyPerLuxuryCarat`, - `energyPerHighTechUnit`, per-person luxury, upkeep/high-tech coefficients) so - the world's seeded supply covers its steady-state demand. -5. **Seed more supply** at world generation (more mines/foundries) — a blunt fix, - but it changes the opening economy, so measure with the warm-up. -6. **Cap the demand side**: e.g. research high-tech spend or advanced upkeep - scaled down. -7. **Warm-up as the acceptance test.** After tuning, `warmUpToStability` should - stop well under the cap with luxury/high-tech within the 10%/7-day band. - ---- - -## 6. Suggested order of work - -1. Write a small measurement harness (a test or script) that runs - `warmUpToStability` on the standard map and prints the final price indices for - all five goods and the day count. Use it for every change. -2. Identify whether the equilibrium is the plateau (hypothesis 2) by logging - supply/demand/equilibrium for the two goods over the settle. -3. Pick the mechanism (synthesis, elastic equilibrium, retune, or reseed) and - tune to the target band. -4. Re-run `warmup_test.js` and the economy suites; add a regression test that the - rare-good indices stay within a band after a fixed settle. -5. Tick the roadmap line. - ---- - -## 7. Tests to add / extend - -- A test that settles the standard map and asserts each good's price index is - within a stated band (e.g. luxury and high-tech both < some multiple of base) - and that `pricesAreSteady` holds. -- Unit tests for any new synthesis curve or elastic-equilibrium function - (monotonic, as expected). -- Keep `tests/warmup_test.js`, `economy_test.js`, `trade_graph_test.js`, - `resources_test.js`, `stockpile_test.js`, `food_synthesis_test.js`, - `food_power_test.js` green. - ---- - -## 8. Gotchas - -- **Warm-up cost.** `warmUpToStability` runs real simulation days; a tuning that - makes it run to the cap will slow every server start and the tests. Keep the - settle bounded. -- **The trade graph cache** is keyed on network/territory/storage counters, not - tile improvements; do not fold new counters into its stamp without reason. -- **`getCityEconomy` must not be memoised outside a snapshot**; a test pins that - a simulation read reflects a direct `tilePopulation` edit at once. -- **Prices are global** — a change helps/hurts every nation at once; watch the - affordability of construction/upkeep, which is priced off the same figures. -- **Energy is deliberately cheap** (`base.energy: 0.051`); do not raise it - casually, or every material's energy bill dwarfs its value. - ---- - -## 9. Relevant files - -- `shared/data/resources.js` — base prices, `RESOURCE_MARKET`, `RESOURCE_RULES` - (all the ratios), `PRICE_INDEX_WINDOW_DAYS`, `FAMINE`. -- `shared/game_state/resources.js` — `_tickResources`, `_consumeCityResources`, - supply/demand/equilibrium, food synthesis, `payCombatResources`. -- `shared/game_state/economy.js` — production, stockpiles, construction costs. -- `server/server.js` / `server/game_server.js` — `configureGame` warm-up. -- Tests: `tests/warmup_test.js`, `tests/economy_test.js`, - `tests/resources_test.js`, `tests/stockpile_test.js`, - `tests/trade_graph_test.js`, `tests/food_synthesis_test.js`. - -## 10. Related design docs - -`INTELLIGENCE.md`, `STACKS.md`, `AIR_MOVEMENT.md`, `POLITICS_PERFORMANCE.md`, -`TRAINING_UI.md`, `ICONS.md` (repo root). diff --git a/FIXME.md b/FIXME.md deleted file mode 100644 index 2ae0d16..0000000 --- a/FIXME.md +++ /dev/null @@ -1,3 +0,0 @@ -* In some circumstances, it seems likes aircraft cannot bomb within their own range, as per an error message reported by a user. -* War crime (pillaging) seems to have a permanent effect on popularity that I just cannot get rid off. War crime popularity debuff should be instant and only once per occurence. -* Double airplanes range diff --git a/ICONS.md b/ICONS.md index 6b418c7..2bafdf0 100644 --- a/ICONS.md +++ b/ICONS.md @@ -40,15 +40,10 @@ Pure JavaScript, no build step, no runtime dependencies. ## 2. Requirement (from `ROADMAP.md`) -> - [ ] Real art for the placeholder "dummy icon" SVGs: `icon_radar.svg`, -> `icon_coastal_cannon.svg`, `icon_chip_foundry.svg`, -> `icon_geothermal_plant.svg`, `icon_offshore_wind_turbines.svg`, -> `icon_wind_turbines.svg`. +> Real art for the placeholder "dummy icon" SVGs. -Note: `icon_radar.svg`, `icon_coastal_cannon.svg`, `icon_chip_foundry.svg` and -`icon_wind_turbines.svg` no longer contain the word "dummy" (they appear to have -been replaced already), so the list may be partly stale — **re-derive the live -list** with the command below before starting. +The roadmap names a specific set of files, but most of those have since been +drawn; **re-derive the live list** with the command below before starting. --- @@ -66,9 +61,12 @@ As of this writing that lists (subject to change): tab; used at 20px). - `icon_geothermal_plant.svg`, `icon_offshore_wind_turbines.svg` — power plants. - `icon_icbm.svg`, `icon_missile.svg` — missiles. -- `icon_spy.svg` — the spy unit. - `icon_submarine.svg` — the submarine. - `icon_uranium_enrichment.svg` — the nuclear building. +- `icon_space_launch_center.svg` — the spy-satellite building. + +`icon_spy.svg` and the roadside `icon_radar.svg`, `icon_coastal_cannon.svg`, +`icon_chip_foundry.svg`, `icon_wind_turbines.svg` have already been drawn. The dummy template (all of them share it): @@ -169,5 +167,4 @@ The dummy template (all of them share it): ## 9. Related design docs -`INTELLIGENCE.md`, `STACKS.md`, `AIR_MOVEMENT.md`, `ECONOMY_BALANCE.md`, -`POLITICS_PERFORMANCE.md`, `TRAINING_UI.md` (repo root). +`SIMPLIFICATIONS.md` (repo root). diff --git a/INTELLIGENCE.md b/INTELLIGENCE.md deleted file mode 100644 index 6aaddc3..0000000 --- a/INTELLIGENCE.md +++ /dev/null @@ -1,602 +0,0 @@ -# Intelligence — design brief - -The **Intelligence** feature: its tab, the spy unit, covert actions (sabotage, -city spying, comms spying), counter-intelligence and spy satellites. This -document began as the design brief and now also records what was built. All -phases are implemented; the status list in §2 is the authoritative summary and -§5 records the design decisions each open question settled to. - ---- - -## 1. Shared project context (read this first) - -*Battle for 'Tismo* is an HTML5 port of a turn-of-the-millennium strategy game. -Pure JavaScript, no build step, no runtime dependencies. - -- **Server-authoritative.** `shared/game_state.js` (`GameState`) is a - framework-free model; `server/game_server.js` (`GameServer`) owns one and - validates orders; `server/server.js` is the transport. -- **Shared logic runs in both Node and the browser.** `shared/` must stay - framework-free: no Node built-ins, no DOM. `server/websocket.js` is the only - place Node built-ins are expected. The browser is a view - (`client/js/map_view.js` and friends) that rebuilds terrain from the shared - seed and renders snapshots. -- **GameState is composed of mixins.** `shared/game_state.js` imports each - `shared/game_state/.js` and `Object.assign`s its methods onto the - prototype (world, territory, economy, movement, combat, air, status, - diplomacy, orders, visibility, serialization, politics, treaties, wmd, ...). - A new method file must be imported **and** added to that `Object.assign`. -- **Data-driven content** lives in `shared/data/` (one module per topic: - `units.js`, `buildings.js`, `technologies.js`, `relations.js`, ...). - `shared/data.js` is a barrel — import from it, edit the file under - `shared/data/`. -- **Client mirroring.** Movement, pathfinding and movement-cost changes must be - mirrored between `shared/game_state/movement.js` and the browser's prediction - in `client/js/map_view/motion.js`, or prediction fights the server. -- **Snapshot performance.** `_broadcastState` rebuilds the snapshot whenever the - state is dirty (orders, hourly ticks). Large collections are versioned in - `DELTA_COLLECTIONS` (`server/game_server.js`) — if you add a big snapshot - collection, pair it with a version counter or it is re-sent whole on every - broadcast. Walks are memoised against version counters (`_populationVersion`, - `_territoryVersion`, `_modifiersVersion`, `_tileImprovementVersion`, - `_gdpEpoch`, `_visibleVersion`, ...). - -### Commands - -- Install dev deps once: `npm install` (only `c8` and `jsdom` for tests). -- Run one suite: `node tests/run_tests.js --file .js` (note the `.js`). - **Never run the full suite during development** — the commit hook does that. -- Server + client locally: - `node server/server.js --port 27015 --bind 127.0.0.1`, then open - `http://127.0.0.1:27015/`. Add `--testing` for the free/instant buttons. -- If `node` is missing: `nix-shell -p nodejs --run "node tests/run_tests.js"`. - -### Conventions - -- 2-space indent for `.js`. No new libraries (only vendored jQuery under - `client/vendor/`). -- Missing icon → add a dummy SVG reading `dummy icon - `. -- Tests live in `tests/_test.js`, extend `TestCase` - (`tests/framework/`), and are discovered automatically. Fixtures: - `tests/framework/helpers.js` (`smallState()`, `defaultState()`, - `adjacentLand()`, `seaTile()`, `grantBuilding()`). -- **Visual changes** are reviewed by eye before commit: build a minimal - standalone HTML example, link `client/css/style.css`, copy in the few assets - needed, and **open it in Firefox** (`firefox `). Do not screenshot. -- Commit messages are a single evocative sentence in the imperative. - Only commit when the user asks; the pre-commit hook runs the whole suite. - ---- - -## 2. Where the feature stands - -Status legend: `[x]` done, `[~]` done with a simplification, `[ ]` open. - -- [x] Intelligence tab exists, split into **Spies** and **Propaganda** sub-tabs. - Propaganda owns the campaigns; Spies owns the viewer's spy **roster** (with - a **Locate** control). -- [x] Spy unit core: data, gating, invisibility, 1-tile vision, travel through - foreign territory. -- [x] Spy operation buttons live in the **unit stack panel** (one button per - operation, from `SPY_OPERATIONS`), shown when a spy is selected. -- [x] Sabotage action: a spy on **foreign land** zeroes that tile's production - for a week. The victim is notified; blame falls on the author, a chosen - scapegoat, or no one, and the victim cannot tell which. -- [x] City spying: a spy beside a foreign city files a report on its people, - buildings and garrison. Counter-intelligence can falsify it. -- [x] Comms spying: a spy beside a foreign capital intercepts the last private - messages sent from and to that nation. Counter-intelligence can garble - them and swap the correspondents. -- [x] Counter-intelligence mechanics: the unlock-only technology now shifts the - blame roll and distorts incoming reports and intercepts. -- [x] Spies are **single-use**: a covert operation spends the whole acting - stack, and each spy past the first blunts the target's counter- - intelligence, so a bigger team is the one lever against it. -- [x] Spy satellites + space launch centre: the building gates a launch, everyone - is told, and each satellite uncovers a random 2-tile patch every two days. -- [x] Foreign intelligence may be warned that a nation is enriching uranium - (a chance roll when the first enrichment centre is raised). - -### What is already built - -**Tab.** `client/index.html` has a `` after Politics and a `#tab-intelligence` page with -`#intelligence-sub`, `#intelligence-subtabs` and the two views. `client/js/modals/nation.js` -builds and refreshes it (`_renderIntelligence`, `_buildIntelligence`, -`_refreshIntelligence`), and `NationModal.show/refresh` take a trailing -`intelligence` argument. `client/js/game_screen/panels.js` supplies it via -`_intelligenceInfo()` → `{ civs, localCiv, totalHours, campaigns, opinions }`. - -**Propaganda moved.** The campaign UI (`_fillCampaigns`, `_buildCampaignRow`, -`_buildCampaignForm`, `_syncCampaignReadout`, `_syncCampaignForm`) now lives on -the Intelligence tab's **Propaganda** sub-tab; the DOM state uses -`this.$intelligenceCampaigns` and -`this._intelligenceState.campaign`. Politics keeps populations, migrations, -minority policies and the opinion breakdown. `#campaignSignature` / -`#campaignRows` remain the rebuild guards. - -**Sub-tabs (Phase A).** The Intelligence tab is split into two sub-tabs, -**Spies** and **Propaganda**, mirroring the Economy and Resources tabs -(`#intelligence-subtabs` `.subtabs`, `_buildIntelligenceSubtabs` / -`_setIntelligenceView`, and the two `.modal-body` views -`#intelligence-spies-view` / `#intelligence-propaganda-view` in -`client/index.html`). Spies opens first; the campaigns moved into the -Propaganda view. Switching only toggles `.hidden`, so a snapshot refresh never -tears a view down. - -**Spy controls (Phase A).** The Spies view holds one card, **Your spies** -(`_buildSpiesCard`, `_fillSpies`, `_hasResearched` in -`client/js/modals/nation.js`). The roster rebuilds only when a spy's id, tile or -location label changes (`_spySignature`); it names each spy by position ("Spy 1") -and its whereabouts ("at Paris", "in home territory", "in Britain", "in -unclaimed territory"). A **Locate** button calls `onLocateUnit(id)`, wired to -`_locateUnit` in `client/js/game_screen/panels.js`, which centres the map, -selects the spy and closes the panel. `_intelligenceInfo()` supplies the roster -as `spies`. - -**Covert operations are unit actions, not tab content.** They live in the unit -panel (`#unit-spy-actions`, a row under the pillage/trench/attack buttons), -because an operation is something a *spy* does, tied to the selected stack. -`_updateSpyActions(units)` in `client/js/game_screen/panels.js` shows the row -only when the selection is all spies; `_buildSpyActionButtons()` fills it once -from `SPY_OPERATIONS`. Sabotage enables when the stack stands on a foreign land -tile (`_canSabotage`, mirroring `GameState.canSabotage`); the other two stay -disabled ("Not yet available") until they are wired. The catalogue lives in -`shared/data/intelligence.js` (`SPY_OPERATIONS`, with the id constants -`SPY_SABOTAGE` / `SPY_CITY` / `SPY_COMMS`), re-exported from `shared/data.js`. - -**Sabotage (Phase B).** A new model mixin `shared/game_state/intelligence.js` -owns the covert operations; only `canSabotage` / `requestSabotage` / -`isTileSabotaged` / `_tickSabotage` exist so far. The target is **the tile the -spy stands on**, which must be foreign land (`civAt >= 0`, not the spy's own); -no map target-picking is needed. `requestSabotage` records the tile in -`GameState.tileSabotage` (key → the hour the block ends, `SABOTAGE.blockDays` -= 7) and drops the tile's GDP memo. The block reaches the economy through -`productionFactors`' new `sabotaged` flag: a last `×0` factor that zeroes the -tile's per-head production (and the client's tile readout), mirroring how -pillage and trenches already mark a tile. `_tickSabotage` runs in `tickHour` and -drops lapsed blocks, restoring the tile. The block list ships as -`tileSabotage: [[x, y, untilHour], ...]`, rebuilt client-side into -`GameScreen._tileSabotage`. The victim gets a `NEWS_SABOTAGE` feed line -(`describeNews`: "Saboteurs have crippled production in ") and, if the -viewer is the victim, a notification; `suspect` names the author, a scapegoat, or is null. -The order is `{ type: "sabotage", units, frame? }`, validated in -`GameServer._handleSabotage` and sent by `_sabotageSelected`. The acting spies -are spent (`_spendSpies`): a spy is single-use, so it is removed quietly (no -news, no notification). A larger acting stack lowers the victim's chance of -identifying the author (`_effectiveCounterIntelligence`). - -**Counter-intelligence and blame (Phase C).** `intelligence.js` adds the two -level helpers `_intelligenceLevel` / `_counterIntelligenceLevel` (0 or 1, read -from the researched set via `hasTechnology`) and `_blameCulprit`. The blame roll -starts at `ESPIONAGE.baseIdentifyChance`, rises with the victim's -counter-intelligence and falls with the author's agencies; a `frame` scapegoat -(another nation, chosen in the unit panel's **Blame** picker) sticks with -`ESPIONAGE.frameChance` when the author is not identified. A correct blame and a -successful false flag are the same `suspect` field, so the victim cannot tell -them apart. - -**City and comms spying (Phases B/D).** `canCitySpy`/`requestCitySpy` and -`canCommsSpy`/`requestCommsSpy` share `_spyStackAtOneTile` and `_spyWatches`: a -spy can never enter a hostile city, so it watches from the city's own tile or a -neighbour. Reports are stored per nation in `GameState.spyReports` (capped at -`ESPIONAGE.reportLimit`) and shipped only to their owner in `viewerSnapshot` as -`spyReports`; the chat log they are drawn from never ships. A city report lists -the population, building levels and garrison; with the target's -counter-intelligence, values are jittered or nudged (`_jitterCount`). A comms -report lists the last `ESPIONAGE.interceptCount` messages involving the target -(`GameState.messages`, fed by `recordChat`); counter-intelligence garbles the -text and swaps sender/receiver (`_garbleMessage`/`_scramble`). Both appear in -the Intelligence tab's **Intelligence reports** card. Both spend the acting -spies (a spy is single-use) and a larger stack blunts the target's -counter-intelligence when it decides whether to falsify or garble the report. -The target picker reuses -the ranged **Attack** overlay: `_toggleSpyOperation` arms it with -`operation: "spy_city"/"spy_comms"` and `_chooseTarget` dispatches on it. - -**Comms store.** `server/server.js` `case "chat"` now takes an optional `to` -nation, telling one peer (the public channel when absent), and records every -line through `GameState.recordChat`. The client's chat gained a recipient -`