Files
Battle-for-Tismo/AIR_MOVEMENT.md
T

198 lines
9.8 KiB
Markdown

# 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/<topic>.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 <name>.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 - <name>` when art is
missing.
- Tests in `tests/<topic>_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).