Files
Battle-for-Tismo/AIR_MOVEMENT.md
T

9.8 KiB

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.assigns 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.

INTELLIGENCE.md, STACKS.md, ECONOMY_BALANCE.md, POLITICS_PERFORMANCE.md, TRAINING_UI.md, ICONS.md (repo root).