198 lines
9.8 KiB
Markdown
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).
|