Pull from Michal Stuff
This commit is contained in:
@@ -159,72 +159,10 @@ public class GameManager : MonoBehaviour
|
||||
{
|
||||
return DeveloperSettingsProvider.Instance?.GetSettings<T>();
|
||||
}
|
||||
|
||||
// PLAYER & FOLLOWER SETTINGS
|
||||
|
||||
// Player settings
|
||||
public float MoveSpeed => GetSettings<IPlayerFollowerSettings>()?.MoveSpeed ?? 5f;
|
||||
public float StopDistance => GetSettings<IPlayerFollowerSettings>()?.StopDistance ?? 0.1f;
|
||||
public bool UseRigidbody => GetSettings<IPlayerFollowerSettings>()?.UseRigidbody ?? true;
|
||||
public HoldMovementMode DefaultHoldMovementMode =>
|
||||
GetSettings<IPlayerFollowerSettings>()?.DefaultHoldMovementMode ?? HoldMovementMode.Pathfinding;
|
||||
|
||||
// Follower settings
|
||||
public float FollowDistance => GetSettings<IPlayerFollowerSettings>()?.FollowDistance ?? 1.5f;
|
||||
public float ManualMoveSmooth => GetSettings<IPlayerFollowerSettings>()?.ManualMoveSmooth ?? 8f;
|
||||
public float ThresholdFar => GetSettings<IPlayerFollowerSettings>()?.ThresholdFar ?? 2.5f;
|
||||
public float ThresholdNear => GetSettings<IPlayerFollowerSettings>()?.ThresholdNear ?? 0.5f;
|
||||
public float StopThreshold => GetSettings<IPlayerFollowerSettings>()?.StopThreshold ?? 0.1f;
|
||||
public float FollowUpdateInterval => GetSettings<IPlayerFollowerSettings>()?.FollowUpdateInterval ?? 0.1f;
|
||||
public float FollowerSpeedMultiplier => GetSettings<IPlayerFollowerSettings>()?.FollowerSpeedMultiplier ?? 1.2f;
|
||||
public float HeldIconDisplayHeight => GetSettings<IPlayerFollowerSettings>()?.HeldIconDisplayHeight ?? 2.0f;
|
||||
|
||||
// INTERACTION SETTINGS
|
||||
|
||||
// LEFTOVER LEGACY SETTINGS
|
||||
public float PlayerStopDistance => GetSettings<IInteractionSettings>()?.PlayerStopDistance ?? 6.0f;
|
||||
public float PlayerStopDistanceDirectInteraction => GetSettings<IInteractionSettings>()?.PlayerStopDistanceDirectInteraction ?? 2.0f;
|
||||
public float FollowerPickupDelay => GetSettings<IInteractionSettings>()?.FollowerPickupDelay ?? 0.2f;
|
||||
public LayerMask InteractableLayerMask => GetSettings<IInteractionSettings>()?.InteractableLayerMask ?? -1;
|
||||
public GameObject BasePickupPrefab => GetSettings<IInteractionSettings>()?.BasePickupPrefab;
|
||||
public GameObject LevelSwitchMenuPrefab => GetSettings<IInteractionSettings>()?.LevelSwitchMenuPrefab;
|
||||
|
||||
// PUZZLE SETTINGS
|
||||
|
||||
public float DefaultPuzzlePromptRange => GetSettings<IInteractionSettings>()?.DefaultPuzzlePromptRange ?? 3.0f;
|
||||
public GameObject DefaultPuzzleIndicatorPrefab => GetSettings<IInteractionSettings>()?.DefaultPuzzleIndicatorPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the combination rule for two items, if any.
|
||||
/// </summary>
|
||||
public CombinationRule GetCombinationRule(PickupItemData item1, PickupItemData item2)
|
||||
{
|
||||
var settings = GetSettings<IInteractionSettings>();
|
||||
if (settings == null || settings.CombinationRules == null) return null;
|
||||
|
||||
foreach (var rule in settings.CombinationRules)
|
||||
{
|
||||
if ((PickupItemData.AreEquivalent(rule.itemA, item1) && PickupItemData.AreEquivalent(rule.itemB, item2)) ||
|
||||
(PickupItemData.AreEquivalent(rule.itemA, item2) && PickupItemData.AreEquivalent(rule.itemB, item1)))
|
||||
{
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the slot item config for a given slot item.
|
||||
/// </summary>
|
||||
public SlotItemConfig GetSlotItemConfig(PickupItemData slotItem)
|
||||
{
|
||||
var settings = GetSettings<IInteractionSettings>();
|
||||
if (settings == null || settings.SlotItemConfigs == null || slotItem == null) return null;
|
||||
|
||||
foreach (var config in settings.SlotItemConfigs)
|
||||
{
|
||||
if (PickupItemData.AreEquivalent(slotItem, config.slotItem))
|
||||
return config;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,5 +53,38 @@ namespace AppleHills.Core.Settings
|
||||
followerPickupDelay = Mathf.Max(0f, followerPickupDelay);
|
||||
defaultPuzzlePromptRange = Mathf.Max(0.1f, defaultPuzzlePromptRange);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the combination rule for two items, if any.
|
||||
/// </summary>
|
||||
public CombinationRule GetCombinationRule(PickupItemData item1, PickupItemData item2)
|
||||
{
|
||||
if (combinationRules == null) return null;
|
||||
|
||||
foreach (var rule in combinationRules)
|
||||
{
|
||||
if ((PickupItemData.AreEquivalent(rule.itemA, item1) && PickupItemData.AreEquivalent(rule.itemB, item2)) ||
|
||||
(PickupItemData.AreEquivalent(rule.itemA, item2) && PickupItemData.AreEquivalent(rule.itemB, item1)))
|
||||
{
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the slot item config for a given slot item.
|
||||
/// </summary>
|
||||
public SlotItemConfig GetSlotItemConfig(PickupItemData slotItem)
|
||||
{
|
||||
if (slotItemConfigs == null || slotItem == null) return null;
|
||||
|
||||
foreach (var config in slotItemConfigs)
|
||||
{
|
||||
if (PickupItemData.AreEquivalent(slotItem, config.slotItem))
|
||||
return config;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ namespace AppleHills.Core.Settings
|
||||
// Puzzle settings
|
||||
GameObject DefaultPuzzleIndicatorPrefab { get; }
|
||||
float DefaultPuzzlePromptRange { get; }
|
||||
|
||||
// Methods to query item configurations
|
||||
CombinationRule GetCombinationRule(PickupItemData item1, PickupItemData item2);
|
||||
SlotItemConfig GetSlotItemConfig(PickupItemData slotItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,6 +3,7 @@ using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.SceneManagement;
|
||||
using AppleHills.Core.Settings; // Added for IInteractionSettings
|
||||
|
||||
namespace Input
|
||||
{
|
||||
@@ -44,6 +45,9 @@ namespace Input
|
||||
}
|
||||
}
|
||||
|
||||
// Settings reference
|
||||
private IInteractionSettings _interactionSettings;
|
||||
|
||||
private PlayerInput playerInput;
|
||||
private InputAction tapMoveAction;
|
||||
private InputAction holdMoveAction;
|
||||
@@ -55,6 +59,10 @@ namespace Input
|
||||
{
|
||||
_instance = this;
|
||||
// DontDestroyOnLoad(gameObject);
|
||||
|
||||
// Initialize settings reference
|
||||
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
|
||||
|
||||
playerInput = GetComponent<PlayerInput>();
|
||||
if (playerInput == null)
|
||||
{
|
||||
@@ -217,7 +225,7 @@ namespace Input
|
||||
/// </summary>
|
||||
private bool TryDelegateToInteractable(Vector2 worldPos)
|
||||
{
|
||||
LayerMask mask = GameManager.Instance != null ? GameManager.Instance.InteractableLayerMask : -1;
|
||||
LayerMask mask = _interactionSettings != null ? _interactionSettings.InteractableLayerMask : -1;
|
||||
Collider2D hit = Physics2D.OverlapPoint(worldPos, mask);
|
||||
if (hit != null)
|
||||
{
|
||||
|
||||
58
Assets/Scripts/Interactions/CharacterMoveToTarget.cs
Normal file
58
Assets/Scripts/Interactions/CharacterMoveToTarget.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a target position for character movement during interaction.
|
||||
/// Attach this to an interactable object's child to specify where
|
||||
/// characters should move during interaction rather than using the default calculations.
|
||||
/// </summary>
|
||||
public class CharacterMoveToTarget : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Which character this target position is for")]
|
||||
public CharacterToInteract characterType = CharacterToInteract.Pulver;
|
||||
|
||||
[Tooltip("Optional offset from this transform's position")]
|
||||
public Vector3 positionOffset = Vector3.zero;
|
||||
|
||||
/// <summary>
|
||||
/// Get the target position for this character to move to
|
||||
/// </summary>
|
||||
public Vector3 GetTargetPosition()
|
||||
{
|
||||
return transform.position + positionOffset;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
// Draw a different colored sphere based on which character this target is for
|
||||
switch (characterType)
|
||||
{
|
||||
case CharacterToInteract.Trafalgar:
|
||||
Gizmos.color = new Color(0f, 0.5f, 1f, 0.8f); // Blue for player
|
||||
break;
|
||||
case CharacterToInteract.Pulver:
|
||||
Gizmos.color = new Color(1f, 0.5f, 0f, 0.8f); // Orange for follower
|
||||
break;
|
||||
case CharacterToInteract.Both:
|
||||
Gizmos.color = new Color(0.7f, 0f, 0.7f, 0.8f); // Purple for both
|
||||
break;
|
||||
default:
|
||||
Gizmos.color = new Color(0.5f, 0.5f, 0.5f, 0.8f); // Gray for none
|
||||
break;
|
||||
}
|
||||
|
||||
Vector3 targetPos = GetTargetPosition();
|
||||
Gizmos.DrawSphere(targetPos, 0.2f);
|
||||
|
||||
// Draw a line from the parent interactable to this target
|
||||
Interactable parentInteractable = GetComponentInParent<Interactable>();
|
||||
if (parentInteractable != null)
|
||||
{
|
||||
Gizmos.DrawLine(parentInteractable.transform.position, targetPos);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bcf343b3ef74f0fb3c64be6fd2893b6
|
||||
timeCreated: 1759744130
|
||||
@@ -1,22 +1,27 @@
|
||||
using Input;
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.Events;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
public enum CharacterToInteract
|
||||
{
|
||||
None,
|
||||
Trafalgar,
|
||||
Pulver
|
||||
Pulver,
|
||||
Both
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an interactable object that can respond to tap input events.
|
||||
/// </summary>
|
||||
public class Interactable : MonoBehaviour, ITouchInputConsumer
|
||||
{
|
||||
[Header("Interaction Settings")]
|
||||
public bool isOneTime = false;
|
||||
public bool isOneTime;
|
||||
public float cooldown = -1f;
|
||||
public CharacterToInteract characterToInteract = CharacterToInteract.Pulver;
|
||||
|
||||
@@ -30,15 +35,64 @@ namespace Interactions
|
||||
private bool _interactionInProgress;
|
||||
private PlayerTouchController _playerRef;
|
||||
private FollowerController _followerController;
|
||||
|
||||
private bool _isActive = true;
|
||||
private InteractionEventType _currentEventType;
|
||||
|
||||
// Action component system
|
||||
private List<InteractionActionBase> _registeredActions = new List<InteractionActionBase>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Subscribe to interactionComplete event
|
||||
interactionComplete.AddListener(OnInteractionComplete);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register an action component with this interactable
|
||||
/// </summary>
|
||||
public void RegisterAction(InteractionActionBase action)
|
||||
{
|
||||
if (!_registeredActions.Contains(action))
|
||||
{
|
||||
_registeredActions.Add(action);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister an action component from this interactable
|
||||
/// </summary>
|
||||
public void UnregisterAction(InteractionActionBase action)
|
||||
{
|
||||
_registeredActions.Remove(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatch an interaction event to all registered actions and await their completion
|
||||
/// </summary>
|
||||
private async Task DispatchEventAsync(InteractionEventType eventType)
|
||||
{
|
||||
_currentEventType = eventType;
|
||||
|
||||
// Collect all tasks from actions that want to respond
|
||||
List<Task<bool>> tasks = new List<Task<bool>>();
|
||||
|
||||
foreach (var action in _registeredActions)
|
||||
{
|
||||
Task<bool> task = action.OnInteractionEvent(eventType, _playerRef, _followerController);
|
||||
if (task != null)
|
||||
{
|
||||
tasks.Add(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.Count > 0)
|
||||
{
|
||||
// Wait for all tasks to complete
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
// If no tasks were added, the method will complete immediately (no need for await)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles tap input. Triggers interaction logic.
|
||||
/// </summary>
|
||||
@@ -50,11 +104,12 @@ namespace Interactions
|
||||
return;
|
||||
}
|
||||
Debug.Log($"[Interactable] OnTap at {worldPosition} on {gameObject.name}");
|
||||
// Broadcast interaction started event
|
||||
TryInteract();
|
||||
|
||||
// Start the interaction process asynchronously
|
||||
_ = TryInteractAsync();
|
||||
}
|
||||
|
||||
public void TryInteract()
|
||||
private async Task TryInteractAsync()
|
||||
{
|
||||
_interactionInProgress = true;
|
||||
|
||||
@@ -63,68 +118,302 @@ namespace Interactions
|
||||
|
||||
interactionStarted?.Invoke(_playerRef, _followerController);
|
||||
|
||||
// Dispatch the InteractionStarted event to action components
|
||||
await DispatchEventAsync(InteractionEventType.InteractionStarted);
|
||||
|
||||
// After all InteractionStarted actions complete, proceed to player movement
|
||||
await StartPlayerMovementAsync();
|
||||
}
|
||||
|
||||
private async Task StartPlayerMovementAsync()
|
||||
{
|
||||
if (_playerRef == null)
|
||||
{
|
||||
Debug.Log($"[Interactable] Player character could not be found. Aborting interaction.");
|
||||
interactionInterrupted.Invoke();
|
||||
await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
|
||||
return;
|
||||
}
|
||||
|
||||
// If characterToInteract is None, immediately trigger the characterArrived event
|
||||
if (characterToInteract == CharacterToInteract.None)
|
||||
{
|
||||
await BroadcastCharacterArrivedAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute closest point on the interaction radius
|
||||
Vector3 interactablePos = transform.position;
|
||||
Vector3 playerPos = _playerRef.transform.position;
|
||||
float stopDistance = characterToInteract == CharacterToInteract.Pulver
|
||||
? GameManager.Instance.PlayerStopDistance
|
||||
: GameManager.Instance.PlayerStopDistanceDirectInteraction;
|
||||
Vector3 toPlayer = (playerPos - interactablePos).normalized;
|
||||
Vector3 stopPoint = interactablePos + toPlayer * stopDistance;
|
||||
// Check for a CharacterMoveToTarget component for Trafalgar (player) or Both
|
||||
Vector3 stopPoint;
|
||||
bool customTargetFound = false;
|
||||
|
||||
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
// Target is valid if it matches Trafalgar specifically or is set to Both
|
||||
if (target.characterType == CharacterToInteract.Trafalgar || target.characterType == CharacterToInteract.Both)
|
||||
{
|
||||
stopPoint = target.GetTargetPosition();
|
||||
customTargetFound = true;
|
||||
|
||||
// We need to wait for the player to arrive, so use a TaskCompletionSource
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
// Use local functions instead of circular lambda references
|
||||
void OnPlayerArrivedLocal()
|
||||
{
|
||||
// First remove both event handlers to prevent memory leaks
|
||||
if (_playerRef != null)
|
||||
{
|
||||
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
|
||||
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
|
||||
}
|
||||
|
||||
// Then continue with the interaction flow
|
||||
OnPlayerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
|
||||
}
|
||||
|
||||
void OnPlayerMoveCancelledLocal()
|
||||
{
|
||||
// First remove both event handlers to prevent memory leaks
|
||||
if (_playerRef != null)
|
||||
{
|
||||
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
|
||||
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
|
||||
}
|
||||
|
||||
// Then handle the cancellation
|
||||
OnPlayerMoveCancelledAsync().ContinueWith(_ => tcs.TrySetResult(false));
|
||||
}
|
||||
|
||||
// Unsubscribe previous handlers (if any)
|
||||
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
|
||||
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
|
||||
|
||||
// Subscribe our new handlers
|
||||
_playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal;
|
||||
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal;
|
||||
|
||||
// Start the player movement
|
||||
_playerRef.MoveToAndNotify(stopPoint);
|
||||
|
||||
// Await player arrival
|
||||
await tcs.Task;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If no custom target was found, use the default behavior
|
||||
if (!customTargetFound)
|
||||
{
|
||||
// Compute closest point on the interaction radius
|
||||
Vector3 interactablePos = transform.position;
|
||||
Vector3 playerPos = _playerRef.transform.position;
|
||||
float stopDistance = characterToInteract == CharacterToInteract.Pulver
|
||||
? GameManager.Instance.PlayerStopDistance
|
||||
: GameManager.Instance.PlayerStopDistanceDirectInteraction;
|
||||
Vector3 toPlayer = (playerPos - interactablePos).normalized;
|
||||
stopPoint = interactablePos + toPlayer * stopDistance;
|
||||
|
||||
// Unsubscribe previous to avoid duplicate calls
|
||||
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
|
||||
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
|
||||
_playerRef.OnArrivedAtTarget += OnPlayerArrived;
|
||||
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelled;
|
||||
_playerRef.MoveToAndNotify(stopPoint);
|
||||
// We need to wait for the player to arrive, so use a TaskCompletionSource
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
// Use local functions instead of circular lambda references
|
||||
void OnPlayerArrivedLocal()
|
||||
{
|
||||
// First remove both event handlers to prevent memory leaks
|
||||
if (_playerRef != null)
|
||||
{
|
||||
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
|
||||
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
|
||||
}
|
||||
|
||||
// Then continue with the interaction flow
|
||||
OnPlayerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
|
||||
}
|
||||
|
||||
void OnPlayerMoveCancelledLocal()
|
||||
{
|
||||
// First remove both event handlers to prevent memory leaks
|
||||
if (_playerRef != null)
|
||||
{
|
||||
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
|
||||
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
|
||||
}
|
||||
|
||||
// Then handle the cancellation
|
||||
OnPlayerMoveCancelledAsync().ContinueWith(_ => tcs.TrySetResult(false));
|
||||
}
|
||||
|
||||
// Unsubscribe previous handlers (if any)
|
||||
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
|
||||
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
|
||||
|
||||
// Subscribe our new handlers
|
||||
_playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal;
|
||||
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal;
|
||||
|
||||
// Start the player movement
|
||||
_playerRef.MoveToAndNotify(stopPoint);
|
||||
|
||||
// Await player arrival
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerMoveCancelled()
|
||||
private async Task OnPlayerMoveCancelledAsync()
|
||||
{
|
||||
_interactionInProgress = false;
|
||||
interactionInterrupted?.Invoke();
|
||||
await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
|
||||
}
|
||||
|
||||
private void OnPlayerArrived()
|
||||
private async Task OnPlayerArrivedAsync()
|
||||
{
|
||||
if (!_interactionInProgress)
|
||||
return;
|
||||
|
||||
// Unsubscribe to avoid memory leaks
|
||||
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
|
||||
// Dispatch PlayerArrived event
|
||||
await DispatchEventAsync(InteractionEventType.PlayerArrived);
|
||||
|
||||
// After all PlayerArrived actions complete, proceed to character interaction
|
||||
await HandleCharacterInteractionAsync();
|
||||
}
|
||||
|
||||
private async Task HandleCharacterInteractionAsync()
|
||||
{
|
||||
if (characterToInteract == CharacterToInteract.Pulver)
|
||||
{
|
||||
_followerController.OnPickupArrived -= OnFollowerArrived;
|
||||
_followerController.OnPickupArrived += OnFollowerArrived;
|
||||
_followerController.GoToPointAndReturn(transform.position, _playerRef.transform);
|
||||
// We need to wait for the follower to arrive, so use a TaskCompletionSource
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
// Create a proper local function for the event handler
|
||||
void OnFollowerArrivedLocal()
|
||||
{
|
||||
// First remove the event handler to prevent memory leaks
|
||||
if (_followerController != null)
|
||||
{
|
||||
_followerController.OnPickupArrived -= OnFollowerArrivedLocal;
|
||||
}
|
||||
|
||||
// Then continue with the interaction flow
|
||||
OnFollowerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
|
||||
}
|
||||
|
||||
// Register our new local function handler
|
||||
_followerController.OnPickupArrived += OnFollowerArrivedLocal;
|
||||
|
||||
// Check for a CharacterMoveToTarget component for Pulver or Both
|
||||
Vector3 targetPosition = transform.position;
|
||||
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both)
|
||||
{
|
||||
targetPosition = target.GetTargetPosition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the new GoToPoint method instead of GoToPointAndReturn
|
||||
_followerController.GoToPoint(targetPosition);
|
||||
|
||||
// Await follower arrival
|
||||
await tcs.Task;
|
||||
}
|
||||
else if (characterToInteract == CharacterToInteract.Trafalgar)
|
||||
{
|
||||
BroadcastCharacterArrived();
|
||||
await BroadcastCharacterArrivedAsync();
|
||||
}
|
||||
else if (characterToInteract == CharacterToInteract.Both)
|
||||
{
|
||||
// We need to wait for the follower to arrive, so use a TaskCompletionSource
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
// Create a proper local function for the event handler
|
||||
void OnFollowerArrivedLocal()
|
||||
{
|
||||
// First remove the event handler to prevent memory leaks
|
||||
if (_followerController != null)
|
||||
{
|
||||
_followerController.OnPickupArrived -= OnFollowerArrivedLocal;
|
||||
}
|
||||
|
||||
// Then continue with the interaction flow
|
||||
OnFollowerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
|
||||
}
|
||||
|
||||
// Register our new local function handler
|
||||
_followerController.OnPickupArrived += OnFollowerArrivedLocal;
|
||||
|
||||
// Check for a CharacterMoveToTarget component for Pulver or Both
|
||||
Vector3 targetPosition = transform.position;
|
||||
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both)
|
||||
{
|
||||
targetPosition = target.GetTargetPosition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the new GoToPoint method instead of GoToPointAndReturn
|
||||
_followerController.GoToPoint(targetPosition);
|
||||
|
||||
// Await follower arrival
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFollowerArrived()
|
||||
private async Task OnFollowerArrivedAsync()
|
||||
{
|
||||
if (!_interactionInProgress)
|
||||
return;
|
||||
|
||||
// Unsubscribe to avoid memory leaks
|
||||
_followerController.OnPickupArrived -= OnFollowerArrived;
|
||||
// Dispatch InteractingCharacterArrived event and WAIT for all actions to complete
|
||||
// This ensures we wait for any timeline animations to finish before proceeding
|
||||
Debug.Log("[Interactable] Follower arrived, dispatching InteractingCharacterArrived event and waiting for completion");
|
||||
await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
|
||||
Debug.Log("[Interactable] All InteractingCharacterArrived actions completed, proceeding with interaction");
|
||||
|
||||
BroadcastCharacterArrived();
|
||||
// Check if we have any components that might have paused the interaction flow
|
||||
bool hasTimelineActions = false;
|
||||
foreach (var action in _registeredActions)
|
||||
{
|
||||
if (action is InteractionTimelineAction timelineAction &&
|
||||
timelineAction.respondToEvents.Contains(InteractionEventType.InteractingCharacterArrived) &&
|
||||
timelineAction.pauseInteractionFlow)
|
||||
{
|
||||
hasTimelineActions = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the follower to return to the player
|
||||
if (_followerController != null && _playerRef != null)
|
||||
{
|
||||
_followerController.ReturnToPlayer(_playerRef.transform);
|
||||
}
|
||||
|
||||
// After all InteractingCharacterArrived actions complete, proceed to character arrived
|
||||
await BroadcastCharacterArrivedAsync();
|
||||
}
|
||||
|
||||
private void BroadcastCharacterArrived()
|
||||
// Legacy non-async method to maintain compatibility with existing code
|
||||
private void OnPlayerArrived()
|
||||
{
|
||||
// This is now just a wrapper for the async version
|
||||
_ = OnPlayerArrivedAsync();
|
||||
}
|
||||
|
||||
// Legacy non-async method to maintain compatibility with existing code
|
||||
private void OnPlayerMoveCancelled()
|
||||
{
|
||||
// This is now just a wrapper for the async version
|
||||
_ = OnPlayerMoveCancelledAsync();
|
||||
}
|
||||
|
||||
private async Task BroadcastCharacterArrivedAsync()
|
||||
{
|
||||
// Check for ObjectiveStepBehaviour and lock state
|
||||
var step = GetComponent<PuzzleS.ObjectiveStepBehaviour>();
|
||||
@@ -138,16 +427,24 @@ namespace Interactions
|
||||
_followerController = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Dispatch CharacterArrived event
|
||||
// await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
|
||||
|
||||
// Broadcast appropriate event
|
||||
characterArrived?.Invoke();
|
||||
|
||||
// Reset variables for next time
|
||||
_interactionInProgress = false;
|
||||
_playerRef = null;
|
||||
_followerController = null;
|
||||
}
|
||||
|
||||
private void OnInteractionComplete(bool success)
|
||||
private async void OnInteractionComplete(bool success)
|
||||
{
|
||||
// Dispatch InteractionComplete event
|
||||
await DispatchEventAsync(InteractionEventType.InteractionComplete);
|
||||
|
||||
if (success)
|
||||
{
|
||||
if (isOneTime)
|
||||
|
||||
91
Assets/Scripts/Interactions/InteractionActionBase.cs
Normal file
91
Assets/Scripts/Interactions/InteractionActionBase.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Input;
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all interaction action components
|
||||
/// These components respond to interaction events and can control the interaction flow
|
||||
/// </summary>
|
||||
public abstract class InteractionActionBase : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Which interaction events this action should respond to")]
|
||||
public List<InteractionEventType> respondToEvents = new List<InteractionEventType>();
|
||||
|
||||
[Tooltip("Whether the interaction flow should wait for this action to complete")]
|
||||
public bool pauseInteractionFlow = true;
|
||||
|
||||
protected Interactable parentInteractable;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
// Get the parent interactable component
|
||||
parentInteractable = GetComponentInParent<Interactable>();
|
||||
|
||||
if (parentInteractable == null)
|
||||
{
|
||||
Debug.LogError($"[{GetType().Name}] Cannot find parent Interactable component!");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
if (parentInteractable != null)
|
||||
{
|
||||
parentInteractable.RegisterAction(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
if (parentInteractable != null)
|
||||
{
|
||||
parentInteractable.UnregisterAction(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an interaction event occurs that this action is registered for
|
||||
/// </summary>
|
||||
/// <param name="eventType">The type of event that occurred</param>
|
||||
/// <returns>A task that completes when the action is finished, or null if action won't execute</returns>
|
||||
public Task<bool> OnInteractionEvent(InteractionEventType eventType, PlayerTouchController player, FollowerController follower)
|
||||
{
|
||||
if (respondToEvents.Contains(eventType) && ShouldExecute(eventType, player, follower))
|
||||
{
|
||||
if (pauseInteractionFlow)
|
||||
{
|
||||
return ExecuteAsync(eventType, player, follower);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we don't need to pause the flow, execute in the background
|
||||
// and return a completed task
|
||||
_ = ExecuteAsync(eventType, player, follower);
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute the action for the given event asynchronously
|
||||
/// </summary>
|
||||
protected abstract Task<bool> ExecuteAsync(InteractionEventType eventType, PlayerTouchController player, FollowerController follower);
|
||||
|
||||
/// <summary>
|
||||
/// Called to determine if this action should execute for the given event
|
||||
/// Override this to add additional conditions for execution
|
||||
/// </summary>
|
||||
protected virtual bool ShouldExecute(InteractionEventType eventType, PlayerTouchController player, FollowerController follower)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cf351d32dac4169a9db20609727a70f
|
||||
timeCreated: 1759746705
|
||||
16
Assets/Scripts/Interactions/InteractionEventType.cs
Normal file
16
Assets/Scripts/Interactions/InteractionEventType.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the different types of events that can occur during an interaction
|
||||
/// </summary>
|
||||
public enum InteractionEventType
|
||||
{
|
||||
InteractionStarted,
|
||||
PlayerArrived,
|
||||
InteractingCharacterArrived,
|
||||
InteractionComplete,
|
||||
InteractionInterrupted
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Interactions/InteractionEventType.cs.meta
Normal file
3
Assets/Scripts/Interactions/InteractionEventType.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78684d31bd4d4636834a494c7cb74f48
|
||||
timeCreated: 1759746690
|
||||
258
Assets/Scripts/Interactions/InteractionTimelineAction.cs
Normal file
258
Assets/Scripts/Interactions/InteractionTimelineAction.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Input;
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that plays timeline animations in response to interaction events
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(PlayableDirector))]
|
||||
public class InteractionTimelineAction : InteractionActionBase
|
||||
{
|
||||
[System.Serializable]
|
||||
public class TimelineEventMapping
|
||||
{
|
||||
public InteractionEventType eventType;
|
||||
public PlayableAsset[] timelines;
|
||||
|
||||
[Tooltip("Whether to bind the player character to the track named 'Player'")]
|
||||
public bool bindPlayerCharacter = false;
|
||||
|
||||
[Tooltip("Whether to bind the follower character to the track named 'Pulver'")]
|
||||
public bool bindPulverCharacter = false;
|
||||
|
||||
[Tooltip("Custom track name for player character binding")]
|
||||
public string playerTrackName = "Player";
|
||||
|
||||
[Tooltip("Custom track name for follower character binding")]
|
||||
public string pulverTrackName = "Pulver";
|
||||
|
||||
[Tooltip("Time in seconds before the timeline is automatically completed (safety feature)")]
|
||||
public float timeoutSeconds = 30f;
|
||||
|
||||
[Tooltip("Whether to loop the last timeline in the sequence")]
|
||||
public bool loopLast = false;
|
||||
|
||||
[Tooltip("Whether to loop through all timelines in the sequence")]
|
||||
public bool loopAll = false;
|
||||
|
||||
// Helper property to check if we have valid timelines
|
||||
public bool HasValidTimelines => timelines != null && timelines.Length > 0 && timelines[0] != null;
|
||||
}
|
||||
|
||||
[Header("Timeline Configuration")] [SerializeField]
|
||||
private PlayableDirector playableDirector;
|
||||
|
||||
[SerializeField] private TimelineEventMapping[] timelineMappings;
|
||||
|
||||
private TaskCompletionSource<bool> _currentPlaybackTCS;
|
||||
private int _currentTimelineIndex = 0;
|
||||
private TimelineEventMapping _currentMapping = null;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
if (playableDirector == null)
|
||||
{
|
||||
playableDirector = GetComponent<PlayableDirector>();
|
||||
}
|
||||
|
||||
if (playableDirector == null)
|
||||
{
|
||||
Debug.LogError("[InteractionTimelineAction] PlayableDirector component is missing!");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to the director's stopped event
|
||||
playableDirector.stopped += OnPlayableDirectorStopped;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (playableDirector != null)
|
||||
{
|
||||
playableDirector.stopped -= OnPlayableDirectorStopped;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task<bool> ExecuteAsync(InteractionEventType eventType, PlayerTouchController player,
|
||||
FollowerController follower)
|
||||
{
|
||||
// Find the timeline for this event type
|
||||
TimelineEventMapping mapping = Array.Find(timelineMappings, m => m.eventType == eventType);
|
||||
|
||||
if (mapping == null || !mapping.HasValidTimelines)
|
||||
{
|
||||
// No timeline configured for this event
|
||||
return true;
|
||||
}
|
||||
|
||||
_currentMapping = mapping;
|
||||
// _currentTimelineIndex = 0;
|
||||
|
||||
return await PlayTimelineSequence(player, follower);
|
||||
}
|
||||
|
||||
private async Task<bool> PlayTimelineSequence(PlayerTouchController player, FollowerController follower)
|
||||
{
|
||||
if (_currentMapping == null || !_currentMapping.HasValidTimelines)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
follower.DropHeldItemAt(follower.transform.position);
|
||||
|
||||
// Play the current timeline in the sequence
|
||||
bool result = await PlaySingleTimeline(_currentMapping.timelines[_currentTimelineIndex], _currentMapping, player, follower);
|
||||
|
||||
// Return false if the playback failed
|
||||
if (!result)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Increment the timeline index for next playback
|
||||
_currentTimelineIndex++;
|
||||
|
||||
// Check if we've reached the end of the sequence
|
||||
if (_currentTimelineIndex >= _currentMapping.timelines.Length)
|
||||
{
|
||||
// If loop all is enabled, start over
|
||||
if (_currentMapping.loopAll)
|
||||
{
|
||||
_currentTimelineIndex = 0;
|
||||
// Don't continue automatically, wait for next interaction
|
||||
return true;
|
||||
}
|
||||
// If loop last is enabled, replay the last timeline
|
||||
else if (_currentMapping.loopLast)
|
||||
{
|
||||
_currentTimelineIndex = _currentMapping.timelines.Length - 1;
|
||||
// Don't continue automatically, wait for next interaction
|
||||
return true;
|
||||
}
|
||||
// Otherwise, we're done with the sequence
|
||||
else
|
||||
{
|
||||
_currentTimelineIndex = 0;
|
||||
_currentMapping = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have more timelines in the sequence, we're done for now
|
||||
// Next interaction will pick up where we left off
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> PlaySingleTimeline(PlayableAsset timelineAsset, TimelineEventMapping mapping,
|
||||
PlayerTouchController player, FollowerController follower)
|
||||
{
|
||||
if (timelineAsset == null)
|
||||
{
|
||||
Debug.LogWarning("[InteractionTimelineAction] Timeline asset is null");
|
||||
return true; // Return true to continue the interaction flow
|
||||
}
|
||||
|
||||
// Set the timeline asset
|
||||
playableDirector.playableAsset = timelineAsset;
|
||||
|
||||
// Bind characters if needed
|
||||
if (mapping.bindPlayerCharacter && player != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var trackOutput = playableDirector.playableAsset.outputs.FirstOrDefault(o => o.streamName == mapping.playerTrackName);
|
||||
if (trackOutput.sourceObject != null)
|
||||
{
|
||||
playableDirector.SetGenericBinding(trackOutput.sourceObject, player.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[InteractionTimelineAction] Could not find track named '{mapping.playerTrackName}' for player binding");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[InteractionTimelineAction] Error binding player to timeline: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping.bindPulverCharacter && follower != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var trackOutput = playableDirector.playableAsset.outputs.FirstOrDefault(o => o.streamName == mapping.pulverTrackName);
|
||||
if (trackOutput.sourceObject != null)
|
||||
{
|
||||
playableDirector.SetGenericBinding(trackOutput.sourceObject, follower.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[InteractionTimelineAction] Could not find track named '{mapping.pulverTrackName}' for follower binding");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[InteractionTimelineAction] Error binding follower to timeline: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Create a task completion source to await the timeline completion
|
||||
_currentPlaybackTCS = new TaskCompletionSource<bool>();
|
||||
|
||||
// Register for the stopped event if not already registered
|
||||
playableDirector.stopped -= OnPlayableDirectorStopped;
|
||||
playableDirector.stopped += OnPlayableDirectorStopped;
|
||||
|
||||
// Log the timeline playback
|
||||
Debug.Log($"[InteractionTimelineAction] Playing timeline {timelineAsset.name} for event {mapping.eventType}");
|
||||
|
||||
// Play the timeline
|
||||
playableDirector.Play();
|
||||
|
||||
// Start a timeout coroutine for safety using the mapping's timeout
|
||||
StartCoroutine(TimeoutCoroutine(mapping.timeoutSeconds));
|
||||
|
||||
// Await the timeline completion (will be signaled by the OnPlayableDirectorStopped callback)
|
||||
bool result = await _currentPlaybackTCS.Task;
|
||||
|
||||
// Log completion
|
||||
Debug.Log($"[InteractionTimelineAction] Timeline {timelineAsset.name} playback completed with result: {result}");
|
||||
|
||||
// Clear the task completion source
|
||||
_currentPlaybackTCS = null;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnPlayableDirectorStopped(PlayableDirector director)
|
||||
{
|
||||
if (director != playableDirector || _currentPlaybackTCS == null)
|
||||
return;
|
||||
|
||||
Debug.Log($"[InteractionTimelineAction] PlayableDirector stopped. Signaling completion.");
|
||||
|
||||
// Signal completion when the director stops
|
||||
_currentPlaybackTCS.TrySetResult(true);
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator TimeoutCoroutine(float timeoutDuration)
|
||||
{
|
||||
yield return new WaitForSeconds(timeoutDuration);
|
||||
|
||||
// If the TCS still exists after timeout, complete it with failure
|
||||
if (_currentPlaybackTCS != null)
|
||||
{
|
||||
Debug.LogWarning($"[InteractionTimelineAction] Timeline playback timed out after {timeoutDuration} seconds");
|
||||
_currentPlaybackTCS.TrySetResult(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42e77a0c97604b6eb7674e58726c831a
|
||||
timeCreated: 1759746720
|
||||
@@ -3,6 +3,7 @@ using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using System; // for Action<T>
|
||||
using Core; // register with ItemManager
|
||||
using AppleHills.Core.Settings; // Added for IInteractionSettings
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
@@ -24,6 +25,10 @@ namespace Interactions
|
||||
// Tracks the current state of the slotted item
|
||||
private ItemSlotState _currentState = ItemSlotState.None;
|
||||
|
||||
// Settings reference
|
||||
private IInteractionSettings _interactionSettings;
|
||||
private IPlayerFollowerSettings _playerFollowerSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only access to the current slotted item state.
|
||||
/// </summary>
|
||||
@@ -64,13 +69,22 @@ namespace Interactions
|
||||
}
|
||||
}
|
||||
|
||||
public override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
// Initialize settings references
|
||||
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
|
||||
_playerFollowerSettings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
|
||||
}
|
||||
|
||||
protected override void OnCharacterArrived()
|
||||
{
|
||||
Debug.Log("[ItemSlot] OnCharacterArrived");
|
||||
|
||||
var heldItemData = FollowerController.CurrentlyHeldItemData;
|
||||
var heldItemObj = FollowerController.GetHeldPickupObject();
|
||||
var config = GameManager.Instance.GetSlotItemConfig(itemData);
|
||||
var config = _interactionSettings?.GetSlotItemConfig(itemData);
|
||||
var forbidden = config?.forbiddenItems ?? new List<PickupItemData>();
|
||||
|
||||
// Held item, slot empty -> try to slot item
|
||||
@@ -120,7 +134,7 @@ namespace Interactions
|
||||
{
|
||||
slottedItemRenderer.sprite = _currentlySlottedItemData.mapSprite;
|
||||
// Scale sprite to desired height, preserve aspect ratio, compensate for parent scale
|
||||
float desiredHeight = GameManager.Instance.HeldIconDisplayHeight;
|
||||
float desiredHeight = _playerFollowerSettings?.HeldIconDisplayHeight ?? 2.0f;
|
||||
var sprite = _currentlySlottedItemData.mapSprite;
|
||||
float spriteHeight = sprite.bounds.size.y;
|
||||
float spriteWidth = sprite.bounds.size.x;
|
||||
@@ -175,7 +189,7 @@ namespace Interactions
|
||||
|
||||
// Once an item is slotted, we know it is not forbidden, so we can skip that check, but now check if it was
|
||||
// the correct item we're looking for
|
||||
var config = GameManager.Instance.GetSlotItemConfig(itemData);
|
||||
var config = _interactionSettings?.GetSlotItemConfig(itemData);
|
||||
var allowed = config?.allowedItems ?? new List<PickupItemData>();
|
||||
if (itemToSlotData != null && PickupItemData.ListContainsEquivalent(allowed, itemToSlotData))
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Interactions
|
||||
/// <summary>
|
||||
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
|
||||
/// </summary>
|
||||
void Awake()
|
||||
public virtual void Awake()
|
||||
{
|
||||
if (iconRenderer == null)
|
||||
iconRenderer = GetComponent<SpriteRenderer>();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Input;
|
||||
using Interactions;
|
||||
using UnityEngine;
|
||||
using AppleHills.Core.Settings; // Added for IInteractionSettings
|
||||
|
||||
/// <summary>
|
||||
/// Handles level switching when interacted with. Applies switch data and triggers scene transitions.
|
||||
@@ -14,6 +15,9 @@ public class LevelSwitch : MonoBehaviour
|
||||
public LevelSwitchData switchData;
|
||||
private SpriteRenderer _iconRenderer;
|
||||
private Interactable _interactable;
|
||||
|
||||
// Settings reference
|
||||
private IInteractionSettings _interactionSettings;
|
||||
|
||||
private bool _isActive = true;
|
||||
|
||||
@@ -30,6 +34,10 @@ public class LevelSwitch : MonoBehaviour
|
||||
{
|
||||
_interactable.characterArrived.AddListener(OnCharacterArrived);
|
||||
}
|
||||
|
||||
// Initialize settings reference
|
||||
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
|
||||
|
||||
ApplySwitchData();
|
||||
}
|
||||
|
||||
@@ -78,10 +86,10 @@ public class LevelSwitch : MonoBehaviour
|
||||
if (switchData == null || string.IsNullOrEmpty(switchData.targetLevelSceneName) || !_isActive)
|
||||
return;
|
||||
|
||||
var menuPrefab = GameManager.Instance.LevelSwitchMenuPrefab;
|
||||
var menuPrefab = _interactionSettings?.LevelSwitchMenuPrefab;
|
||||
if (menuPrefab == null)
|
||||
{
|
||||
Debug.LogError("LevelSwitchMenu prefab not assigned in GameSettings!");
|
||||
Debug.LogError("LevelSwitchMenu prefab not assigned in InteractionSettings!");
|
||||
return;
|
||||
}
|
||||
// Spawn the menu overlay (assume Canvas parent is handled in prefab setup)
|
||||
|
||||
@@ -3,6 +3,7 @@ using UnityEngine;
|
||||
using Pathfinding;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Utils;
|
||||
using AppleHills.Core.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Controls the follower character, including following the player, handling pickups, and managing held items.
|
||||
@@ -20,6 +21,10 @@ public class FollowerController: MonoBehaviour
|
||||
/// </summary>
|
||||
public float manualMoveSmooth = 8f;
|
||||
|
||||
// Settings reference
|
||||
private IPlayerFollowerSettings _settings;
|
||||
private IInteractionSettings _interactionSettings;
|
||||
|
||||
private GameObject _playerRef;
|
||||
private Transform _playerTransform;
|
||||
private AIPath _playerAIPath;
|
||||
@@ -80,6 +85,10 @@ public class FollowerController: MonoBehaviour
|
||||
_animator = GetComponentInChildren<Animator>(); // fallback
|
||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
}
|
||||
|
||||
// Initialize settings references
|
||||
_settings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
|
||||
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
@@ -108,7 +117,7 @@ public class FollowerController: MonoBehaviour
|
||||
}
|
||||
|
||||
_timer += Time.deltaTime;
|
||||
if (_timer >= GameManager.Instance.FollowUpdateInterval)
|
||||
if (_timer >= _settings.FollowUpdateInterval)
|
||||
{
|
||||
_timer = 0f;
|
||||
UpdateFollowTarget();
|
||||
@@ -120,24 +129,24 @@ public class FollowerController: MonoBehaviour
|
||||
Vector2 target2D = new Vector2(_targetPoint.x, _targetPoint.y);
|
||||
float dist = Vector2.Distance(current2D, target2D);
|
||||
float minSpeed = _followerMaxSpeed * 0.3f;
|
||||
float lerpFactor = GameManager.Instance.ManualMoveSmooth * Time.deltaTime;
|
||||
float lerpFactor = _settings.ManualMoveSmooth * Time.deltaTime;
|
||||
float targetSpeed = 0f;
|
||||
if (dist > GameManager.Instance.StopThreshold)
|
||||
if (dist > _settings.StopThreshold)
|
||||
{
|
||||
if (dist > GameManager.Instance.ThresholdFar)
|
||||
if (dist > _settings.ThresholdFar)
|
||||
{
|
||||
targetSpeed = _followerMaxSpeed;
|
||||
}
|
||||
else if (dist > GameManager.Instance.ThresholdNear && dist <= GameManager.Instance.ThresholdFar)
|
||||
else if (dist > _settings.ThresholdNear && dist <= _settings.ThresholdFar)
|
||||
{
|
||||
targetSpeed = _followerMaxSpeed;
|
||||
}
|
||||
else if (dist > GameManager.Instance.StopThreshold && dist <= GameManager.Instance.ThresholdNear)
|
||||
else if (dist > _settings.StopThreshold && dist <= _settings.ThresholdNear)
|
||||
{
|
||||
targetSpeed = minSpeed;
|
||||
}
|
||||
_currentSpeed = Mathf.Lerp(_currentSpeed, targetSpeed, lerpFactor);
|
||||
if (dist > GameManager.Instance.StopThreshold && dist <= GameManager.Instance.ThresholdNear)
|
||||
if (dist > _settings.StopThreshold && dist <= _settings.ThresholdNear)
|
||||
{
|
||||
_currentSpeed = Mathf.Max(_currentSpeed, minSpeed);
|
||||
}
|
||||
@@ -215,7 +224,7 @@ public class FollowerController: MonoBehaviour
|
||||
{
|
||||
_playerMaxSpeed = _playerAIPath.maxSpeed;
|
||||
_defaultFollowerMaxSpeed = _playerMaxSpeed;
|
||||
_followerMaxSpeed = _playerMaxSpeed * GameManager.Instance.FollowerSpeedMultiplier;
|
||||
_followerMaxSpeed = _playerMaxSpeed * _settings.FollowerSpeedMultiplier;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -258,8 +267,8 @@ public class FollowerController: MonoBehaviour
|
||||
{
|
||||
moveDir = _lastMoveDir;
|
||||
}
|
||||
// Use GameSettings for followDistance
|
||||
_targetPoint = playerPos - moveDir * GameManager.Instance.FollowDistance;
|
||||
// Use settings for followDistance
|
||||
_targetPoint = playerPos - moveDir * _settings.FollowDistance;
|
||||
_targetPoint.z = 0;
|
||||
if (_aiPath != null)
|
||||
{
|
||||
@@ -268,25 +277,22 @@ public class FollowerController: MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// Command follower to go to a specific point (pathfinding mode)
|
||||
/// <summary>
|
||||
/// Command follower to go to a specific point (pathfinding mode).
|
||||
/// Make the follower move to a specific point only. Will not automatically return.
|
||||
/// </summary>
|
||||
/// <param name="worldPosition">The world position to move to.</param>
|
||||
public void GoToPoint(Vector2 worldPosition)
|
||||
/// <param name="targetPosition">The position to move to.</param>
|
||||
public void GoToPoint(Vector2 targetPosition)
|
||||
{
|
||||
_isManualFollowing = false;
|
||||
if (_pickupCoroutine != null)
|
||||
StopCoroutine(_pickupCoroutine);
|
||||
if (_aiPath != null)
|
||||
{
|
||||
_aiPath.enabled = true;
|
||||
_aiPath.maxSpeed = _followerMaxSpeed;
|
||||
_aiPath.destination = new Vector3(worldPosition.x, worldPosition.y, 0);
|
||||
}
|
||||
_pickupCoroutine = StartCoroutine(GoToPointSequence(targetPosition));
|
||||
}
|
||||
|
||||
// Command follower to go to a specific point and return to player
|
||||
/// <summary>
|
||||
/// Command follower to go to a specific point and return to player.
|
||||
/// Command follower to go to a specific point and return to player after a brief delay.
|
||||
/// Legacy method that combines GoToPoint and ReturnToPlayer for backward compatibility.
|
||||
/// </summary>
|
||||
/// <param name="itemPosition">The position of the item to pick up.</param>
|
||||
/// <param name="playerTransform">The transform of the player.</param>
|
||||
@@ -299,6 +305,19 @@ public class FollowerController: MonoBehaviour
|
||||
_pickupCoroutine = StartCoroutine(PickupSequence(itemPosition, playerTransform));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make the follower return to the player after it has reached a point.
|
||||
/// </summary>
|
||||
/// <param name="playerTransform">The transform of the player to return to.</param>
|
||||
public void ReturnToPlayer(Transform playerTransform)
|
||||
{
|
||||
if (_pickupCoroutine != null)
|
||||
StopCoroutine(_pickupCoroutine);
|
||||
if (_aiPath != null)
|
||||
_aiPath.maxSpeed = _followerMaxSpeed;
|
||||
_pickupCoroutine = StartCoroutine(ReturnToPlayerSequence(playerTransform));
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator PickupSequence(Vector2 itemPosition, Transform playerTransform)
|
||||
{
|
||||
_isManualFollowing = false;
|
||||
@@ -310,14 +329,14 @@ public class FollowerController: MonoBehaviour
|
||||
_aiPath.destination = new Vector3(itemPosition.x, itemPosition.y, 0);
|
||||
}
|
||||
// Wait until follower reaches item (2D distance)
|
||||
while (Vector2.Distance(new Vector2(transform.position.x, transform.position.y), new Vector2(itemPosition.x, itemPosition.y)) > GameManager.Instance.StopThreshold)
|
||||
while (Vector2.Distance(new Vector2(transform.position.x, transform.position.y), new Vector2(itemPosition.x, itemPosition.y)) > _settings.StopThreshold)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
OnPickupArrived?.Invoke();
|
||||
|
||||
// Wait briefly, then return to player
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
yield return new WaitForSeconds(_interactionSettings.FollowerPickupDelay);
|
||||
if (_aiPath != null && playerTransform != null)
|
||||
{
|
||||
_aiPath.maxSpeed = _followerMaxSpeed;
|
||||
@@ -325,7 +344,7 @@ public class FollowerController: MonoBehaviour
|
||||
}
|
||||
_isReturningToPlayer = true;
|
||||
// Wait until follower returns to player (2D distance)
|
||||
while (playerTransform != null && Vector2.Distance(new Vector2(transform.position.x, transform.position.y), new Vector2(playerTransform.position.x, playerTransform.position.y)) > GameManager.Instance.StopThreshold)
|
||||
while (playerTransform != null && Vector2.Distance(new Vector2(transform.position.x, transform.position.y), new Vector2(playerTransform.position.x, playerTransform.position.y)) > _settings.StopThreshold)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
@@ -340,6 +359,63 @@ public class FollowerController: MonoBehaviour
|
||||
_aiPath.enabled = false;
|
||||
_pickupCoroutine = null;
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator GoToPointSequence(Vector2 targetPosition)
|
||||
{
|
||||
_isManualFollowing = false;
|
||||
_isReturningToPlayer = false;
|
||||
|
||||
if (_aiPath != null)
|
||||
{
|
||||
_aiPath.enabled = true;
|
||||
_aiPath.maxSpeed = _followerMaxSpeed;
|
||||
_aiPath.destination = new Vector3(targetPosition.x, targetPosition.y, 0);
|
||||
}
|
||||
|
||||
// Wait until follower reaches target
|
||||
while (Vector2.Distance(new Vector2(transform.position.x, transform.position.y),
|
||||
new Vector2(targetPosition.x, targetPosition.y)) > _settings.StopThreshold)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Signal arrival
|
||||
OnPickupArrived?.Invoke();
|
||||
|
||||
_pickupCoroutine = null;
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator ReturnToPlayerSequence(Transform playerTransform)
|
||||
{
|
||||
if (_aiPath != null && playerTransform != null)
|
||||
{
|
||||
_aiPath.maxSpeed = _followerMaxSpeed;
|
||||
_aiPath.destination = playerTransform.position;
|
||||
}
|
||||
|
||||
_isReturningToPlayer = true;
|
||||
|
||||
// Wait until follower returns to player
|
||||
while (playerTransform != null &&
|
||||
Vector2.Distance(new Vector2(transform.position.x, transform.position.y),
|
||||
new Vector2(playerTransform.position.x, playerTransform.position.y)) > _settings.StopThreshold)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_isReturningToPlayer = false;
|
||||
OnPickupReturned?.Invoke();
|
||||
|
||||
// Reset follower speed to normal after pickup
|
||||
_followerMaxSpeed = _defaultFollowerMaxSpeed;
|
||||
if (_aiPath != null)
|
||||
_aiPath.maxSpeed = _followerMaxSpeed;
|
||||
_isManualFollowing = true;
|
||||
if (_aiPath != null)
|
||||
_aiPath.enabled = false;
|
||||
|
||||
_pickupCoroutine = null;
|
||||
}
|
||||
#endregion Movement
|
||||
|
||||
#region ItemInteractions
|
||||
@@ -375,15 +451,18 @@ public class FollowerController: MonoBehaviour
|
||||
{
|
||||
return CombinationResult.NotApplicable;
|
||||
}
|
||||
var rule = GameManager.Instance.GetCombinationRule(pickupA.itemData, pickupB.itemData);
|
||||
|
||||
// Use the InteractionSettings directly instead of GameManager
|
||||
CombinationRule matchingRule = _interactionSettings.GetCombinationRule(pickupA.itemData, pickupB.itemData);
|
||||
|
||||
Vector3 spawnPos = pickupA.gameObject.transform.position;
|
||||
if (rule != null && rule.resultPrefab != null)
|
||||
if (matchingRule != null && matchingRule.resultPrefab != null)
|
||||
{
|
||||
newItem = Instantiate(rule.resultPrefab, spawnPos, Quaternion.identity);
|
||||
newItem = Instantiate(matchingRule.resultPrefab, spawnPos, Quaternion.identity);
|
||||
PickupItemData itemData = newItem.GetComponent<Pickup>().itemData;
|
||||
Destroy(pickupA.gameObject);
|
||||
Destroy(pickupB.gameObject);
|
||||
TryPickupItem(newItem,itemData);
|
||||
TryPickupItem(newItem, itemData);
|
||||
return CombinationResult.Successful;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using AppleHills.Core.Settings; // Added for IInteractionSettings
|
||||
|
||||
namespace PuzzleS
|
||||
{
|
||||
@@ -21,6 +22,9 @@ namespace PuzzleS
|
||||
private Transform _playerTransform;
|
||||
private Coroutine _proximityCheckCoroutine;
|
||||
|
||||
// Settings reference
|
||||
private IInteractionSettings _interactionSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance of the PuzzleManager.
|
||||
/// </summary>
|
||||
@@ -58,6 +62,9 @@ namespace PuzzleS
|
||||
_instance = this;
|
||||
// DontDestroyOnLoad(gameObject);
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
|
||||
// Initialize settings reference
|
||||
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
|
||||
}
|
||||
|
||||
void Start()
|
||||
@@ -123,8 +130,8 @@ namespace PuzzleS
|
||||
{
|
||||
if (_playerTransform != null)
|
||||
{
|
||||
// Get the proximity threshold from settings (half of the prompt range)
|
||||
float proximityThreshold = GameManager.Instance.DefaultPuzzlePromptRange;
|
||||
// Get the proximity threshold from settings directly using our settings reference
|
||||
float proximityThreshold = _interactionSettings?.DefaultPuzzlePromptRange ?? 3.0f;
|
||||
|
||||
// Check distance to each step behavior
|
||||
foreach (var kvp in _stepBehaviours)
|
||||
@@ -198,16 +205,36 @@ namespace PuzzleS
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks all initial steps (those with no dependencies).
|
||||
/// Unlocks all initial steps (those with no dependencies) and any steps whose dependencies are already met.
|
||||
/// </summary>
|
||||
private void UnlockInitialSteps()
|
||||
{
|
||||
// First, unlock all steps with no dependencies (initial steps)
|
||||
var initialSteps = PuzzleGraphUtility.FindInitialSteps(_runtimeDependencies);
|
||||
foreach (var step in initialSteps)
|
||||
{
|
||||
Debug.Log($"[Puzzles] Initial step unlocked: {step.stepId}");
|
||||
UnlockStep(step);
|
||||
}
|
||||
|
||||
// Keep trying to unlock steps as long as we're making progress
|
||||
bool madeProgress;
|
||||
do
|
||||
{
|
||||
madeProgress = false;
|
||||
|
||||
// Check all steps that haven't been unlocked yet
|
||||
foreach (var step in _runtimeDependencies.Keys.Where(s => !_unlockedSteps.Contains(s)))
|
||||
{
|
||||
// Check if all dependencies have been completed
|
||||
if (AreRuntimeDependenciesMet(step))
|
||||
{
|
||||
Debug.Log($"[Puzzles] Chain step unlocked: {step.stepId}");
|
||||
UnlockStep(step);
|
||||
madeProgress = true;
|
||||
}
|
||||
}
|
||||
} while (madeProgress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user