Skip to content

Proposal: add server-authoritative flying across linked map levels #20

Description

@zoeyrose

Important

This issue is implemented in the fresh MIT-licensed Go server under the replacement program. Its gameplay and content-design decisions remain authoritative. C, CPython, classic packet, file-path, and enum details in the preserved specification are historical evidence only; do not copy, translate, or structurally port GPL implementation code.

Replacement implementation contract

Preserve discrete linked-map altitude, capability/state separation, vertical collision, and cross-depth deferrals. Use stable map/surface identities and bounded generated request/state contracts.

The server remains authoritative, consumes versioned compiled content, and exposes bounded generated Game Protocol 1 messages. Pure rules may use a specifically approved typed CEL environment. Starlark is not part of this issue unless the separate residual-scripting decision explicitly approves it.

Required verification

  • Preserve every observable rule, balance decision, disclosure boundary, and anti-exploit invariant from the specification below.
  • Add deterministic Go unit/property tests and wrapper-managed scenario coverage at the appropriate integration boundary.
  • Add bounded malformed-input and persistence-failure cases where this feature accepts content, network, or stored data.
  • Add Go/Rust protocol conformance fixtures for every new cross-process field; the client must not reconstruct authoritative rules from prose.
  • Demonstrate that implementation and tests contain no copied GPL source/test material and execute no runtime Python.
Preserved product/design specification and historical implementation notes

Summary

Add server-authoritative player flight by treating the existing physical
TILED_UP/TILED_DOWN map stack as discrete altitude bands. A player who
ascends moves to the linked map at the same world coordinate; ordinary players
remain on their current physical map and can see the flyer through the
already-shipped multi-level map stream whenever LOS and vertical occlusion
allow it.

This should be implemented as a focused first slice: take off, ascend, descend,
land, render, select, save/reconnect, and fall safely when flight is lost.
Cross-depth attacks and fully simulated 3D projectiles should be designed
separately rather than accidentally inheriting inconsistent 2D behavior.

Why this fits the current codebase

Most of the expensive visibility and rendering foundation already exists:

  • server/src/include/global.h defines TILED_UP and TILED_DOWN, while map
    filename coordinates and mapstruct.coords[2] already identify physical
    levels.
  • server/src/socket/request.c:draw_client_map2() resolves and serializes each
    visible linked map as a separate, length-delimited CLIENT_CMD_MAP depth.
    It applies base-map gameplay LOS, vertical floor/opaque occlusion, building
    cutaways, and independent per-depth delta caches.
  • client/src/gui/widgets/map.c:map_draw_map() projects all active depths with
    a 46-pixel level lift and paints them in one cross-level isometric order.
  • A connected TILED_UP/TILED_DOWN player transition already sends a signed
    depth offset. copy_lastmap() and map_level_scroll() shift the server and
    client depth caches instead of forcing a full map refresh.
  • map_redraw() already locates observers across linked levels using
    rv_vector.distance_z, so an actor moving on an upper map invalidates the
    correct depth in a lower player's cache.

There is also an existing but narrower flight concept:

  • FLAG_FLYING lets an object traverse water/fire/cloud terrain in
    server/src/server/map.c:blocked() but does not bypass walls or closed doors.
  • Flying objects use FLY_ON/FLY_OFF instead of WALK_ON/WALK_OFF, do not
    press normal floor mechanisms, cannot pick up/apply ordinary ground objects,
    and receive the existing combat altitude modifier.
  • object_insert_map(..., INS_FALL_THROUGH) stops falling when it encounters a
    floor or when the object has FLAG_FLYING.

Those pieces mean an actor placed on a linked upper map is already visible to a
ground observer when authorized. The missing feature is a coherent player
state and movement contract that puts the actor there.

Current gaps

  1. Movement is two-dimensional. SERVER_CMD_MOVE accepts only directions
    0-8, and move_ob()/object_move_to() only change X/Y or cross horizontal
    map edges.
  2. FLAG_FLYING conflates an acquired capability with the active airborne
    state. Equipment recalculation directly sets/clears it, so there is no clean
    takeoff/landing toggle.
  3. There is no authoritative vertical collision operation. Ascending must not
    tunnel through an upper floor/opaque ceiling, and descending must not pass
    through the floor of the current level.
  4. Losing flight does not immediately settle an actor. The existing fall-through
    path runs on a later insertion/move and should not leave a player suspended
    after an effect expires or equipment changes.
  5. Target selection is depth-blind. The client scans only the depth-zero
    MapCell cache, and CMD_TARGET_MAPXY sends only X, Y, and object count;
    the server then searches only the player's physical map.
  6. Combat is mostly 2D. Melee correctly requires the same physical map, while
    projectile movement and much spell logic have no intentional cross-depth
    trajectory policy.
  7. Existing maps need an explicit way to forbid takeoff or cap ascent so flight
    cannot silently bypass indoor, quest, or private-area boundaries.

Proposed model

1. Keep physical maps authoritative for altitude

Do not add a second free-form Z coordinate to players. A player occupies
exactly one authoritative mapstruct; its linked depth is the altitude band.
Ascending or descending one band transfers the player at the same X/Y through
the reciprocal TILED_UP/TILED_DOWN link.

MAP2_MAX_DEPTH remains a render/serialization window, not a promise that the
world has that many flyable levels. The initial implementation must use only
authored/explicitly linked maps and must not synthesize an unbounded stack of
empty air maps.

This naturally gives each observer a relative view:

  • the flyer's physical map is depth zero for that client;
  • the ground is a negative depth for the flyer; and
  • the flyer is a positive depth for a player below.

The existing connected-map update and depth-cache shift should be reused for
vertical movement.

2. Separate capability from state

Introduce a distinct flight capability (for example FLAG_CAN_FLY or a
derived capability query) and retain FLAG_FLYING as the active airborne
state used by terrain, triggers, pickup/apply, and combat rules.

For players:

  • equipment, spells, skills, or species grant can fly;
  • takeoff sets flying without changing the current physical map;
  • losing the last flight capability immediately invokes the landing/fall
    resolver and then clears the active state; and
  • reconnect/load validates the saved physical level against the restored
    capability and settles the player if necessary.

Existing flying monsters, spell effects, and projectiles should retain their
current behavior in the first slice. Do not make every FLAG_FLYING object
start changing physical levels implicitly.

3. Add two explicit player actions

Add client actions and default-unbound keybind commands for:

  • Ascend / Take off: if grounded and capable, enter the airborne state on
    the current level; if already airborne, attempt to cross one TILED_UP link.
  • Descend / Land: if a supporting floor exists on the current level, land
    there; otherwise attempt to cross one TILED_DOWN link.

Use a dedicated, strictly framed client-to-server action such as
SERVER_CMD_MOVE_VERTICAL with a signed one-step direction. Handle action
cost, paralysis/freeze checks, movement/path cancellation, animation updates,
and rate limiting in the same authoritative layer as ordinary movement. A
text command may be useful for debugging, but should not be the gameplay wire
contract.

Expose can fly and currently flying in player state updates so the client
can disable actions and display an unambiguous status indicator. Horizontal
click-to-move and run paths remain on the current physical map in this slice;
they must never auto-ascend.

4. Make vertical collision directional and shared

Move the reusable vertical-boundary predicate out of the private map sender
helpers and give movement, visibility, and lighting one documented definition.
At minimum it must consider FLAG_IS_FLOOR and gameplay opacity
(P_BLOCKSVIEW); a dedicated authored vertical-blocking flag is preferable if
roof/floor semantics cannot be expressed unambiguously.

Rules:

  • Ascending crosses only when an upper link exists, the boundary at the upper
    cell is open from below, and the destination is legal for the complete
    player object.
  • Descending crosses only when the current cell's lower boundary is open and
    the destination is legal.
  • A floor is a landing surface, not something that can be crossed from below.
    To reach a roof, rise through an adjacent open column and move horizontally
    over it on the upper physical map.
  • Walls, closed doors, living occupancy, no_pass, map bounds, and multipart
    collision remain authoritative at the destination. Flight bypasses terrain
    classes, not solid geometry.
  • Missing links, map-level policy, collision, and vertical blockers produce a
    clear failure message and do not partially move or spend multiple actions.

The transfer helper must perform remove/insert, map enter/leave events, map
timeouts, player map bookkeeping, LOS refresh, target/path invalidation, and
connected-map cache reuse as one operation. It must not use a generic exit in a
way that can relocate X/Y or choose an unrelated entry point.

5. Add map-level flight policy

Add authored policy such as no_flight and, if needed, a bounded flight range
or ceiling. Apply it before loading/transferring into a linked level. This is
required for interiors, puzzle/quest spaces, private maps, and deliberately
sealed structures.

Update the map checker to validate:

  • reciprocal vertical links and compatible coordinate/dimension assumptions;
  • valid flight bounds/ceilings;
  • an open ascent column not leading into a sealed/private level; and
  • intended flight test fixtures at level edges.

Do not mass-generate air maps in this issue. Add explicit air levels only where
gameplay calls for them, preserving map-author control over access and world
silhouette.

6. Preserve server-owned visibility

No new actor-visibility packet is needed. Continue to send a flyer as a normal
LAYER_LIVING object on its authoritative upper map. A lower observer may see
it only when:

  • the relevant positive depth is linked and within the streamed window;
  • base 2D gameplay LOS authorizes live content at that X/Y;
  • vertical camera/information occlusion does not reduce the level to a
    structural boundary; and
  • ordinary darkness, invisibility, hidden-object, and player-specific rules
    allow it.

In particular, do not reveal a flyer through an unexplored wall silhouette,
roof, upper floor, secret/private level, or building cutaway. This proposal is
compatible with atrinik/client#17, but does not depend on moving camera cutaways to the
client.

Add a visible airborne status treatment for the local player and observers
without changing the sprite's authoritative map placement. The existing
per-depth projection supplies the actual height difference for observers.

7. Make selection depth-aware, but defer cross-depth combat

Extend map targets with signed relative depth. The client should gather
targets from every currently authorized depth cache and use the final painter
order/projected sprite bounds for mouse hit testing. Send depth plus X/Y/count
to the server; resolve the linked physical map and independently revalidate
viewport, LOS, vertical authorization, object identity, and visibility.

Selection, examination, names, and target UI should work for visible flyers.
For the first slice:

  • melee remains valid only at distance_z == 0;
  • ordinary floor apply/pickup/talk remains on the actor's physical level;
  • attacks/spells/projectiles without an explicit vertical policy reject a
    target at another depth as out of reach; and
  • the old flying/non-flying attack-roll modifier continues to describe actors
    sharing a physical level but occupying different local altitude states.

Cross-depth archery, spell trajectories, monster pursuit, pathfinding, and
vertical projectile collision need a separate combat design. They should use
rv_vector.distance_z deliberately rather than silently ignoring it.

8. Resolve landing and forced falls immediately

Create one server helper that settles an actor downward through linked maps
until it finds support or reaches a configured lower boundary. Use it when:

  • a player lands;
  • the last flight capability expires/is removed;
  • a flying player logs in or reconnects in an invalid state;
  • the current air map/link becomes unavailable; or
  • an admin/map transition places a non-flying player over an empty level.

Reuse the existing fall damage calculation by reporting the number of physical
levels fallen, but make the operation atomic and bounded. Validate each
destination, choose a nearby legal landing square only under the existing
well-defined collision rule, and use the emergency map only for a genuinely
unrecoverable invalid save/map state.

Implementation sequence

  1. Add server unit fixtures with a ground map, open +1 air map, solid +1
    floor/roof, and -1 level. Extract shared vertical-boundary and linked-level
    transfer helpers.
  2. Split player flight capability/state; implement takeoff, landing, one-level
    ascent/descent, immediate forced falling, persistence validation, map
    policy, and action/path/target cleanup.
  3. Add the vertical action and player-state protocol fields, client keybind
    actions/status UI, malformed-packet checks, and ADS-2 documentation.
  4. Extend client target caches/hit testing and CMD_TARGET_MAPXY with depth;
    add server-side disclosure/range revalidation and explicit cross-depth
    rejection for unsupported combat/interaction paths.
  5. Add one debug/admin grant plus at least one intentionally balanced gameplay
    source of the capability. Add a small authored flight test area rather than
    modifying unrelated world maps.
  6. Run server unit/protocol tests, both legacy builds, map checks, and the
    visual matrix below; then update the owning architecture/content guidance
    if the final boundaries differ from this proposal.

Acceptance criteria

  • A capable player can take off, move horizontally with existing flying
    terrain/trigger semantics, ascend one open authored level, descend, and
    land.
  • Incapable, grounded, paralyzed/frozen, or policy-blocked players cannot
    ascend; malformed/replayed vertical commands cannot produce partial or
    free movement.
  • Floors/opaque ceilings block ascent from below, current-level floors
    block descent through them, and flight never bypasses walls, closed
    doors, living collision, map bounds, or explicit no-flight policy.
  • A lower non-flying player sees an authorized flyer on +1/+2 with the
    correct projection, name, animation, darkness, lighting, and cache
    invalidation while either player moves.
  • Roofs, solid floors, gameplay LOS/FOW, cutaways, darkness, invisibility,
    secrets, and private levels continue to prevent unauthorized disclosure.
  • The flying player's transition is a connected map update whose client
    and server caches shift by the correct signed depth without stale or
    duplicated actors.
  • Visible flyers can be selected/inspected by cycle and mouse targeting;
    forged depth/X/Y/count combinations are rejected server-side.
  • Melee, apply/pickup/talk, spells, and projectiles cannot affect another
    physical depth unless that individual mechanic has an explicit tested
    vertical rule.
  • Losing flight, landing, logout/login, death, teleport, map reset/unload,
    and capability changes never leave a non-flying player suspended or
    inside blocked geometry; fall damage counts crossed physical levels.
  • Horizontal click-to-move/run never changes depth implicitly, and vertical
    actions consume the intended server-authoritative action cost.
  • Existing flying monsters, spell effects, projectiles, FLY_ON/FLY_OFF
    triggers, terrain access, and ordinary stair/exit behavior do not regress.
  • common/toolkit/socket.h, sender/receiver dispatch, bounds checks,
    target packet, tests, client controls, map policy/checker, and
    doc/ADS/ADS-2 describe one coherent contract with no compatibility shim.

Validation matrix

  • Server tests: open/blocked/missing up/down links, destination occupancy,
    reciprocal-map errors, floor landing, multi-level forced fall and damage,
    capability loss, save/reload, no-flight maps, action cost, path/run cancel,
    map enter/leave events, and target forgery.
  • Visibility/protocol tests: observers at depths -2 through +2, base LOS
    blocked/unblocked, upper floor/roof/cutaway, darkness/invisibility, target
    depth framing, duplicate/truncated/out-of-range payloads, and connected cache
    offsets.
  • Client tests/manual checks: keyboard actions and status, target cycling,
    projected mouse hit testing, local player centered on an upper map, observer
    projection from below/above, map-edge movement, minimap, reconnect, and rapid
    alternating ascent/descent.
  • Regression: ./build.sh atrinik-server, ./build.sh atrinik, targeted
    CTest suites (then ./build.sh check), focused map checks, clang-format, and
    git diff --check.

Out of scope

  • Free-form continuous Z movement, arbitrary voxel physics, or a rotating/3D
    camera.
  • Implicit generation of infinite air maps or retrofitting every world map in
    the first change.
  • Cross-depth melee, ballistic/projectile arcs, spell propagation, monster
    vertical pursuit, or 3D pathfinding.
  • Client-authoritative collision, LOS, flight eligibility, target visibility,
    or map-access policy.
  • Replacing the current stacked-map renderer; atrinik/client#17 may modernize its camera
    presentation independently.

Metadata

Metadata

Assignees

No one assigned

    Fields

    Priority

    None yet

    Start date

    None yet

    Target date

    None yet

    Effort

    None yet

    Projects

    Status
    Backlog

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions