diff --git a/Assets/Data/Bootstrap/Runtime/CustomBootSettings_Runtime.asset b/Assets/Data/Bootstrap/Runtime/CustomBootSettings_Runtime.asset
index 2f934632..a13c9da9 100644
--- a/Assets/Data/Bootstrap/Runtime/CustomBootSettings_Runtime.asset
+++ b/Assets/Data/Bootstrap/Runtime/CustomBootSettings_Runtime.asset
@@ -18,3 +18,4 @@ MonoBehaviour:
- {fileID: 458265635552197097, guid: a77d1e8b2fa8aa945a6f39b312536e0d, type: 3}
- {fileID: 552225285624929822, guid: e39992796d5459442be9967c77e27066, type: 3}
- {fileID: 7644433920135100480, guid: 12d242e44fe80ab44af852254b7cab0f, type: 3}
+ - {fileID: 5970756976527527001, guid: eaab28d7e21337b4baef062e2977e616, type: 3}
diff --git a/Assets/Prefabs/Managers/InteractionManager.prefab b/Assets/Prefabs/Managers/InteractionManager.prefab
new file mode 100644
index 00000000..dcb39eca
--- /dev/null
+++ b/Assets/Prefabs/Managers/InteractionManager.prefab
@@ -0,0 +1,46 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &5970756976527527001
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2154246752606426586}
+ - component: {fileID: 5680731486320555959}
+ m_Layer: 0
+ m_Name: InteractionManager
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &2154246752606426586
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5970756976527527001}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 20.57967, y: 22.03297, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &5680731486320555959
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5970756976527527001}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 705c4ee7f8204cc68aacd79e2a4a506d, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
diff --git a/Assets/Prefabs/Managers/InteractionManager.prefab.meta b/Assets/Prefabs/Managers/InteractionManager.prefab.meta
new file mode 100644
index 00000000..14b39a04
--- /dev/null
+++ b/Assets/Prefabs/Managers/InteractionManager.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: eaab28d7e21337b4baef062e2977e616
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scenes/Levels/AppleHillsOverworld.unity b/Assets/Scenes/Levels/AppleHillsOverworld.unity
index dbdb43c9..e8f6a468 100644
--- a/Assets/Scenes/Levels/AppleHillsOverworld.unity
+++ b/Assets/Scenes/Levels/AppleHillsOverworld.unity
@@ -868,7 +868,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: c119ffb87b2a16d4f925ff5d5ffd7092, type: 3}
m_Name:
m_EditorClassIdentifier:
- ShouldPlayIntro: 1
+ shouldPlayIntro: 0
--- !u!95 &948124911
Animator:
serializedVersion: 7
@@ -1998,6 +1998,10 @@ PrefabInstance:
propertyPath: heldIconDisplayHeight
value: 2
objectReference: {fileID: 0}
+ - target: {fileID: 7852204877518954380, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
+ propertyPath: endReachedDistance
+ value: 1
+ objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
diff --git a/Assets/Scripts/Characters.meta b/Assets/Scripts/Characters.meta
new file mode 100644
index 00000000..ca2412db
--- /dev/null
+++ b/Assets/Scripts/Characters.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 01ee82c489314d60bf27aa9a405f2633
+timeCreated: 1757513074
\ No newline at end of file
diff --git a/Assets/Scripts/Characters/Character.cs b/Assets/Scripts/Characters/Character.cs
new file mode 100644
index 00000000..1c3a97c5
--- /dev/null
+++ b/Assets/Scripts/Characters/Character.cs
@@ -0,0 +1,11 @@
+using UnityEngine;
+
+///
+/// Base class for all characters that can interact in the world (e.g., player, follower).
+///
+public abstract class Character : MonoBehaviour
+{
+ // Placeholder for shared character logic or properties.
+ // For now, this is intentionally minimal.
+}
+
diff --git a/Assets/Scripts/Characters/Character.cs.meta b/Assets/Scripts/Characters/Character.cs.meta
new file mode 100644
index 00000000..90416577
--- /dev/null
+++ b/Assets/Scripts/Characters/Character.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 9253c7c4ca6946b1b31196c4bf8d685e
+timeCreated: 1757513074
\ No newline at end of file
diff --git a/Assets/Scripts/Input/InputManager.cs b/Assets/Scripts/Input/InputManager.cs
index eaffd4e3..1c92b883 100644
--- a/Assets/Scripts/Input/InputManager.cs
+++ b/Assets/Scripts/Input/InputManager.cs
@@ -157,11 +157,28 @@ public class InputManager : MonoBehaviour
Collider2D hit = Physics2D.OverlapPoint(worldPos, mask);
if (hit != null)
{
- var interactable = hit.GetComponent();
+ var interactable = hit.GetComponent();
if (interactable != null)
{
Debug.unityLogger.Log("Interactable", $"[InputManager] Delegating tap to interactable at {worldPos} (GameObject: {hit.gameObject.name})");
- interactable.OnTap(worldPos);
+ // Find the player Character (by tag)
+ var playerObj = GameObject.FindGameObjectWithTag("Player");
+ var playerCharacter = playerObj != null ? playerObj.GetComponent() : null;
+ if (playerCharacter != null)
+ {
+ InteractionOrchestrator.Instance.RequestInteraction(interactable, playerCharacter);
+ }
+ else
+ {
+ Debug.LogWarning("[InputManager] Player Character not found for interaction delegation.");
+ }
+ return true;
+ }
+ // Fallback: support other ITouchInputConsumer implementations
+ var consumer = hit.GetComponent();
+ if (consumer != null)
+ {
+ consumer.OnTap(worldPos);
return true;
}
else
diff --git a/Assets/Scripts/Input/PlayerTouchController.cs b/Assets/Scripts/Input/PlayerTouchController.cs
index 8a853e77..49674c1e 100644
--- a/Assets/Scripts/Input/PlayerTouchController.cs
+++ b/Assets/Scripts/Input/PlayerTouchController.cs
@@ -7,7 +7,7 @@ namespace Input
/// Handles player movement in response to tap and hold input events.
/// Supports both direct and pathfinding movement modes, and provides event/callbacks for arrival/cancellation.
///
- public class PlayerTouchController : MonoBehaviour, ITouchInputConsumer
+ public class PlayerTouchController : Character, ITouchInputConsumer
{
// --- Movement State ---
private Vector3 targetPosition;
diff --git a/Assets/Scripts/Interactions/Interactable.cs b/Assets/Scripts/Interactions/Interactable.cs
index bab8a866..517ad3fa 100644
--- a/Assets/Scripts/Interactions/Interactable.cs
+++ b/Assets/Scripts/Interactions/Interactable.cs
@@ -9,13 +9,6 @@ public class Interactable : MonoBehaviour, ITouchInputConsumer
public event Action StartedInteraction;
public event Action InteractionComplete;
- private ObjectiveStepBehaviour stepBehaviour;
-
- void Awake()
- {
- stepBehaviour = GetComponent();
- }
-
///
/// Handles tap input. Triggers interaction logic.
///
@@ -25,48 +18,20 @@ public class Interactable : MonoBehaviour, ITouchInputConsumer
StartedInteraction?.Invoke();
}
- ///
- /// No hold behavior for interactables.
- ///
+ // No hold behavior for interactables.
public void OnHoldStart(Vector2 worldPosition) { }
public void OnHoldMove(Vector2 worldPosition) { }
public void OnHoldEnd(Vector2 worldPosition) { }
///
- /// Called when the follower arrives at this interactable.
+ /// Called to interact with this object by a character (player, follower, etc).
///
- public bool OnFollowerArrived(FollowerController follower)
+ public virtual void OnInteract(Character character)
{
- // Check if step is locked here
- if (stepBehaviour != null && !stepBehaviour.IsStepUnlocked())
- {
- DebugUIMessage.Show("Item is not unlocked yet");
- Debug.Log("[Puzzles] Tried to interact with locked step: " + gameObject.name);
- InteractionComplete?.Invoke(false);
- return false;
- }
- var requirements = GetComponents();
- if (requirements.Length == 0)
- {
- InteractionComplete?.Invoke(true);
- return true;
- }
- bool anySuccess = false;
- foreach (var req in requirements)
- {
- if (req.TryInteract(follower))
- {
- anySuccess = true;
- break;
- }
- }
- InteractionComplete?.Invoke(anySuccess);
- if (!anySuccess)
- {
- Debug.Log($"[Interactable] No interaction requirements succeeded for {gameObject.name}");
- // Optionally trigger a default failure event or feedback here
- }
- return anySuccess;
+ // In the new architecture, requirements and step checks will be handled by orchestrator.
+ StartedInteraction?.Invoke();
+ // For now, immediately complete interaction as success (can be extended later).
+ InteractionComplete?.Invoke(true);
}
public void CompleteInteraction(bool success)
diff --git a/Assets/Scripts/Interactions/InteractionOrchestrator.cs b/Assets/Scripts/Interactions/InteractionOrchestrator.cs
new file mode 100644
index 00000000..3621e793
--- /dev/null
+++ b/Assets/Scripts/Interactions/InteractionOrchestrator.cs
@@ -0,0 +1,154 @@
+using System;
+using UnityEngine;
+
+///
+/// Handles the process of moving characters to interactables and orchestrating interactions.
+///
+public class InteractionOrchestrator : MonoBehaviour
+{
+ private static InteractionOrchestrator _instance;
+ private static bool _isQuitting = false;
+
+ // Singleton for easy access (optional, can be replaced with DI or scene reference)
+ public static InteractionOrchestrator Instance
+ {
+ get
+ {
+ if (_instance == null && Application.isPlaying && !_isQuitting)
+ {
+ _instance = FindAnyObjectByType();
+ if (_instance == null)
+ {
+ var go = new GameObject("InteractionOrchestrator");
+ _instance = go.AddComponent();
+ // DontDestroyOnLoad(go);
+ }
+ }
+ return _instance;
+ }
+ }
+
+ void Awake()
+ {
+ _instance = this;
+ // DontDestroyOnLoad(gameObject);
+ }
+
+ void OnApplicationQuit()
+ {
+ _isQuitting = true;
+ }
+
+ // Store pending interaction state
+ private Interactable _pendingInteractable;
+ private Input.PlayerTouchController _pendingPlayer;
+ private FollowerController _pendingFollower;
+ private bool _interactionInProgress;
+
+ ///
+ /// Request an interaction between a character and an interactable.
+ ///
+ public void RequestInteraction(Interactable interactable, Character character)
+ {
+ // Only support player-initiated interactions for now
+ if (character is Input.PlayerTouchController player)
+ {
+ // Compute closest point on the interaction radius
+ Vector3 interactablePos = interactable.transform.position;
+ Vector3 playerPos = player.transform.position;
+ float stopDistance = GameManager.Instance.PlayerStopDistance;
+ Vector3 toPlayer = (playerPos - interactablePos).normalized;
+ Vector3 stopPoint = interactablePos + toPlayer * stopDistance;
+
+ // Unsubscribe previous to avoid duplicate calls
+ player.OnArrivedAtTarget -= OnPlayerArrived;
+ player.OnArrivedAtTarget += OnPlayerArrived;
+ _pendingInteractable = interactable;
+ _pendingPlayer = player;
+ _pendingFollower = FindFollower();
+ _interactionInProgress = true;
+ player.MoveToAndNotify(stopPoint);
+ }
+ else
+ {
+ // Fallback: immediately interact
+ OnCharacterArrived(interactable, character);
+ }
+ }
+
+ // Helper to find the follower in the scene
+ private FollowerController FindFollower()
+ {
+ // Use the recommended Unity API for finding objects
+ return GameObject.FindFirstObjectByType();
+ }
+
+ private void OnPlayerArrived()
+ {
+ if (!_interactionInProgress || _pendingInteractable == null || _pendingPlayer == null)
+ return;
+ // Unsubscribe to avoid memory leaks
+ _pendingPlayer.OnArrivedAtTarget -= OnPlayerArrived;
+ // Now dispatch the follower to the interactable
+ if (_pendingFollower != null)
+ {
+ _pendingFollower.OnPickupArrived -= OnFollowerArrived;
+ _pendingFollower.OnPickupArrived += OnFollowerArrived;
+ _pendingFollower.GoToPointAndReturn(_pendingInteractable.transform.position, _pendingPlayer.transform);
+ }
+ else
+ {
+ // No follower found, just interact as player
+ OnCharacterArrived(_pendingInteractable, _pendingPlayer);
+ _interactionInProgress = false;
+ _pendingInteractable = null;
+ _pendingPlayer = null;
+ _pendingFollower = null;
+ }
+ }
+
+ private void OnFollowerArrived()
+ {
+ if (!_interactionInProgress || _pendingInteractable == null || _pendingFollower == null)
+ return;
+ _pendingFollower.OnPickupArrived -= OnFollowerArrived;
+ // Now check requirements and interact as follower
+ OnCharacterArrived(_pendingInteractable, _pendingFollower);
+ _interactionInProgress = false;
+ _pendingInteractable = null;
+ _pendingPlayer = null;
+ _pendingFollower = null;
+ }
+
+ ///
+ /// Called when the character has arrived at the interactable.
+ ///
+ private void OnCharacterArrived(Interactable interactable, Character character)
+ {
+ // Check all requirements
+ var requirements = interactable.GetComponents();
+ bool allMet = true;
+ foreach (var req in requirements)
+ {
+ // For now, only FollowerController is supported for requirements
+ // (can be extended to support Player, etc.)
+ if (character is FollowerController follower)
+ {
+ if (!req.TryInteract(follower))
+ {
+ allMet = false;
+ break;
+ }
+ }
+ // Add more character types as needed
+ }
+ if (allMet)
+ {
+ interactable.OnInteract(character);
+ }
+ else
+ {
+ interactable.CompleteInteraction(false);
+ }
+ }
+}
diff --git a/Assets/Scripts/Interactions/InteractionOrchestrator.cs.meta b/Assets/Scripts/Interactions/InteractionOrchestrator.cs.meta
new file mode 100644
index 00000000..afd3e441
--- /dev/null
+++ b/Assets/Scripts/Interactions/InteractionOrchestrator.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 705c4ee7f8204cc68aacd79e2a4a506d
+timeCreated: 1757513080
\ No newline at end of file
diff --git a/Assets/Scripts/Interactions/Pickup.cs b/Assets/Scripts/Interactions/Pickup.cs
index d974649f..415ef5a6 100644
--- a/Assets/Scripts/Interactions/Pickup.cs
+++ b/Assets/Scripts/Interactions/Pickup.cs
@@ -13,8 +13,6 @@ public class Pickup : MonoBehaviour
public SpriteRenderer iconRenderer;
private Interactable interactable;
- private bool pickupInProgress = false;
-
///
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
///
@@ -98,76 +96,11 @@ public class Pickup : MonoBehaviour
}
///
- /// Handles the start of an interaction (player approaches, then follower picks up).
+ /// Handles the start of an interaction (for feedback/UI only).
///
private void OnStartedInteraction()
{
- if (pickupInProgress) return;
- var playerObj = GameObject.FindGameObjectWithTag("Player");
- var followerObj = GameObject.FindGameObjectWithTag("Pulver");
- if (playerObj == null || followerObj == null)
- {
- Debug.LogWarning("Pickup: Player or Follower not found.");
- return;
- }
- var playerController = playerObj.GetComponent();
- var followerController = followerObj.GetComponent();
- if (playerController == null || followerController == null)
- {
- Debug.LogWarning("Pickup: PlayerTouchController or FollowerController missing.");
- return;
- }
- float playerStopDistance = GameManager.Instance.PlayerStopDistance;
- float followerPickupDelay = GameManager.Instance.FollowerPickupDelay;
- // --- Local event/coroutine handlers ---
- void OnPlayerArrived()
- {
- playerController.OnArrivedAtTarget -= OnPlayerArrived;
- playerController.OnMoveToCancelled -= OnPlayerMoveCancelled;
- pickupInProgress = true;
- StartCoroutine(DispatchFollower());
- }
- void OnPlayerMoveCancelled()
- {
- playerController.OnArrivedAtTarget -= OnPlayerArrived;
- playerController.OnMoveToCancelled -= OnPlayerMoveCancelled;
- pickupInProgress = false;
- }
- System.Collections.IEnumerator DispatchFollower()
- {
- yield return new WaitForSeconds(followerPickupDelay);
- followerController.OnPickupArrived += OnFollowerArrived;
- followerController.OnPickupReturned += OnFollowerReturned;
- followerController.GoToPointAndReturn(transform.position, playerObj.transform);
- }
- void OnFollowerArrived()
- {
- followerController.OnPickupArrived -= OnFollowerArrived;
- bool interactionSuccess = true;
- if (interactable != null)
- {
- interactionSuccess = interactable.OnFollowerArrived(followerController);
- }
- followerController.SetInteractionResult(interactionSuccess);
- }
- void OnFollowerReturned()
- {
- followerController.OnPickupReturned -= OnFollowerReturned;
- pickupInProgress = false;
- }
- playerController.OnArrivedAtTarget += OnPlayerArrived;
- playerController.OnMoveToCancelled += OnPlayerMoveCancelled;
- Vector3 stopPoint = transform.position + (playerObj.transform.position - transform.position).normalized * playerStopDistance;
- float distToPickup = Vector2.Distance(new Vector2(playerObj.transform.position.x, playerObj.transform.position.y), new Vector2(transform.position.x, transform.position.y));
- float dist = Vector2.Distance(new Vector2(playerObj.transform.position.x, playerObj.transform.position.y), new Vector2(stopPoint.x, stopPoint.y));
- if (distToPickup <= playerStopDistance || dist <= 0.2f)
- {
- OnPlayerArrived();
- }
- else
- {
- playerController.MoveToAndNotify(stopPoint);
- }
+ // Optionally, add pickup-specific feedback here (e.g., highlight, sound).
}
///
diff --git a/Assets/Scripts/Interactions/SlotItemBehavior.cs b/Assets/Scripts/Interactions/SlotItemBehavior.cs
index 84bfebc7..e45b3012 100644
--- a/Assets/Scripts/Interactions/SlotItemBehavior.cs
+++ b/Assets/Scripts/Interactions/SlotItemBehavior.cs
@@ -47,14 +47,13 @@ public class SlotItemBehavior : InteractionRequirementBase
SetSlottedObject(obj);
}
- private void RestoreSlottedObject(Vector3 position)
+ private void RestoreSlottedObject()
{
if (_cachedSlottedObject != null)
{
- _cachedSlottedObject.transform.position = position;
_cachedSlottedObject.transform.SetParent(null);
- _cachedSlottedObject.SetActive(true);
- _cachedSlottedObject = null;
+ // Do NOT activate or move the object here; it stays hidden until dropped
+ // Activation is handled by the drop logic elsewhere
}
}
@@ -76,8 +75,10 @@ public class SlotItemBehavior : InteractionRequirementBase
// CASE 1: No held item, slot has item -> pick up slotted item
if (heldItem == null && _cachedSlottedObject != null)
{
+ // Give the hidden slotted object to the follower (do NOT activate or move it)
+ RestoreSlottedObject();
follower.SetHeldItemFromObject(_cachedSlottedObject);
- RemoveSlottedObject();
+ _cachedSlottedObject = null;
currentlySlottedItem = null;
UpdateSlottedSprite();
return true;
@@ -95,7 +96,8 @@ public class SlotItemBehavior : InteractionRequirementBase
currentlySlottedItem = followerHeldItem;
UpdateSlottedSprite();
- // 2. Give the slot's object to the follower
+ // 2. Give the slot's object to the follower (do NOT activate or move it)
+ RestoreSlottedObject();
follower.SetHeldItemFromObject(slotObj);
return true;
diff --git a/Assets/Scripts/Movement/FollowerController.cs b/Assets/Scripts/Movement/FollowerController.cs
index fc26d6c6..3c081ae9 100644
--- a/Assets/Scripts/Movement/FollowerController.cs
+++ b/Assets/Scripts/Movement/FollowerController.cs
@@ -6,7 +6,7 @@ using Utils;
///
/// Controls the follower character, including following the player, handling pickups, and managing held items.
///
-public class FollowerController : MonoBehaviour
+public class FollowerController : Character
{
[Header("Follower Settings")]
public bool debugDrawTarget = true;
@@ -457,5 +457,3 @@ public class FollowerController : MonoBehaviour
}
}
}
-
-