From 1a9d48cdc9f3306294137cce8f199d251239dd20 Mon Sep 17 00:00:00 2001 From: Adrien Jaguenet Date: Fri, 18 Sep 2026 21:55:30 +0200 Subject: [PATCH] Added aircraft sorties, artillery bombardment and local testing mode - aircraft attack orders fly a sortie to the target, bomb once, return to a friendly airport and re-arm (two-day cooldown, charged per aircraft); rebasing is bounded by fuel rather than the 10-tile combat radius - artillery and warships open a sustained hourly bombardment on a standing target, drawn as a red dashed line and ring while either endpoint is selected - a game launched from localhost can enable a testing mode whose train and build rows offer free, instant orders; the server only honours them for a loopback admin in a testing game - an open city panel now unlocks aircraft as soon as an airport finishes --- AGENTS.md | 46 +++++++- client/css/style.css | 30 +++++ client/index.html | 8 ++ client/js/app.js | 12 +- client/js/game_screen.js | 43 +++++-- client/js/game_screen/panels.js | 123 ++++++++++++++++---- client/js/map_view.js | 5 + client/js/map_view/motion.js | 55 +++++++++ client/js/modals/city.js | 41 ++++++- server/game_server.js | 27 +++-- server/server.js | 26 ++++- server/websocket.js | 3 + shared/data/combat.js | 17 +++ shared/data/units.js | 9 +- shared/game_state.js | 4 + shared/game_state/air.js | 152 +++++++++++++++++++------ shared/game_state/combat.js | 11 +- shared/game_state/movement.js | 29 ++++- shared/game_state/orders.js | 21 +++- shared/game_state/serialization.js | 9 ++ shared/game_state/world.js | 5 + tests/air_test.js | 175 ++++++++++++++++++++++------- tests/app_test.js | 24 ++++ tests/framework/helpers.js | 1 + tests/game_screen_test.js | 165 +++++++++++++++++++++++++++ tests/game_server_test.js | 2 + tests/map_view_test.js | 31 ++++- tests/testing_mode_test.js | 107 ++++++++++++++++++ 28 files changed, 1048 insertions(+), 133 deletions(-) create mode 100644 tests/testing_mode_test.js diff --git a/AGENTS.md b/AGENTS.md index 1fce6fe..5fef790 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,8 +9,10 @@ everything else (transport included) is hand-written. - Install dev dependencies once: `npm install` (only `c8` and `jsdom`, used by the test suite — nothing the game runs needs them). -- Run the test suite: `node tests/run_tests.js` (alias `npm test`; prints a - summary and exits non-zero on failure). +- Run the test suite: `node tests/run_tests.js` (alias `npm test`; discovers + every `tests/*_test.js` and runs each in its own worker process, prints a + summary and exits non-zero on failure). `--jobs N` caps the pool and + `--file ` runs a single suite in-process; the worker marker is internal. - Coverage report: `npm run coverage` (c8, writes `coverage/`). - Start the game server and client locally: `node server/server.js --port 27015 --bind 127.0.0.1`. Then open @@ -24,6 +26,19 @@ everything else (transport included) is hand-written. There is no linter, formatter, or CI. Do not invent commands beyond these. +### Local testing mode + +- Opening the client from localhost (`http://127.0.0.1:27015/`) adds a + **Testing mode** checkbox to the New game screen. When the admin starts such a + game, every city's train and build rows gain a second "Free & instant" button. +- This is gated on the server, never trusted from the client: `server/server.js` + only sets `setup.testing` when the admin's WebSocket peer is loopback + (`isLoopback`, handling `127.x`, `::1` and `::ffff:127.0.0.1`), strips a `free` + flag from any non-loopback order, and `GameServer` honours it only while + `this.testing` is set. `GameState.requestTrain/requestBuild(..., free)` then + spawn or raise immediately without cost or queue (placement rules still apply). +- The `testing` flag is shipped in the snapshot so joiners see the same buttons. + ## Gotchas - Keep the shared modules framework-free: they are imported unchanged by both @@ -40,6 +55,10 @@ There is no linter, formatter, or CI. Do not invent commands beyond these. fragmented and large frames (`server/websocket.js`). Do not lower the buffer limits without checking the snapshot path. - Indent `.js` files with 2 spaces. `.editorconfig` only enforces UTF-8. +- Keep aircraft out of the melee: `_battleTiles`/`_combatTarget` skip them and + their strikes are routed explicitly. Mirror the air rules (airport-only + rebasing, strike radius, ferry fuel) on both client and server, or prediction + and the authoritative state will disagree. ## Architecture @@ -58,6 +77,25 @@ There is no linter, formatter, or CI. Do not invent commands beyond these. pre-generated into `GameState.roads`) flatten a tile's movement cost on both sides through `_tileTravelHours` / `_stepHours`; the road set is shipped in the snapshot and drawn in the chunked `layer-roads`. +- Air and artillery are both ranged strikers (`GameState._isStrikeUnit`). A + ground or naval battery ordered to attack opens a **sustained hourly + bombardment**: it stays put and `_tickStrikes` removes a small share of its + attack damage each hour (`BOMBARD.hourlyDamageFraction`), clearing the target + when it dies or slips out of reach. An aircraft ordered to attack flies a + **sortie** (`requestAirStrike`): it bombs once on arrival in `advanceMovement` + and then returns to a friendly airport, where landing arms a two-day + `strikeReadyHour` cooldown. The strike is charged up front per aircraft + (`AIR_STRIKE`). Aircraft may only be rebased to one of their own airports and + the ferry is bounded by fuel, not the combat `missionRange`; they never join + melee. A standing `strikeTarget` is shipped in the snapshot and + `map_view/motion.js` draws the red dashed line and target ring on + `#layer-targets` while either endpoint is selected. +- Terrain generation is deterministic from `(seed, mapConfig)`, so + `GameState._buildTerrain` and the settlement/road step cache the world shape + (`shared/game_state/world.js`) rather than regenerating it for every game with + a known seed. The snapshot ships `mapConfig` and `MapView.ensureTerrain` + rebuilds the exact same terrain from it (not the global `MAP_CONFIG`), which + also lets the DOM tests use a small map fixture. - `shared/` holds the reusable algorithms so they stay testable without a server: `hex.js` (topology/geometry), `map_generator.js`, `hex_pathfinder.js`, `terrain_stats.js`, `rules.js`, `economy` figures in `game_state.js`, @@ -67,8 +105,8 @@ There is no linter, formatter, or CI. Do not invent commands beyond these. `shared/data.js` is a barrel re-exporting the whole catalogue, so keep importing from `shared/data.js` and edit the file under `shared/data/`. `shared/game_state.js` is likewise a thin class composed from the mixins in - `shared/game_state/` (world, territory, economy, movement, combat, status, - diplomacy, orders, visibility, serialization). + `shared/game_state/` (world, territory, economy, movement, combat, air, + status, diplomacy, orders, visibility, serialization). - `client/js/` splits the UI: `app.js` (screens and flow), `net.js` (transport wrapper), `game_screen.js` + `game_screen/` (HUD and panels), `map_view.js` + `map_view/` (DOM map), `modals.js` + `modals/` (city diff --git a/client/css/style.css b/client/css/style.css index b427666..ea403c0 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -216,6 +216,30 @@ input:focus, select:focus { border-color: var(--accent); } pointer-events: none; } +/* A ranged unit's bombardment: a red dashed line from the battery to the + target and a red ring around the target. Drawn on the layer below the entity + icons so the units themselves stay on top. */ +.target-line { + position: absolute; + height: 0; + border-top: 3px dashed #ff2b2b; + transform-origin: 0 50%; + pointer-events: none; + filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.7)); +} +.target-ring { + position: absolute; + width: 46px; + height: 46px; + margin-left: -23px; + margin-top: -23px; + box-sizing: border-box; + border: 3px solid #ff2b2b; + border-radius: 50%; + pointer-events: none; + box-shadow: 0 0 6px rgba(255, 43, 43, 0.85); +} + .tile-selection { position: absolute; width: var(--hex-w); @@ -770,6 +794,12 @@ input:focus, select:focus { border-color: var(--accent); } gap: 10px; text-align: left; } +/* Local test game: the free, instant twin of an order button. */ +.free-button { + border: 1px dashed rgba(130, 225, 170, 0.75) !important; + color: #b8f5cd; +} +.free-button:disabled { opacity: 0.4; } #modal-city .training-status { flex: 0 0 auto; } #modal-city .training-queue { flex: 1 1 auto; min-height: 0; } .progress { height: 14px; margin-top: 8px; background: rgba(0, 0, 0, 0.5); border-radius: 7px; overflow: hidden; } diff --git a/client/index.html b/client/index.html index fe7a1ba..16e5a3f 100644 --- a/client/index.html +++ b/client/index.html @@ -31,6 +31,13 @@ Nations in the world +