acdream/src/AcDream.App/Streaming/DungeonStreamingGate.cs
Erik 3ce1fae332 feat(teleport C): TAS-driven fade transit + retire TeleportArrivalController
Wires the dormant TeleportAnimSequencer as the transit driver: on PlayerTeleport
the player holds in PortalSpace behind a full-screen fade (FadeOverlay) until the
destination terrain is resident (TeleportWorldReady, gated on the priority-applied
landblock), then materializes (Place), and after the world fades back in regains
control + acks the server (FireLoginComplete). No movement resolves against the
empty world, so the outbound cell frame can't corrupt. Outdoor changes from
place-immediately back to hold-until-resident (now fast, not a band-aid).

- FadeOverlay: fullscreen NDC black quad, alpha = ShowTunnel ? 1 : FadeAlpha.
- Retires TeleportArrivalController + its 2 tests (TAS subsumes the driver role).
- Divergence register: AD-2 updated to the new mechanism; AD-31 (fade vs swirl).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:03:25 +02:00

51 lines
3.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

namespace AcDream.App.Streaming;
/// <summary>Result of the per-frame dungeon streaming-gate decision.</summary>
/// <param name="InsideDungeon">Passed to <see cref="StreamingController.Tick"/> — collapse
/// streaming to the single dungeon landblock.</param>
/// <param name="ObserverLandblockKey">When non-null, override the streaming observer to this
/// landblock key (the cell id's high 16 bits, 0xXXYY). Null leaves the caller's observer as-is.</param>
public readonly record struct DungeonGateResult(bool InsideDungeon, uint? ObserverLandblockKey);
/// <summary>
/// AP-36: the dungeon streaming gate (#133 FPS). When the player stands in a SEALED
/// EnvCell (an indoor cell that doesn't see outside), streaming collapses to the single
/// dungeon landblock — AC dungeons have no adjacent landblocks, so the normal 25×25
/// window would pull in ~129 unrelated ocean-grid dungeons. The trigger is the player's
/// CURRENT cell (<c>CellGraph.CurrCell</c>, set the moment the player is placed), and the
/// observer is pinned to that cell's OWN landblock (the cell id high 16 bits) because a
/// dungeon's EnvCells sit at arbitrary ocean-grid world coords with negative local offsets.
///
/// <para>Extracted from <c>GameWindow.OnUpdate</c> as a pure function so the
/// teleport-hold rule (below) is unit-testable without the GL/dat/network stack.</para>
/// </summary>
public static class DungeonStreamingGate
{
/// <summary>
/// Decide the streaming gate from the player's current cell.
/// </summary>
/// <param name="isTeleportHold">True while a teleport arrival is held (the controller is in
/// PortalSpace): the player is NOT yet placed, so <paramref name="currCellId"/> is the frozen
/// SOURCE cell, not where the player is going.</param>
/// <param name="currCellIsSealedDungeon"><c>CurrCell is EnvCell &amp;&amp; !SeenOutside</c>.</param>
/// <param name="currCellId">The current cell id (0xXXYYNNNN).</param>
public static DungeonGateResult Compute(
bool isTeleportHold, bool currCellIsSealedDungeon, uint currCellId)
{
// #145/#138: during a teleport hold the player is NOT yet placed, so CurrCell is
// the frozen SOURCE cell — where the player IS, not where they're going. Streaming
// must follow the DESTINATION, which the PortalSpace observer pin already does, so
// the source-cell gate is suppressed. Otherwise a teleport OUT of a dungeon keeps
// streaming collapsed on the source dungeon (CurrCell still sealed) → the outdoor
// destination never hydrates → the TAS transit holds 600 frames → force-snap to
// ocean. A teleport INTO a dungeon is handled explicitly upstream by
// StreamingController.PreCollapseToDungeon (and the controller's _collapsed latch
// holds it through the hold), so suppressing the gate here doesn't regress it.
if (isTeleportHold)
return new DungeonGateResult(false, null);
if (currCellIsSealedDungeon)
return new DungeonGateResult(true, currCellId >> 16);
return new DungeonGateResult(false, null);
}
}