Refactor interactions, introduce template-method lifecycle management, work on save-load system (#51)

# Lifecycle Management & Save System Revamp

## Overview
Complete overhaul of game lifecycle management, interactable system, and save/load architecture. Introduces centralized `ManagedBehaviour` base class for consistent initialization ordering and lifecycle hooks across all systems.

## Core Architecture

### New Lifecycle System
- **`LifecycleManager`**: Centralized coordinator for all managed objects
- **`ManagedBehaviour`**: Base class replacing ad-hoc initialization patterns
  - `OnManagedAwake()`: Priority-based initialization (0-100, lower = earlier)
  - `OnSceneReady()`: Scene-specific setup after managers ready
  - Replaces `BootCompletionService` (deleted)
- **Priority groups**: Infrastructure (0-20) → Game Systems (30-50) → Data (60-80) → UI/Gameplay (90-100)
- **Editor support**: `EditorLifecycleBootstrap` ensures lifecycle works in editor mode

### Unified SaveID System
- Consistent format: `{ParentName}_{ComponentType}`
- Auto-registration via `AutoRegisterForSave = true`
- New `DebugSaveIds` editor tool for inspection

## Save/Load Improvements

### Enhanced State Management
- **Extended SaveLoadData**: Unlocked minigames, card collection states, combination items, slot occupancy
- **Async loading**: `ApplyCardCollectionState()` waits for card definitions before restoring
- **New `SaveablePlayableDirector`**: Timeline sequences save/restore playback state
- **Fixed race conditions**: Proper initialization ordering prevents data corruption

## Interactable & Pickup System

- Migrated to `OnManagedAwake()` for consistent initialization
- Template method pattern for state restoration (`RestoreInteractionState()`)
- Fixed combination item save/load bugs (items in slots vs. follower hand)
- Dynamic spawning support for combined items on load
- **Breaking**: `Interactable.Awake()` now sealed, use `OnManagedAwake()` instead

##  UI System Changes

- **AlbumViewPage** and **BoosterNotificationDot**: Migrated to `ManagedBehaviour`
- **Fixed menu persistence bug**: Menus no longer reappear after scene transitions
- **Pause Menu**: Now reacts to all scene loads (not just first scene)
- **Orientation Enforcer**: Enforces per-scene via `SceneManagementService`
- **Loading Screen**: Integrated with new lifecycle

## ⚠️ Breaking Changes

1. **`BootCompletionService` removed** → Use `ManagedBehaviour.OnManagedAwake()` with priority
2. **`Interactable.Awake()` sealed** → Override `OnManagedAwake()` instead
3. **SaveID format changed** → Now `{ParentName}_{ComponentType}` consistently
4. **MonoBehaviours needing init ordering** → Must inherit from `ManagedBehaviour`

Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Reviewed-on: #51
This commit is contained in:
2025-11-07 15:38:31 +00:00
parent dfa42b2296
commit e27bb7bfb6
93 changed files with 7900 additions and 4347 deletions

View File

@@ -2,14 +2,13 @@
using AppleHills.Core.Settings;
using Cinematics;
using Core;
using Core.Lifecycle;
using Input;
using Minigames.DivingForPictures.PictureCamera;
using System;
using System.Collections;
using System.Collections.Generic;
using Bootstrap;
using Minigames.DivingForPictures.Bubbles;
using UI;
using UI.Core;
using UnityEngine;
using UnityEngine.Events;
@@ -17,7 +16,7 @@ using UnityEngine.Playables;
namespace Minigames.DivingForPictures
{
public class DivingGameManager : MonoBehaviour, IPausable
public class DivingGameManager : ManagedBehaviour, IPausable
{
[Header("Monster Prefabs")]
[Tooltip("Array of monster prefabs to spawn randomly")]
@@ -104,10 +103,12 @@ namespace Minigames.DivingForPictures
public static DivingGameManager Instance => _instance;
private void Awake()
public override int ManagedAwakePriority => 190;
public override bool AutoRegisterPausable => true; // Automatic GameManager registration
protected override void Awake()
{
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
_currentSpawnProbability = _settings?.BaseSpawnProbability ?? 0.2f;
base.Awake();
if (_instance == null)
{
@@ -117,20 +118,29 @@ namespace Minigames.DivingForPictures
{
Destroy(gameObject);
}
// Ensure any previous run state is reset when this manager awakes
_isGameOver = false;
}
private void Start()
protected override void OnManagedAwake()
{
// Register for post-boot initialization
BootCompletionService.RegisterInitAction(InitializePostBoot);
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
_currentSpawnProbability = _settings?.BaseSpawnProbability ?? 0.2f;
// Subscribe to player damage events (this doesn't depend on initialization)
// Ensure any previous run state is reset when this manager awakes
_isGameOver = false;
Logging.Debug("[DivingGameManager] Initialized");
}
protected override void OnSceneReady()
{
InitializeGame();
// Subscribe to scene-specific events
CinematicsManager.Instance.OnCinematicStopped += EndGame;
PlayerCollisionBehavior.OnDamageTaken += OnPlayerDamageTaken;
OnMonsterSpawned += DoMonsterSpawned;
// Validate rope references (this doesn't depend on initialization)
// Validate rope references
ValidateRopeReferences();
viewfinderManager = CameraViewfinderManager.Instance;
@@ -151,33 +161,21 @@ namespace Minigames.DivingForPictures
RegisterExemptFromPhotoSequencePausing(viewfinderPausable);
}
}
OnMonsterSpawned += DoMonsterSpawned;
}
private void InitializePostBoot()
protected override void OnDestroy()
{
// Register this manager with the global GameManager
if (GameManager.Instance != null)
{
GameManager.Instance.RegisterPausableComponent(this);
}
base.OnDestroy(); // Handles auto-unregister from GameManager
InitializeGame();
CinematicsManager.Instance.OnCinematicStopped += EndGame;
}
private void OnDestroy()
{
// Unsubscribe from events when the manager is destroyed
PlayerCollisionBehavior.OnDamageTaken -= OnPlayerDamageTaken;
OnMonsterSpawned -= DoMonsterSpawned;
// Unregister from GameManager
if (GameManager.Instance != null)
if (CinematicsManager.Instance != null)
{
GameManager.Instance.UnregisterPausableComponent(this);
CinematicsManager.Instance.OnCinematicStopped -= EndGame;
}
// Unregister all pausable components
_pausableComponents.Clear();
@@ -194,6 +192,8 @@ namespace Minigames.DivingForPictures
private void Update()
{
if(_settings == null) return;
_timeSinceLastSpawn += Time.deltaTime;
// Gradually increase spawn probability over time
@@ -662,18 +662,14 @@ namespace Minigames.DivingForPictures
// 1) Call the booster pack giver if available
bool completed = false;
var giver = UI.CardSystem.BoosterPackGiver.Instance;
var giver = UI.CardSystem.MinigameBoosterGiver.Instance;
if (giver != null)
{
// Temporarily subscribe to completion
UnityAction onDone = null;
onDone = () => { completed = true; giver.OnCompleted.RemoveListener(onDone); };
giver.OnCompleted.AddListener(onDone);
UIPageController.Instance.ShowAllUI();
giver.GiveBoosterPack();
giver.GiveBooster(() => { completed = true; });
// 2) Wait for it to finish (with a safety timeout in case it's not wired)
float timeout = 5f; // fallback to avoid blocking forever
float timeout = 10f; // fallback to avoid blocking forever
float elapsed = 0f;
while (!completed && elapsed < timeout)
{
@@ -684,7 +680,7 @@ namespace Minigames.DivingForPictures
else
{
// If no giver is present, proceed immediately
Logging.Debug("[DivingGameManager] BoosterPackGiver not found; skipping booster animation.");
Logging.Debug("[DivingGameManager] MinigameBoosterGiver not found; skipping booster animation.");
}
// 3) Only then show the game over screen