Files
AppleHillsProduction/Assets/Scripts/UI/Tutorial/DivingTutorial.cs
tschesky 0aa2270e1a Lifecycle System Refactor & Logging Centralization (#56)
## ManagedBehaviour System Refactor

- **Sealed `Awake()`** to prevent override mistakes that break singleton registration
- **Added `OnManagedAwake()`** for early initialization (fires during registration)
- **Renamed lifecycle hook:** `OnManagedAwake()` → `OnManagedStart()` (fires after boot, mirrors Unity's Awake→Start)
- **40 files migrated** to new pattern (2 core, 38 components)
- Eliminated all fragile `private new void Awake()` patterns
- Zero breaking changes - backward compatible

## Centralized Logging System

- **Automatic tagging** via `CallerMemberName` and `CallerFilePath` - logs auto-tagged as `[ClassName][MethodName] message`
- **Unified API:** Single `Logging.Debug/Info/Warning/Error()` replaces custom `LogDebugMessage()` implementations
- **~90 logging call sites** migrated across 10 files
- **10 redundant helper methods** removed
- All logs broadcast via `Logging.OnLogEntryAdded` event for real-time monitoring

## Custom Log Console (Editor Window)

- **Persistent filter popups** for multi-selection (classes, methods, log levels) - windows stay open during selection
- **Search** across class names, methods, and message content
- **Time range filter** with MinMaxSlider
- **Export** filtered logs to timestamped `.txt` files
- **Right-click context menu** for quick filtering and copy actions
- **Visual improvements:** White text, alternating row backgrounds, color-coded log levels
- **Multiple instances** supported for simultaneous system monitoring
- Open via `AppleHills > Custom Log Console`

Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: #56
2025-11-11 08:48:29 +00:00

230 lines
7.5 KiB
C#

using System.Collections;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using Input;
using Pixelplacement;
using UI.Core;
using UnityEngine;
using UnityEngine.Audio;
namespace UI.Tutorial
{
public class DivingTutorial : ManagedBehaviour, ITouchInputConsumer
{
public enum ProgressType
{
Manual, // Wait for player tap after animation loop
Auto // Automatically progress after animation loop
}
private StateMachine _stateMachine;
public bool playTutorial;
public AudioSource bottleAudioPlayer;
public AudioResource introVO;
[SerializeField] private ProgressType progressType = ProgressType.Auto;
// gating for input until current state's animation finishes first loop
[SerializeField] private GameObject tapPrompt;
private bool _canAcceptInput;
private Coroutine _waitLoopCoroutine;
public override int ManagedAwakePriority => 200; // Tutorial runs late, after other systems
internal override void OnManagedStart()
{
// Ensure prompt is hidden initially (even before tutorial initialization)
if (tapPrompt != null)
tapPrompt.SetActive(false);
if (playTutorial && !SaveLoadManager.Instance.currentSaveData.playedDivingTutorial)
{
// TODO: Possibly do it better, but for now just mark tutorial as played immediately
SaveLoadManager.Instance.currentSaveData.playedDivingTutorial = true;
// pause the game, hide UI, and register for input overrides
GameManager.Instance.RequestPause(this);
UIPageController.Instance.HideAllUI();
InputManager.Instance.RegisterOverrideConsumer(this);
// Setup references
_stateMachine = GetComponentInChildren<StateMachine>();
_stateMachine.OnLastStateExited.AddListener(RemoveTutorial);
// prepare gating for the initial active state
SetupInputGateForCurrentState();
}
else
{
RemoveTutorial();
}
}
void RemoveTutorial()
{
Logging.Debug("Remove me!");
if (_waitLoopCoroutine != null)
{
StopCoroutine(_waitLoopCoroutine);
_waitLoopCoroutine = null;
}
// Unpause, unregister input, and show UI
InputManager.Instance.UnregisterOverrideConsumer(this);
UIPageController.Instance.ShowAllUI();
GameManager.Instance.ReleasePause(this);
// hide prompt if present
if (tapPrompt != null)
tapPrompt.SetActive(false);
Destroy(gameObject);
bottleAudioPlayer.resource = introVO;
bottleAudioPlayer.Play();
}
public void OnTap(Vector2 position)
{
if (!_canAcceptInput) return; // block taps until allowed
// consume this tap and immediately block further taps
SetInputEnabled(false);
// move to next state
_stateMachine.Next(true);
// after the state changes, set up gating for the new active state's animation
SetupInputGateForCurrentState();
}
public void OnHoldStart(Vector2 position)
{
}
public void OnHoldMove(Vector2 position)
{
}
public void OnHoldEnd(Vector2 position)
{
}
// centralize enabling/disabling input and the tap prompt
private void SetInputEnabled(bool allow)
{
_canAcceptInput = allow;
if (tapPrompt != null)
{
// Only show tap prompt in Manual mode
tapPrompt.SetActive(allow && progressType == ProgressType.Manual);
}
}
private void SetupInputGateForCurrentState()
{
if (_waitLoopCoroutine != null)
{
StopCoroutine(_waitLoopCoroutine);
_waitLoopCoroutine = null;
}
_waitLoopCoroutine = StartCoroutine(WaitForFirstLoopOnActiveState());
}
private IEnumerator WaitForFirstLoopOnActiveState()
{
// wait a frame to ensure StateMachine has activated the correct state GameObject
yield return null;
// find the active child under the StateMachine (the current state)
Transform smTransform = _stateMachine != null ? _stateMachine.transform : transform;
Transform activeState = null;
for (int i = 0; i < smTransform.childCount; i++)
{
var child = smTransform.GetChild(i);
if (child.gameObject.activeInHierarchy)
{
activeState = child;
break;
}
}
if (activeState == null)
{
// if we can't find an active state, fail open: allow input
SetInputEnabled(true);
yield break;
}
// look for a legacy Animation component on the active state
var anim = activeState.GetComponent<Animation>();
if (anim == null)
{
// no animation to wait for; allow input immediately
SetInputEnabled(true);
yield break;
}
// determine a clip/state to observe
string clipName = anim.clip != null ? anim.clip.name : null;
AnimationState observedState = null;
if (!string.IsNullOrEmpty(clipName))
{
observedState = anim[clipName];
}
else
{
// fallback: take the first enabled state in the Animation
foreach (AnimationState st in anim)
{
observedState = st;
break;
}
}
if (observedState == null)
{
// nothing to observe; allow input
SetInputEnabled(true);
yield break;
}
// wait until the animation starts playing the observed clip
float safetyTimer = 0f;
while (anim.isActiveAndEnabled && activeState.gameObject.activeInHierarchy && !anim.IsPlaying(observedState.name) && safetyTimer < 2f)
{
safetyTimer += Time.deltaTime;
yield return null;
}
// wait until the first loop completes (normalizedTime >= 1)
while (anim.isActiveAndEnabled && activeState.gameObject.activeInHierarchy)
{
// if state changed (not playing anymore), allow input to avoid deadlock
if (!anim.IsPlaying(observedState.name)) break;
if (observedState.normalizedTime >= 1f)
{
break;
}
yield return null;
}
// After first loop completes, handle based on progress type
if (progressType == ProgressType.Auto)
{
// Auto mode: immediately progress to next state
_stateMachine.Next(true);
SetupInputGateForCurrentState();
}
else
{
// Manual mode: enable input and wait for player tap
SetInputEnabled(true);
}
_waitLoopCoroutine = null;
}
}
}