9.8 KiB
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.jsowns one and validates orders;server/server.jsdrives the clocks (movement every tick, an in-game hour perSECONDS_PER_HOUR). - Shared logic runs in Node and the browser.
shared/stays framework-free (no Node, no DOM). - GameState is mixins.
shared/game_state.jsimports eachshared/game_state/<topic>.jsandObject.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_pathinclient/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.jsextendingTestCase; fixtures intests/framework/helpers.js. Force RNG withstate._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_fighterandbomberhaveair: true,speed: 5,traversableTerrains: ["Land", "Sea", "Ice"],enduranceHours(10 / 20) andmissionRange(80 / 100), plusrequiresBuilding: "airport".bomber.strike = true. Missiles (missile,icbm) arestrikeunits too but notair.shared/game_state/air.js:_isAirProto/_isAirUnit;_isStrikeUnit(any aircraft, orstrike, 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 cooldownstrikeReadyHour).requestAirStrike(unitIds, goal): a sortie — the plane flies to the target, bombs once on arrival inadvanceMovement, then returns to a friendly airport, where landing arms theAIR_STRIKE.cooldownHourscooldown.- 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, notmissionRange.- Client:
client/js/map_view/motion.jsextrapolates the snapshot path and predicts local move orders;map_view/motion.jsalready has_nearestCopyfor 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)
- 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, usingtopology.pixelDeltafor the shortest wrapped copy) inshared/(hex.js or air.js), called by bothGameStateandmotion.js. - 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. - Destination semantics. A straight line ends at the destination tile
centre. For a move order, the plane ends on that tile (then
_tickAirsends 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. - Interception / air defense. Combat is tile-based:
AIR_DEFENSEfires 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". - Strike radius and range.
missionRangeandrangeare 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. - The cylindrical map. A straight line must take the shortest wrapped copy
and be drawn across the seam.
topology.pixelDeltagives 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. - 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.jsand the entity icon code. - Zone of control / territory. Irrelevant to aircraft (they already ignore
both). Make sure the new path code does not accidentally reintroduce the
tile-based
_canUnitEnterchecks for air. - Missiles.
missile/icbmarestrikebut notair; decide whether the straight-line rule applies to them too (they are "fired, not marched", so likely yes for their strike flight). - 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
- Add the shared straight-line path helper (with wrap) and unit tests for the distance/time/wrap maths.
- Use it for ferrying (rebasing) first — the simplest case, no combat.
- Use it for sorties and keep the round-trip fuel check.
- Decide and implement interception/air-defense semantics (question 4).
- Mirror in the client prediction and the icon/arc rendering.
- 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.
_pathHoursreturns 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 rawx2 - 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.
_tickAiridle-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).