feat(core/slice-1): TeleportAnimSequencer — Begin(), IsActive, enter-sound edge event
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4f7e8ec30a
commit
0468df21f5
2 changed files with 189 additions and 0 deletions
|
|
@ -35,3 +35,164 @@ public readonly record struct TeleportAnimSnapshot(
|
|||
float FadeAlpha, // 0 = clear world, 1 = full black
|
||||
bool ShowTunnel, // true during TunnelFadeIn..TunnelFadeOut
|
||||
bool ShowPleaseWait); // true during TunnelContinue only
|
||||
|
||||
/// <summary>
|
||||
/// Pure 7-state teleport animation machine (spec §2.1, acclient gmSmartBoxUI::UseTime 0x004d6e30).
|
||||
/// No GL / dat / network dependency — fully unit-testable.
|
||||
/// </summary>
|
||||
public sealed class TeleportAnimSequencer
|
||||
{
|
||||
// Golden constants (spec §2.1, verified VAs):
|
||||
// TELEPORT_ANIM_FADE_TIME 0x007BD278 = 1.0s
|
||||
// TELEPORT_ANIM_MIN_CONTINUE_TIME 0x007BD268 = 2.0s
|
||||
// TELEPORT_ANIM_MAX_CONTINUE_TIME 0x007BD270 = 5.0s
|
||||
public const float FadeTime = 1.0f;
|
||||
public const float MinContinue = 2.0f;
|
||||
public const float MaxContinue = 5.0f;
|
||||
|
||||
private TeleportAnimState _state = TeleportAnimState.Off;
|
||||
private float _elapsed = 0f; // time in current state
|
||||
private float _continueElapsed = 0f; // tracks time inside TunnelContinue
|
||||
private bool _enterSoundPending = false;
|
||||
private bool _enterTunnelPending = false;
|
||||
private bool _exitSoundPending = false;
|
||||
private bool _loginCompletePending = false;
|
||||
|
||||
public bool IsActive => _state != TeleportAnimState.Off;
|
||||
public TeleportAnimState State => _state;
|
||||
|
||||
/// <summary>
|
||||
/// Start the animation. Portal/Login/Death enter at Tunnel (skipping world-fade-out);
|
||||
/// Logout enters at WorldFadeOut (spec §2.5).
|
||||
/// </summary>
|
||||
public void Begin(TeleportEntryKind kind)
|
||||
{
|
||||
_state = kind == TeleportEntryKind.Logout
|
||||
? TeleportAnimState.WorldFadeOut
|
||||
: TeleportAnimState.Tunnel;
|
||||
_elapsed = 0f;
|
||||
_continueElapsed = 0f;
|
||||
_enterSoundPending = true;
|
||||
_enterTunnelPending = _state == TeleportAnimState.Tunnel; // true for Portal/Login/Death
|
||||
_exitSoundPending = false;
|
||||
_loginCompletePending = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advance the machine by <paramref name="dt"/> seconds.
|
||||
/// <paramref name="worldReady"/> = destination collision landblock is resident.
|
||||
/// Returns the current snapshot + edge-triggered events fired THIS tick.
|
||||
/// </summary>
|
||||
public (TeleportAnimSnapshot snapshot, IReadOnlyList<TeleportAnimEvent> events)
|
||||
Tick(float dt, bool worldReady)
|
||||
{
|
||||
var evts = new List<TeleportAnimEvent>();
|
||||
|
||||
// Edge events queued by Begin or a prior transition:
|
||||
if (_enterSoundPending) { evts.Add(TeleportAnimEvent.PlayEnterSound); _enterSoundPending = false; }
|
||||
if (_enterTunnelPending) { evts.Add(TeleportAnimEvent.EnterTunnel); _enterTunnelPending = false; }
|
||||
if (_exitSoundPending) { evts.Add(TeleportAnimEvent.PlayExitSound); _exitSoundPending = false; }
|
||||
if (_loginCompletePending) { evts.Add(TeleportAnimEvent.FireLoginComplete); _loginCompletePending = false; }
|
||||
|
||||
_elapsed += dt;
|
||||
|
||||
// State-machine transitions:
|
||||
switch (_state)
|
||||
{
|
||||
case TeleportAnimState.WorldFadeOut:
|
||||
if (_elapsed >= FadeTime)
|
||||
Advance(TeleportAnimState.TunnelFadeIn, enterTunnel: false);
|
||||
break;
|
||||
|
||||
case TeleportAnimState.TunnelFadeIn:
|
||||
if (_elapsed >= FadeTime)
|
||||
Advance(TeleportAnimState.Tunnel, enterTunnel: true);
|
||||
break;
|
||||
|
||||
case TeleportAnimState.Tunnel:
|
||||
// Hold here until worldReady (EndTeleportAnimation analogue).
|
||||
if (worldReady)
|
||||
{
|
||||
evts.Add(TeleportAnimEvent.Place);
|
||||
Advance(TeleportAnimState.TunnelContinue, enterTunnel: false);
|
||||
_continueElapsed = 0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case TeleportAnimState.TunnelContinue:
|
||||
_continueElapsed += dt;
|
||||
bool minMet = _continueElapsed >= MinContinue;
|
||||
bool maxForce = _continueElapsed >= MaxContinue;
|
||||
if (minMet || maxForce)
|
||||
Advance(TeleportAnimState.TunnelFadeOut, enterTunnel: false);
|
||||
break;
|
||||
|
||||
case TeleportAnimState.TunnelFadeOut:
|
||||
if (_elapsed >= FadeTime)
|
||||
{
|
||||
Advance(TeleportAnimState.WorldFadeIn, enterTunnel: false);
|
||||
// Flush exit sound in the same tick:
|
||||
evts.Add(TeleportAnimEvent.PlayExitSound);
|
||||
_exitSoundPending = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case TeleportAnimState.WorldFadeIn:
|
||||
if (_elapsed >= FadeTime)
|
||||
{
|
||||
Advance(TeleportAnimState.Off, enterTunnel: false);
|
||||
evts.Add(TeleportAnimEvent.FireLoginComplete);
|
||||
_loginCompletePending = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case TeleportAnimState.Off:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (BuildSnapshot(), evts);
|
||||
}
|
||||
|
||||
private void Advance(TeleportAnimState next, bool enterTunnel)
|
||||
{
|
||||
_state = next;
|
||||
_elapsed = 0f;
|
||||
if (enterTunnel) _enterTunnelPending = true;
|
||||
}
|
||||
|
||||
private TeleportAnimSnapshot BuildSnapshot()
|
||||
{
|
||||
float alpha = ComputeFadeAlpha(_state, _elapsed);
|
||||
bool showTunnel = _state is TeleportAnimState.TunnelFadeIn
|
||||
or TeleportAnimState.Tunnel
|
||||
or TeleportAnimState.TunnelContinue
|
||||
or TeleportAnimState.TunnelFadeOut;
|
||||
bool pleaseWait = _state == TeleportAnimState.TunnelContinue;
|
||||
return new TeleportAnimSnapshot(_state, alpha, showTunnel, pleaseWait);
|
||||
}
|
||||
|
||||
private static float Smoothstep(float t)
|
||||
{
|
||||
t = Math.Clamp(t, 0f, 1f);
|
||||
return t * t * (3f - 2f * t);
|
||||
}
|
||||
|
||||
private static float ComputeFadeAlpha(TeleportAnimState state, float elapsed)
|
||||
{
|
||||
float t = Math.Clamp(elapsed / FadeTime, 0f, 1f);
|
||||
return state switch
|
||||
{
|
||||
// Fading TO black (alpha 0→1):
|
||||
TeleportAnimState.WorldFadeOut => Smoothstep(t),
|
||||
TeleportAnimState.TunnelFadeIn => 1f - Smoothstep(t), // tunnel fades IN: overlay goes clear
|
||||
// Full black / fully clear inside tunnel states:
|
||||
TeleportAnimState.Tunnel => 0f, // world hidden, overlay not needed
|
||||
TeleportAnimState.TunnelContinue => 0f,
|
||||
// Fading back out of tunnel:
|
||||
TeleportAnimState.TunnelFadeOut => Smoothstep(t), // tunnel fades out: overlay goes black
|
||||
TeleportAnimState.WorldFadeIn => 1f - Smoothstep(t), // world fades in: overlay clears
|
||||
_ => 0f,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue