using UnityEngine; using System; public class HoldNoteController : MonoBehaviour { private bool isInsideJudgeZone = false; public event Action OnJudgeZoneChanged; private bool isMoving = false; private float speed = 0f; private float activationTime; // Absolute positioning for hold segments (keeps segments connected under frame spikes) private bool useAbsolutePositioning = false; private Vector3 spawnPosition; private float baseSpawnYOffset = 0f; // can be negative to offset upward for delayed segments private float visualOffset = 0f; // additional Y offset for visual compensation private void Update() { if (useAbsolutePositioning) { if (!isMoving) return; ApplyAbsolutePosition(GameplayClock.NowSongTime); return; } // Start moving after activation time in legacy mode if (!isMoving && GameplayClock.NowSongTime >= activationTime) { isMoving = true; } if (isMoving) { transform.Translate(Vector3.down * speed * Time.deltaTime); } } /// /// Configure legacy timing (per-segment activation). /// public void ConfigureTiming(float baseHitTime, float segmentDelay, float travelTime) { float hitTime = baseHitTime + segmentDelay; activationTime = hitTime - travelTime; isMoving = false; useAbsolutePositioning = false; visualOffset = 0f; } /// /// Legacy delay setter (not recommended). /// public void SetSegmentDelay(float segmentDelay) { activationTime = GameplayClock.NowSongTime + segmentDelay; isMoving = false; useAbsolutePositioning = false; visualOffset = 0f; } public void SetSpeed(float newSpeed) { speed = newSpeed; } /// /// Configure absolute positioning for hold segments so they stay connected. /// spawnPoint should be the original spawn point for the lane. /// baseSpawnYOffset is applied to keep delayed segments offset upward (can be negative). /// public void ConfigureAbsolutePositioning(Vector3 spawnPoint, float newActivationTime, float spawnYOffset, float newVisualOffset = 0f) { spawnPosition = spawnPoint; activationTime = newActivationTime; baseSpawnYOffset = spawnYOffset; visualOffset = newVisualOffset; useAbsolutePositioning = true; isMoving = true; // Snap immediately to the expected position to avoid a 1-frame pop. ApplyAbsolutePosition(GameplayClock.NowSongTime); } public void StopMovement() { isMoving = false; visualOffset = 0f; } private void OnTriggerEnter2D(Collider2D collision) { if (!collision.CompareTag("JudgmentLine")) return; isInsideJudgeZone = true; OnJudgeZoneChanged?.Invoke(true); } private void OnTriggerExit2D(Collider2D collision) { if (!collision.CompareTag("JudgmentLine")) return; isInsideJudgeZone = false; OnJudgeZoneChanged?.Invoke(false); } public bool IsInsideJudgArea() { return isInsideJudgeZone; } public float ActivationTime => activationTime; public float CurrentSpeed => speed; public bool UsesAbsolutePositioning => useAbsolutePositioning; public float BaseSpawnYOffset => baseSpawnYOffset; public Vector3 SpawnPosition => spawnPosition; public float VisualOffset => visualOffset; private void ApplyAbsolutePosition(float time) { float elapsed = time - activationTime; float travelDistance = speed * elapsed; Vector3 newPos = spawnPosition; // Apply visual offset (positive moves the note down its path earlier) newPos.y -= (baseSpawnYOffset + travelDistance + visualOffset); transform.position = newPos; } }