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 +