# 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
169 lines
6.2 KiB
C#
169 lines
6.2 KiB
C#
using UnityEngine;
|
|
using System.Collections;
|
|
using Core;
|
|
using Pathfinding;
|
|
|
|
// TODO: Remove movement based logic
|
|
public class AnneLiseBehaviour : MonoBehaviour
|
|
{
|
|
[SerializeField] public float moveSpeed;
|
|
private Animator animator;
|
|
private AIPath aiPath;
|
|
private bool hasArrived = false;
|
|
private LureSpot currentLureSpot;
|
|
private SpriteRenderer spriteRenderer; // Cached reference
|
|
private bool allowFacingByVelocity = true; // New flag
|
|
private Coroutine walkingCoroutine;
|
|
private bool annaLiseIsReady = false; // Flag to know if Anna Lise is ready to take the picture
|
|
|
|
private void Awake()
|
|
{
|
|
animator = GetComponentInChildren<Animator>();
|
|
aiPath = GetComponent<AIPath>();
|
|
spriteRenderer = GetComponentInChildren<SpriteRenderer>(); // Cache the reference
|
|
if (aiPath != null)
|
|
{
|
|
aiPath.maxSpeed = moveSpeed;
|
|
aiPath.OnTargetReachedEvent += HandleArriveAtSpot;
|
|
}
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (aiPath != null)
|
|
{
|
|
aiPath.OnTargetReachedEvent -= HandleArriveAtSpot;
|
|
}
|
|
}
|
|
|
|
public void TeleportJustOutOfView(Camera cam, float offset = 2f)
|
|
{
|
|
if (aiPath == null || cam == null || currentLureSpot == null || currentLureSpot.annaLiseSpot == null) return;
|
|
|
|
// Calculate direction from the target spot to Anna Lise's current position
|
|
Vector3 from = currentLureSpot.annaLiseSpot.transform.position;
|
|
Vector3 to = transform.position;
|
|
Vector3 direction = (to - from).normalized;
|
|
|
|
// Project the target spot to screen space
|
|
Vector3 targetScreen = cam.WorldToScreenPoint(from);
|
|
|
|
// Find the screen edge in the direction
|
|
Vector2 dir2D = new Vector2(direction.x, direction.y);
|
|
if (dir2D == Vector2.zero) dir2D = Vector2.right; // fallback
|
|
dir2D.Normalize();
|
|
|
|
// Calculate intersection with screen bounds
|
|
float tX = dir2D.x > 0 ? (Screen.width - targetScreen.x) / dir2D.x : (0 - targetScreen.x) / dir2D.x;
|
|
float tY = dir2D.y > 0 ? (Screen.height - targetScreen.y) / dir2D.y : (0 - targetScreen.y) / dir2D.y;
|
|
float t = Mathf.Min(Mathf.Abs(tX), Mathf.Abs(tY));
|
|
Vector2 edgeScreen = new Vector2(targetScreen.x, targetScreen.y) + dir2D * t;
|
|
edgeScreen += dir2D * offset; // Move outside the screen by offset
|
|
|
|
// Convert back to world position
|
|
Vector3 teleportWorld = cam.ScreenToWorldPoint(new Vector3(edgeScreen.x, edgeScreen.y, cam.WorldToScreenPoint(from).z));
|
|
teleportWorld.z = transform.position.z; // Keep original Z
|
|
|
|
aiPath.Teleport(teleportWorld, true);
|
|
}
|
|
|
|
public void GotoSpot(GameObject lurespot)
|
|
{
|
|
currentLureSpot = lurespot.GetComponent<LureSpot>();
|
|
// Teleport Anna Lise just out of view before moving
|
|
TeleportJustOutOfView(Camera.main, 2f);
|
|
|
|
if (aiPath == null) return;
|
|
|
|
aiPath.destination = currentLureSpot.annaLiseSpot.transform.position;
|
|
aiPath.canMove = true;
|
|
aiPath.SearchPath();
|
|
hasArrived = false;
|
|
allowFacingByVelocity = true;
|
|
if (walkingCoroutine != null)
|
|
{
|
|
StopCoroutine(walkingCoroutine);
|
|
}
|
|
walkingCoroutine = StartCoroutine(UpdateSpeedWhenWalking());
|
|
}
|
|
|
|
private IEnumerator UpdateSpeedWhenWalking()
|
|
{
|
|
while (!hasArrived && aiPath != null && animator != null)
|
|
{
|
|
float currentSpeed = aiPath.velocity.magnitude;
|
|
animator.SetFloat("speed", currentSpeed);
|
|
|
|
// Only allow facing by velocity if not arrived
|
|
if (allowFacingByVelocity && currentSpeed > 0.01f && spriteRenderer != null)
|
|
{
|
|
Vector3 velocity = aiPath.velocity;
|
|
if (velocity.x != 0)
|
|
{
|
|
Vector3 scale = spriteRenderer.transform.localScale;
|
|
scale.x = Mathf.Abs(scale.x) * (velocity.x > 0 ? 1 : -1);
|
|
spriteRenderer.transform.localScale = scale;
|
|
}
|
|
}
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
private void HandleArriveAtSpot()
|
|
{
|
|
if (hasArrived) return;
|
|
hasArrived = true;
|
|
allowFacingByVelocity = false; // Disable facing by velocity after arrival
|
|
aiPath.canMove = false;
|
|
if (walkingCoroutine != null)
|
|
{
|
|
StopCoroutine(walkingCoroutine);
|
|
walkingCoroutine = null;
|
|
}
|
|
// Face the "luredBird" of the current lurespot, if available
|
|
if (currentLureSpot != null)
|
|
{
|
|
if (currentLureSpot.luredBird != null)
|
|
{
|
|
FaceTarget(currentLureSpot.luredBird);
|
|
annaLiseIsReady = true; // Now Anna Lise is ready to take the picture
|
|
}
|
|
}
|
|
|
|
if (animator != null && currentLureSpot.name != "LureSpotB")// Horrible way to not take the photo if its Wolter
|
|
{
|
|
animator.SetTrigger("TakePhoto");
|
|
annaLiseIsReady = false; // Reset the flag after taking the photo
|
|
}
|
|
animator.SetFloat("speed", 0);
|
|
}
|
|
|
|
public void FaceTarget(GameObject target)
|
|
{
|
|
if (target == null || spriteRenderer == null) return;
|
|
|
|
// Compare X positions to determine facing direction
|
|
float direction = target.transform.position.x - transform.position.x;
|
|
if (Mathf.Abs(direction) > 0.01f) // Avoid flipping if almost aligned
|
|
{
|
|
Vector3 scale = spriteRenderer.transform.localScale;
|
|
scale.x = Mathf.Abs(scale.x) * (direction > 0 ? 1 : -1);
|
|
spriteRenderer.transform.localScale = scale;
|
|
}
|
|
}
|
|
public void TrafalgarTouchedAnnaLise()
|
|
{
|
|
if (annaLiseIsReady == true && currentLureSpot.name == "LureSpotB") // Only allow if Anna Lise is ready and it's the correct lure spot
|
|
{
|
|
// Trigger the photo taken animation
|
|
if (animator != null)
|
|
{
|
|
currentLureSpot.GetComponentInChildren<BirdEyesBehavior>().BirdReveal();
|
|
animator.SetTrigger("TakePhoto");
|
|
}
|
|
annaLiseIsReady = false; // Reset the flag after taking the photo
|
|
}
|
|
Logging.Debug("Trafalgar touched Anna Lise");
|
|
}
|
|
}
|