using Godot;
namespace RetroGame.Features.Typewriter;
///
/// Reusable typewriter effect for RichTextLabels.
/// Add as a child node and call Update() from _Process().
///
public partial class TypewriterEffect : Node
{
[Export] public AudioStream[] Sounds { get; set; } = [];
[Export(PropertyHint.Range, "0.0,0.2")] public float PitchVariation { get; set; } = 0.05f;
[Export(PropertyHint.Range, "0.0,0.3")] public float SoundCooldown { get; set; } = 0.08f;
[Export] public float FastForwardMultiplier { get; set; } = 4f;
public bool IsActive { get; private set; }
public bool IsComplete { get; private set; }
private RichTextLabel _label;
private AudioStreamPlayer _audioPlayer;
private RandomNumberGenerator _rng = new();
private float _duration;
private Curve _curve;
private float _elapsed;
private int _lastVisibleChars;
private float _soundCooldownRemaining;
private bool _fastForward;
public override void _Ready()
{
_audioPlayer = new AudioStreamPlayer { Bus = Audio.AudioBuses.Sfx };
AddChild(_audioPlayer);
}
///
/// Start the typewriter effect on a label.
///
public void Start(RichTextLabel label, float duration, Curve curve = null)
{
_label = label;
_duration = duration;
_curve = curve;
_elapsed = 0f;
_lastVisibleChars = 0;
_soundCooldownRemaining = 0f;
_fastForward = false;
IsActive = true;
IsComplete = false;
if (_label != null)
{
_label.VisibleRatio = 0f;
}
}
///
/// Call this from the parent's _Process().
///
public void Update(float delta)
{
if (!IsActive || _label == null) return;
float effectiveDelta = _fastForward ? delta * FastForwardMultiplier : delta;
_soundCooldownRemaining -= delta; // Real delta for sound cooldown
_elapsed += effectiveDelta;
float t = Mathf.Clamp(_elapsed / _duration, 0f, 1f);
// Apply curve if provided, otherwise linear
if (_curve != null)
{
_label.VisibleRatio = _curve.SampleBaked(t);
}
else
{
_label.VisibleRatio = t;
}
// Play sound when new characters appear
int visibleChars = _label.VisibleCharacters;
if (visibleChars > _lastVisibleChars)
{
TryPlaySound();
_lastVisibleChars = visibleChars;
}
if (t >= 1f)
{
IsActive = false;
IsComplete = true;
_fastForward = false;
}
}
///
/// Enable fast-forward mode (multiplied speed).
///
public void SetFastForward(bool enabled)
{
_fastForward = enabled;
}
///
/// Immediately complete the typewriter effect.
///
public void Complete()
{
if (_label != null)
{
_label.VisibleRatio = 1f;
}
IsActive = false;
IsComplete = true;
_fastForward = false;
}
///
/// Stop the typewriter without completing.
///
public void Stop()
{
IsActive = false;
_fastForward = false;
_label = null;
}
private void TryPlaySound()
{
if (_audioPlayer == null || Sounds == null || Sounds.Length == 0)
return;
if (_soundCooldownRemaining > 0) return;
_soundCooldownRemaining = SoundCooldown;
var sound = Sounds[_rng.RandiRange(0, Sounds.Length - 1)];
_audioPlayer.Stream = sound;
// Compensate for AudioServer.PlaybackSpeedScale so typewriter sounds
// always play at normal pitch regardless of global audio slow-down.
float speedCompensation = AudioServer.PlaybackSpeedScale > 0
? 1.0f / AudioServer.PlaybackSpeedScale
: 1.0f;
_audioPlayer.PitchScale = (float)GD.RandRange(1f - PitchVariation, 1f + PitchVariation) * speedCompensation;
_audioPlayer.Play();
}
}