Scripts/Room/ClickableButton.cs
using System;
using Milaine.Managers;

namespace Milaine.Room;

public sealed class ClickableButton : Component {
  [Property, Group("References")]
  public SkinnedModelRenderer Model { get; set; }

  [Property, Group("Animation"), Title("Sequence Name")]
  public string PressSequence { get; set; } = "press";

  [Property, Group("Animation")]
  public bool UsesAnimGraph { get; set; } = false;

  [Property, Group("Interaction")]
  public float MaxClickDistance { get; set; } = 1000f;

  [Property, Group("Squish (Scale Z)"), Range(0.1f, 1f)]
  public float PressedScale { get; set; } = 0.8f;

  [Property, Group("Squish (Scale Z)"), Range(1f, 30f)]
  public float ScaleSpeed { get; set; } = 14f;

  public Action OnPressed { get; set; }

  private bool isHeld;
  private Vector3 originalScale;
  private float currentScaleFactor = 1f;

  protected override void OnStart() {
    if (!Model.IsValid()) {
      Model = Components.Get<SkinnedModelRenderer>();
    }

    originalScale = LocalScale;
  }

  protected override void OnUpdate() {
    if (MouseManager.State == StateMouse.Busy) {
      return;
    }

    var camera = Scene.Camera;

    if (!camera.IsValid()) {
      return;
    }

    var ray = camera.ScreenPixelToRay(Mouse.Position);
    var trace = Scene.Trace.Ray(ray, MaxClickDistance).Run();
    bool isHoveredNow = trace.Hit && trace.GameObject == GameObject;

    if ((Input.Pressed("attack1") || Input.Pressed("attack2")) && isHoveredNow) {
      Press();
      isHeld = true;
    }

    if (Input.Released("attack1") || Input.Released("attack2")) {
      isHeld = false;
    }

    float targetFactor = isHeld ? PressedScale : 1f;
    currentScaleFactor = MathX.Lerp(currentScaleFactor, targetFactor, Time.Delta * ScaleSpeed);

    LocalScale = new Vector3(originalScale.x, originalScale.y, originalScale.z * currentScaleFactor);
  }

  public void Press() {
    PlayPressAnimation();
    Sound.Play("sounds/sound_key_down.sound");
    OnPressed?.Invoke();
  }

  private void PlayPressAnimation() {
    if (!Model.IsValid()) {
      return;
    }

    if (UsesAnimGraph) {
      Model.Set(PressSequence, true);
    } else {
      Model.SceneModel.UseAnimGraph = false;
      Model.SceneModel.CurrentSequence.Name = PressSequence;
    }
  }
}