fix(runtime): align portal and movement presentation

Port retail portal viewport projection and reveal behavior, preserve outbound combat style, drive remote and local grounded movement from authored CSequence root frames, and reuse the local prepared pose so animation hooks advance once.

User-verified portal, observer movement, combat stance, and short-tap locomotion gates. Release build passed with 5,767 tests and five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 08:48:27 +02:00
parent e95f55f25b
commit 124e046976
30 changed files with 1362 additions and 348 deletions

View file

@ -53,6 +53,7 @@ public sealed class TeleportAnimSequencer
public const int TunnelEndFrame = 120;
public const float ExitWindowLow = FadeTime + 0.1f;
public const float ExitWindowHigh = FadeTime + 0.3f;
private const short LastVisibleOutgoingAnimationLevel = 1022;
// UIGlobals::Init @ 0x004EE470. Retail integrates 100 integer sine
// samples into a 0..1024 easing table.
@ -119,7 +120,7 @@ public sealed class TeleportAnimSequencer
switch (_state)
{
case TeleportAnimState.WorldFadeOut:
if (_elapsed >= FadeTime)
if (OutgoingViewportReachedTerminalProjection())
// UseTime's viewport-visibility block precedes this state
// transition, so retail shows portal space on the next tick.
Advance(TeleportAnimState.TunnelFadeIn, enterTunnel: true);
@ -157,7 +158,7 @@ public sealed class TeleportAnimSequencer
break;
case TeleportAnimState.TunnelFadeOut:
if (_elapsed >= FadeTime)
if (OutgoingViewportReachedTerminalProjection())
{
Advance(TeleportAnimState.WorldFadeIn, enterTunnel: false);
evts.Add(TeleportAnimEvent.PlayExitSound);
@ -180,6 +181,17 @@ public sealed class TeleportAnimSequencer
return (BuildSnapshot(), evts);
}
/// <summary>
/// Retire an outgoing viewport before the first quantized projection that
/// exposes the finite tunnel boundary. Retail's whole-frame black clear and
/// frame-paced D3D presentation show level 1022 as the last tunnel sample;
/// paired captures show no 1023/1024 portal frame before the world swap.
/// An uncapped modern loop can otherwise publish those sub-20 ms samples
/// and let the desktop compositor hold one for a complete display refresh.
/// </summary>
private bool OutgoingViewportReachedTerminalProjection()
=> GetRetailAnimationLevel(_elapsed / FadeTime) > LastVisibleOutgoingAnimationLevel;
private void Advance(TeleportAnimState next, bool enterTunnel)
{
_state = next;