Rework interactables into a flatter hierarchy, reenable puzzles as well

This commit is contained in:
Michal Pikulski
2025-09-11 13:00:26 +02:00
parent 487a0fa6d4
commit 629ffe7ffc
32 changed files with 816 additions and 1024 deletions

View File

@@ -18,4 +18,3 @@ 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}

View File

@@ -1,4 +1,6 @@
using UnityEditor;
using Interactions;
using PuzzleS;
using UnityEditor;
using UnityEngine;
namespace Editor
@@ -64,27 +66,16 @@ namespace Editor
{
PrefabEditorUtility.RemoveComponent<Pickup>(_selectedGameObject);
}
// CombineWithBehavior
bool hasCombine = _selectedGameObject.GetComponent<CombineWithBehavior>() != null;
bool addCombine = EditorGUILayout.Toggle("CombineWithBehavior", hasCombine);
if (addCombine && !hasCombine)
{
PrefabEditorUtility.AddOrGetComponent<CombineWithBehavior>(_selectedGameObject);
}
else if (!addCombine && hasCombine)
{
PrefabEditorUtility.RemoveComponent<CombineWithBehavior>(_selectedGameObject);
}
// SlotItemBehavior
bool hasSlot = _selectedGameObject.GetComponent<SlotItemBehavior>() != null;
bool hasSlot = _selectedGameObject.GetComponent<ItemSlot>() != null;
bool addSlot = EditorGUILayout.Toggle("SlotItemBehavior", hasSlot);
if (addSlot && !hasSlot)
{
PrefabEditorUtility.AddOrGetComponent<SlotItemBehavior>(_selectedGameObject);
PrefabEditorUtility.AddOrGetComponent<ItemSlot>(_selectedGameObject);
}
else if (!addSlot && hasSlot)
{
PrefabEditorUtility.RemoveComponent<SlotItemBehavior>(_selectedGameObject);
PrefabEditorUtility.RemoveComponent<ItemSlot>(_selectedGameObject);
}
// ObjectiveStepBehaviour
bool hasObjective = _selectedGameObject.GetComponent<ObjectiveStepBehaviour>() != null;

View File

@@ -1,6 +1,8 @@
using UnityEditor;
using UnityEngine;
using System.IO;
using Interactions;
using PuzzleS;
namespace Editor
{
@@ -121,13 +123,9 @@ namespace Editor
var pickup = go.AddComponent<Pickup>();
pickup.itemData = _pickupData;
}
if (_addCombineWith)
{
go.AddComponent<CombineWithBehavior>();
}
if (_addSlot)
{
go.AddComponent<SlotItemBehavior>();
go.AddComponent<ItemSlot>();
}
if (_addObjective)
{

View File

@@ -3,6 +3,8 @@ using UnityEngine;
using UnityEditor.SceneManagement;
using System.Collections.Generic;
using System.Linq;
using Interactions;
using PuzzleS;
public class SceneObjectLocatorWindow : EditorWindow
{
@@ -43,12 +45,10 @@ public class SceneObjectLocatorWindow : EditorWindow
foreach (var pickup in pickups)
{
var go = pickup.gameObject;
bool hasCombine = go.GetComponent<CombineWithBehavior>() != null;
bool hasSlot = go.GetComponent<SlotItemBehavior>() != null;
bool hasSlot = go.GetComponent<ItemSlot>() != null;
pickupInfos.Add(new PickupInfo
{
pickup = pickup,
hasCombine = hasCombine,
hasSlot = hasSlot
});
}

View File

@@ -13,6 +13,7 @@ GameObject:
- component: {fileID: 3435632802124758411}
- component: {fileID: 8947209170748834035}
- component: {fileID: 7852204877518954380}
- component: {fileID: 1621671461027776358}
m_Layer: 8
m_Name: PulverCharacter
m_TagString: Pulver
@@ -86,16 +87,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: f82afe7b57bd4e0b9f51a1cca06765f1, type: 3}
m_Name:
m_EditorClassIdentifier:
followDistance: 3
debugDrawTarget: 1
followUpdateInterval: 0.1
manualMoveSmooth: 5
acceleration: 10
deceleration: 12
thresholdFar: 7
thresholdNear: 5
stopThreshold: 0.5
currentlyHeldItem: {fileID: 0}
manualMoveSmooth: 100
justCombined: 0
heldObjectRenderer: {fileID: 2099200424669714683}
--- !u!114 &8947209170748834035
MonoBehaviour:
@@ -162,11 +157,33 @@ MonoBehaviour:
rotationSpeed: 360
slowdownDistance: 3
pickNextWaypointDist: 2
endReachedDistance: 0.5
endReachedDistance: 1
alwaysDrawGizmos: 0
slowWhenNotFacingTarget: 1
whenCloseToDestination: 0
constrainInsideGraph: 0
--- !u!114 &1621671461027776358
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1102400833121127473}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cb6a34d769a1e4ac7b0b30e433aa443c, type: 3}
m_Name:
m_EditorClassIdentifier:
version: 1
smoothType: 0
subdivisions: 2
iterations: 2
strength: 0.5
uniformLength: 1
maxSegmentLength: 2
bezierTangentLength: 0.4
offset: 0.2
factor: 0.1
--- !u!1 &5934518940303293264
GameObject:
m_ObjectHideFlags: 0

View File

@@ -148,6 +148,21 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 73d6494a73174ffabc6a7d3089d51e73, type: 3}
m_Name:
m_EditorClassIdentifier:
isOneTime: 0
cooldown: -1
characterToInteract: 0
interactionStarted:
m_PersistentCalls:
m_Calls: []
interactionInterrupted:
m_PersistentCalls:
m_Calls: []
characterArrived:
m_PersistentCalls:
m_Calls: []
interactionComplete:
m_PersistentCalls:
m_Calls: []
--- !u!114 &7629925223318462841
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@@ -1,46 +0,0 @@
%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:

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: eaab28d7e21337b4baef062e2977e616
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -190,7 +190,7 @@ Transform:
m_GameObject: {fileID: 100481742}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalPosition: {x: -4, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -299,6 +299,10 @@ PrefabInstance:
propertyPath: stepData
value:
objectReference: {fileID: 11400000, guid: 13b0c411066f85a41ba40c3bbbc281ed, type: 2}
- target: {fileID: 6254953093500072797, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
propertyPath: characterToInteract
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6303063351359542479, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
propertyPath: m_Sprite
value:
@@ -494,6 +498,10 @@ PrefabInstance:
propertyPath: m_Name
value: Quarry
objectReference: {fileID: 0}
- target: {fileID: 8086761134767870039, guid: 539b408cd1191614abdcd99506f1157d, type: 3}
propertyPath: characterToInteract
value: 0
objectReference: {fileID: 0}
- target: {fileID: 9191656170436146298, guid: 539b408cd1191614abdcd99506f1157d, type: 3}
propertyPath: m_Sprite
value:
@@ -567,17 +575,23 @@ PrefabInstance:
propertyPath: m_Enabled
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6254953093500072797, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
propertyPath: isOneTime
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6254953093500072797, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
propertyPath: characterToInteract
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6350287859698694726, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
propertyPath: m_Name
value: TestAss
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedComponents:
- {fileID: 4778083634590203921, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 6350287859698694726, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
insertIndex: -1
addedObject: {fileID: 800207724}
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
--- !u!1 &384576743
GameObject:
@@ -737,6 +751,10 @@ PrefabInstance:
propertyPath: stepData
value:
objectReference: {fileID: 11400000, guid: 9de0c57af6191384e96e2ba7c04a3d0d, type: 2}
- target: {fileID: 6254953093500072797, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
propertyPath: characterToInteract
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6303063351359542479, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
propertyPath: m_Sprite
value:
@@ -755,29 +773,6 @@ Transform:
m_CorrespondingSourceObject: {fileID: 2844046668579196942, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
m_PrefabInstance: {fileID: 368640488}
m_PrefabAsset: {fileID: 0}
--- !u!1 &800207718 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: 6350287859698694726, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
m_PrefabInstance: {fileID: 368640488}
m_PrefabAsset: {fileID: 0}
--- !u!114 &800207724
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 800207718}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 21401a3b30134380bb205964d9e5c67d, type: 3}
m_Name:
m_EditorClassIdentifier:
OnSuccess:
m_PersistentCalls:
m_Calls: []
OnFailure:
m_PersistentCalls:
m_Calls: []
--- !u!1 &948124904
GameObject:
m_ObjectHideFlags: 0
@@ -982,6 +977,10 @@ PrefabInstance:
propertyPath: stepData
value:
objectReference: {fileID: 11400000, guid: a84cbe9804e13f74e857c55d90cc10d1, type: 2}
- target: {fileID: 6254953093500072797, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
propertyPath: characterToInteract
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6303063351359542479, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
propertyPath: m_Sprite
value:
@@ -1103,6 +1102,10 @@ PrefabInstance:
propertyPath: m_WasSpriteAssigned
value: 1
objectReference: {fileID: 0}
- target: {fileID: 7616859841301711022, guid: bf4b9d7045397f946b2125b1ad4a3fbd, type: 3}
propertyPath: characterToInteract
value: 1
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
@@ -1446,7 +1449,16 @@ PrefabInstance:
propertyPath: m_WasSpriteAssigned
value: 1
objectReference: {fileID: 0}
m_RemovedComponents: []
- target: {fileID: 7616859841301711022, guid: bf4b9d7045397f946b2125b1ad4a3fbd, type: 3}
propertyPath: isOneTime
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7616859841301711022, guid: bf4b9d7045397f946b2125b1ad4a3fbd, type: 3}
propertyPath: characterToInteract
value: 1
objectReference: {fileID: 0}
m_RemovedComponents:
- {fileID: 592045584872845087, guid: bf4b9d7045397f946b2125b1ad4a3fbd, type: 3}
m_RemovedGameObjects: []
m_AddedGameObjects:
- targetCorrespondingSourceObject: {fileID: 1730119453103664125, guid: bf4b9d7045397f946b2125b1ad4a3fbd, type: 3}
@@ -1562,37 +1574,15 @@ PrefabInstance:
propertyPath: m_WasSpriteAssigned
value: 1
objectReference: {fileID: 0}
- target: {fileID: 7616859841301711022, guid: bf4b9d7045397f946b2125b1ad4a3fbd, type: 3}
propertyPath: characterToInteract
value: 1
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 7447346505753002421, guid: bf4b9d7045397f946b2125b1ad4a3fbd, type: 3}
insertIndex: -1
addedObject: {fileID: 1578994557}
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: bf4b9d7045397f946b2125b1ad4a3fbd, type: 3}
--- !u!1 &1578994556 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: 7447346505753002421, guid: bf4b9d7045397f946b2125b1ad4a3fbd, type: 3}
m_PrefabInstance: {fileID: 1578994555}
m_PrefabAsset: {fileID: 0}
--- !u!114 &1578994557
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1578994556}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 21401a3b30134380bb205964d9e5c67d, type: 3}
m_Name:
m_EditorClassIdentifier:
OnSuccess:
m_PersistentCalls:
m_Calls: []
OnFailure:
m_PersistentCalls:
m_Calls: []
--- !u!4 &1627665103 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 2844046668579196942, guid: b5fc01af35233eb4cbeede05e50a7c34, type: 3}
@@ -1620,14 +1610,14 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: ec1a2e6e32f746c4990c579e13b79104, type: 3}
m_Name:
m_EditorClassIdentifier:
OnSuccess:
m_PersistentCalls:
m_Calls: []
OnFailure:
m_PersistentCalls:
m_Calls: []
currentlySlottedItem: {fileID: 0}
itemData: {fileID: 11400000, guid: e0fad48a84a6b6346ac17c84bc512500, type: 2}
iconRenderer: {fileID: 1631660128}
slottedItemRenderer: {fileID: 124275613}
--- !u!212 &1631660128 stripped
SpriteRenderer:
m_CorrespondingSourceObject: {fileID: 7494677664706785084, guid: bf4b9d7045397f946b2125b1ad4a3fbd, type: 3}
m_PrefabInstance: {fileID: 1336824707}
m_PrefabAsset: {fileID: 0}
--- !u!1 &1741016587
GameObject:
m_ObjectHideFlags: 0
@@ -1716,7 +1706,7 @@ Transform:
m_GameObject: {fileID: 1741016587}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalPosition: {x: -4, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -1974,34 +1964,6 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3435632802124758411, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
propertyPath: acceleration
value: 15
objectReference: {fileID: 0}
- target: {fileID: 3435632802124758411, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
propertyPath: thresholdFar
value: 12
objectReference: {fileID: 0}
- target: {fileID: 3435632802124758411, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
propertyPath: thresholdNear
value: 7
objectReference: {fileID: 0}
- target: {fileID: 3435632802124758411, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
propertyPath: followDistance
value: 5
objectReference: {fileID: 0}
- target: {fileID: 3435632802124758411, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
propertyPath: manualMoveSmooth
value: 100
objectReference: {fileID: 0}
- target: {fileID: 3435632802124758411, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
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: []
@@ -2017,7 +1979,7 @@ PrefabInstance:
m_Modifications:
- target: {fileID: 3823830588451517910, guid: 301b4e0735896334f8f6fb9a68a7e419, type: 3}
propertyPath: m_LocalPosition.x
value: 0
value: -4
objectReference: {fileID: 0}
- target: {fileID: 3823830588451517910, guid: 301b4e0735896334f8f6fb9a68a7e419, type: 3}
propertyPath: m_LocalPosition.y

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 01ee82c489314d60bf27aa9a405f2633
timeCreated: 1757513074

View File

@@ -1,11 +0,0 @@
using UnityEngine;
/// <summary>
/// Base class for all characters that can interact in the world (e.g., player, follower).
/// </summary>
public abstract class Character : MonoBehaviour
{
// Placeholder for shared character logic or properties.
// For now, this is intentionally minimal.
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 9253c7c4ca6946b1b31196c4bf8d685e
timeCreated: 1757513074

View File

@@ -59,6 +59,7 @@ public class GameManager : MonoBehaviour
public float HeldIconDisplayHeight => gameSettings != null ? gameSettings.heldIconDisplayHeight : 2.0f;
public GameObject BasePickupPrefab => gameSettings != null ? gameSettings.basePickupPrefab : null;
public LayerMask InteractableLayerMask => gameSettings != null ? gameSettings.interactableLayerMask : -1;
public float PlayerStopDistanceDirectInteraction => gameSettings != null ? gameSettings.playerStopDistanceDirectInteraction : 2.0f;
/// <summary>
/// Returns the combination rule for two items, if any.

View File

@@ -8,6 +8,7 @@ public class GameSettings : ScriptableObject
{
[Header("Interactions")]
public float playerStopDistance = 6.0f;
public float playerStopDistanceDirectInteraction = 2.0f;
public float followerPickupDelay = 0.2f;
[Header("Follower Settings")]

View File

@@ -157,34 +157,14 @@ public class InputManager : MonoBehaviour
Collider2D hit = Physics2D.OverlapPoint(worldPos, mask);
if (hit != null)
{
var interactable = hit.GetComponent<Interactable>();
if (interactable != null)
{
Debug.unityLogger.Log("Interactable", $"[InputManager] Delegating tap to interactable at {worldPos} (GameObject: {hit.gameObject.name})");
// Find the player Character (by tag)
var playerObj = GameObject.FindGameObjectWithTag("Player");
var playerCharacter = playerObj != null ? playerObj.GetComponent<Character>() : 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<ITouchInputConsumer>();
if (consumer != null)
{
Debug.unityLogger.Log("Interactable", $"[InputManager] Delegating tap to consumer at {worldPos} (GameObject: {hit.gameObject.name})");
consumer.OnTap(worldPos);
return true;
}
else
{
Debug.unityLogger.Log("Interactable", $"[InputManager] Collider2D hit at {worldPos} (GameObject: {hit.gameObject.name}), but no ITouchInputConsumer found.");
}
Debug.unityLogger.Log("Interactable", $"[InputManager] Collider2D hit at {worldPos} (GameObject: {hit.gameObject.name}), but no ITouchInputConsumer found.");
}
else
{

View File

@@ -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.
/// </summary>
public class PlayerTouchController : Character, ITouchInputConsumer
public class PlayerTouchController : MonoBehaviour, ITouchInputConsumer
{
// --- Movement State ---
private Vector3 targetPosition;

View File

@@ -1,45 +0,0 @@
using UnityEngine;
/// <summary>
/// Interaction requirement that allows combining the follower's held item with this pickup if a valid combination rule exists.
/// </summary>
[RequireComponent(typeof(Pickup))]
public class CombineWithBehavior : InteractionRequirementBase
{
/// <summary>
/// Attempts to combine the follower's held item with this pickup's item.
/// </summary>
/// <param name="follower">The follower attempting the interaction.</param>
/// <returns>True if the combination was successful, false otherwise.</returns>
public override bool TryInteract(FollowerController follower)
{
var heldItem = follower.GetHeldPickupObject();
var pickup = GetComponent<Pickup>();
if (heldItem == null)
{
// DebugUIMessage.Show("You need an item to combine.");
OnFailure?.Invoke();
return true;
}
if (pickup == null || pickup.itemData == null)
{
DebugUIMessage.Show("Target item is missing or invalid.");
OnFailure?.Invoke();
return false;
}
var combinedItem = InteractionOrchestrator.Instance.CombineItems(heldItem, pickup.gameObject);
if (combinedItem != null)
{
InteractionOrchestrator.Instance.PickupItem(follower, combinedItem);
follower.justCombined = true;
OnSuccess?.Invoke();
return true;
}
else
{
// DebugUIMessage.Show($"Cannot combine {follower.CurrentlyHeldItem?.itemName ?? "an item"} with {pickup.itemData.itemName ?? "target item"}.");
OnFailure?.Invoke();
return true;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 21401a3b30134380bb205964d9e5c67d
timeCreated: 1756981777

View File

@@ -1,41 +1,230 @@
using UnityEngine;
using Input;
using UnityEngine;
using System;
using UnityEngine.Events;
/// <summary>
/// Represents an interactable object that can respond to tap input events.
/// </summary>
public class Interactable : MonoBehaviour, ITouchInputConsumer
namespace Interactions
{
public event Action StartedInteraction;
public event Action<bool> InteractionComplete;
/// <summary>
/// Handles tap input. Triggers interaction logic.
/// </summary>
public void OnTap(Vector2 worldPosition)
public enum CharacterToInteract
{
Debug.Log($"[Interactable] OnTap at {worldPosition} on {gameObject.name}");
StartedInteraction?.Invoke();
Trafalgar,
Pulver
}
// No hold behavior for interactables.
public void OnHoldStart(Vector2 worldPosition) { }
public void OnHoldMove(Vector2 worldPosition) { }
public void OnHoldEnd(Vector2 worldPosition) { }
/// <summary>
/// Called to interact with this object by a character (player, follower, etc).
/// Represents an interactable object that can respond to tap input events.
/// </summary>
public virtual void OnInteract(Character character)
public class Interactable : MonoBehaviour, ITouchInputConsumer
{
// 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);
}
[Header("Interaction Settings")]
public bool isOneTime = false;
public float cooldown = -1f;
public CharacterToInteract characterToInteract = CharacterToInteract.Pulver;
public void CompleteInteraction(bool success)
{
InteractionComplete?.Invoke(success);
[Header("Interaction Events")]
public UnityEvent<PlayerTouchController, FollowerController> interactionStarted;
public UnityEvent interactionInterrupted;
public UnityEvent characterArrived;
public UnityEvent<bool> interactionComplete;
// Helpers for managing interaction state
private bool _interactionInProgress;
private PlayerTouchController _playerRef;
private FollowerController _followerController;
private bool _isActive = true;
private void Awake()
{
// Subscribe to interactionComplete event
interactionComplete.AddListener(OnInteractionComplete);
}
/// <summary>
/// Handles tap input. Triggers interaction logic.
/// </summary>
public void OnTap(Vector2 worldPosition)
{
if (!_isActive)
{
Debug.Log($"[Interactable] Is disabled!");
return;
}
Debug.Log($"[Interactable] OnTap at {worldPosition} on {gameObject.name}");
// Broadcast interaction started event
TryInteract();
}
public void TryInteract()
{
_interactionInProgress = true;
_playerRef = FindFirstObjectByType<PlayerTouchController>();
_followerController = FindFirstObjectByType<FollowerController>();
interactionStarted?.Invoke(_playerRef, _followerController);
if (_playerRef == null)
{
Debug.Log($"[Interactable] Player character could not be found. Aborting interaction.");
interactionInterrupted.Invoke();
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;
// Unsubscribe previous to avoid duplicate calls
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
_playerRef.OnArrivedAtTarget += OnPlayerArrived;
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelled;
_playerRef.MoveToAndNotify(stopPoint);
}
private void OnPlayerMoveCancelled()
{
_interactionInProgress = false;
interactionInterrupted?.Invoke();
}
private void OnPlayerArrived()
{
if (!_interactionInProgress)
return;
// Unsubscribe to avoid memory leaks
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
if (characterToInteract == CharacterToInteract.Pulver)
{
_followerController.OnPickupArrived -= OnFollowerArrived;
_followerController.OnPickupArrived += OnFollowerArrived;
_followerController.GoToPointAndReturn(transform.position, _playerRef.transform);
}
else if (characterToInteract == CharacterToInteract.Trafalgar)
{
BroadcastCharacterArrived();
}
}
private void OnFollowerArrived()
{
if (!_interactionInProgress)
return;
// Unsubscribe to avoid memory leaks
_followerController.OnPickupArrived -= OnFollowerArrived;
BroadcastCharacterArrived();
}
private void BroadcastCharacterArrived()
{
// Check for ObjectiveStepBehaviour and lock state
var step = GetComponent<PuzzleS.ObjectiveStepBehaviour>();
if (step != null && !step.IsStepUnlocked())
{
DebugUIMessage.Show("This step is locked!", 2f);
BroadcastInteractionComplete(false);
// Reset variables for next time
_interactionInProgress = false;
_playerRef = null;
_followerController = null;
return;
}
// Broadcast appropriate event
characterArrived?.Invoke();
// Reset variables for next time
_interactionInProgress = false;
_playerRef = null;
_followerController = null;
}
private void OnInteractionComplete(bool success)
{
if (success)
{
if (isOneTime)
{
_isActive = false;
}
else if (cooldown >= 0f)
{
StartCoroutine(HandleCooldown());
}
}
}
private System.Collections.IEnumerator HandleCooldown()
{
_isActive = false;
yield return new WaitForSeconds(cooldown);
_isActive = true;
}
public void OnHoldStart(Vector2 position)
{
throw new NotImplementedException();
}
public void OnHoldMove(Vector2 position)
{
throw new NotImplementedException();
}
public void OnHoldEnd(Vector2 position)
{
throw new NotImplementedException();
}
public void BroadcastInteractionComplete(bool success)
{
interactionComplete?.Invoke(success);
}
#if UNITY_EDITOR
/// <summary>
/// Draws gizmos for pickup interaction range in the editor.
/// </summary>
void OnDrawGizmos()
{
float playerStopDistance;
if (Application.isPlaying)
{
playerStopDistance = characterToInteract == CharacterToInteract.Trafalgar
? GameManager.Instance.PlayerStopDistanceDirectInteraction
: GameManager.Instance.PlayerStopDistance;
}
else
{
// Load settings directly from asset path in editor
var settings =
UnityEditor.AssetDatabase.LoadAssetAtPath<GameSettings>(
"Assets/Data/Settings/DefaultSettings.asset");
playerStopDistance = settings != null
? (characterToInteract == CharacterToInteract.Trafalgar
? settings.playerStopDistanceDirectInteraction
: settings.playerStopDistance)
: 1.0f;
}
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, playerStopDistance);
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null)
{
Vector3 stopPoint = transform.position +
(playerObj.transform.position - transform.position).normalized * playerStopDistance;
Gizmos.color = Color.cyan;
Gizmos.DrawSphere(stopPoint, 0.15f);
}
}
#endif
}
}

View File

@@ -1,218 +0,0 @@
using System;
using UnityEngine;
/// <summary>
/// Handles the process of moving characters to interactables and orchestrating interactions.
/// </summary>
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<InteractionOrchestrator>();
if (_instance == null)
{
var go = new GameObject("InteractionOrchestrator");
_instance = go.AddComponent<InteractionOrchestrator>();
// 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;
/// <summary>
/// Request an interaction between a character and an interactable.
/// </summary>
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<FollowerController>();
}
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;
}
/// <summary>
/// Called when the character has arrived at the interactable.
/// </summary>
private void OnCharacterArrived(Interactable interactable, Character character)
{
// Check all requirements
var requirements = interactable.GetComponents<InteractionRequirementBase>();
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);
}
}
// --- ITEM MANAGEMENT API ---
public void PickupItem(FollowerController follower, GameObject item)
{
if (item == null || follower == null) return;
item.SetActive(false);
item.transform.SetParent(null);
follower.SetHeldItemFromObject(item);
// Optionally: fire event, update UI, etc.
}
public void DropItem(FollowerController follower, Vector3 position)
{
var item = follower.GetHeldPickupObject();
if (item == null) return;
item.transform.position = position;
item.transform.SetParent(null);
item.SetActive(true);
follower.ClearHeldItem();
// Optionally: fire event, update UI, etc.
}
public void SlotItem(SlotItemBehavior slot, GameObject item)
{
if (slot == null || item == null) return;
item.SetActive(false);
item.transform.SetParent(null);
slot.SetSlottedObject(item);
// Optionally: update visuals, fire event, etc.
}
public void SwapItems(FollowerController follower, SlotItemBehavior slot)
{
var heldObj = follower.GetHeldPickupObject();
var slotObj = slot.GetSlottedObject();
// 1. Slot the follower's held object
SlotItem(slot, heldObj);
// 2. Give the slot's object to the follower
PickupItem(follower, slotObj);
}
public GameObject CombineItems(GameObject itemA, GameObject itemB)
{
if (itemA == null || itemB == null) return null;
var pickupA = itemA.GetComponent<Pickup>();
var pickupB = itemB.GetComponent<Pickup>();
if (pickupA == null || pickupB == null) {
if (itemA != null) Destroy(itemA);
if (itemB != null) Destroy(itemB);
return null;
}
var rule = GameManager.Instance.GetCombinationRule(pickupA.itemData, pickupB.itemData);
Vector3 spawnPos = itemA.transform.position;
if (rule != null && rule.resultPrefab != null) {
var newItem = Instantiate(rule.resultPrefab, spawnPos, Quaternion.identity);
Destroy(itemA);
Destroy(itemB);
return newItem;
}
// If no combination found, just destroy both
Destroy(itemA);
Destroy(itemB);
return null;
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 705c4ee7f8204cc68aacd79e2a4a506d
timeCreated: 1757513080

View File

@@ -0,0 +1,145 @@
using System.Collections.Generic;
using UnityEngine;
namespace Interactions
{
/// <summary>
/// Interaction requirement that allows slotting, swapping, or picking up items in a slot.
/// </summary>
[RequireComponent(typeof(Interactable))]
public class ItemSlot : Pickup
{
private PickupItemData _currentlySlottedItemData;
public SpriteRenderer slottedItemRenderer;
private GameObject _currentlySlottedItemObject = null;
public GameObject GetSlottedObject()
{
return _currentlySlottedItemObject;
}
public void SetSlottedObject(GameObject obj)
{
_currentlySlottedItemObject = obj;
if (_currentlySlottedItemObject != null)
{
_currentlySlottedItemObject.SetActive(false);
}
}
protected override void OnCharacterArrived()
{
var heldItemData = FollowerController.CurrentlyHeldItemData;
var heldItemObj = FollowerController.GetHeldPickupObject();
var pickup = GetComponent<Pickup>();
var slotItem = pickup != null ? pickup.itemData : null;
var config = GameManager.Instance.GetSlotItemConfig(slotItem);
var allowed = config?.allowedItems ?? new List<PickupItemData>();
var forbidden = config?.forbiddenItems ?? new List<PickupItemData>();
if ((heldItemData == null && _currentlySlottedItemObject != null)
|| (heldItemData != null && _currentlySlottedItemObject != null))
{
FollowerController.TryPickupItem(_currentlySlottedItemObject, _currentlySlottedItemData);
_currentlySlottedItemObject = null;
_currentlySlottedItemData = null;
UpdateSlottedSprite();
return;
}
// // CASE 1: No held item, slot has item -> pick up slotted item
// if (heldItemData == null && _cachedSlottedObject != null)
// {
// InteractionOrchestrator.Instance.PickupItem(FollowerController, _cachedSlottedObject);
// _cachedSlottedObject = null;
// currentlySlottedItem = null;
// UpdateSlottedSprite();
// Interactable.BroadcastInteractionComplete(false);
// return;
// }
// // CASE 2: Held item, slot has item -> swap
// if
// {
// InteractionOrchestrator.Instance.SwapItems(FollowerController, this);
// currentlySlottedItem = heldItemData;
// UpdateSlottedSprite();
// return;
// }
// CASE 3: Held item, slot empty -> slot the held item
if (heldItemData != null && _currentlySlottedItemObject == null)
{
if (forbidden.Contains(heldItemData))
{
DebugUIMessage.Show("Can't place that here.");
Interactable.BroadcastInteractionComplete(false);
return;
}
SlotItem(heldItemObj, heldItemData);
if (allowed.Contains(heldItemData))
{
Interactable.BroadcastInteractionComplete(true);
return;
}
else
{
DebugUIMessage.Show("I'm not sure this works.");
Interactable.BroadcastInteractionComplete(false);
return;
}
}
// CASE 4: No held item, slot empty -> show warning
if (heldItemData == null && _currentlySlottedItemObject == null)
{
DebugUIMessage.Show("This requires an item.");
return;
}
return;
}
/// <summary>
/// Updates the sprite and scale for the currently slotted item.
/// </summary>
private void UpdateSlottedSprite()
{
if (slottedItemRenderer != null && _currentlySlottedItemData != null && _currentlySlottedItemData.mapSprite != null)
{
slottedItemRenderer.sprite = _currentlySlottedItemData.mapSprite;
// Scale sprite to desired height, preserve aspect ratio, compensate for parent scale
float desiredHeight = GameManager.Instance.HeldIconDisplayHeight;
var sprite = _currentlySlottedItemData.mapSprite;
float spriteHeight = sprite.bounds.size.y;
float spriteWidth = sprite.bounds.size.x;
Vector3 parentScale = slottedItemRenderer.transform.parent != null
? slottedItemRenderer.transform.parent.localScale
: Vector3.one;
if (spriteHeight > 0f)
{
float uniformScale = desiredHeight / spriteHeight;
float scale = uniformScale / Mathf.Max(parentScale.x, parentScale.y);
slottedItemRenderer.transform.localScale = new Vector3(scale, scale, 1f);
}
}
else if (slottedItemRenderer != null)
{
slottedItemRenderer.sprite = null;
}
}
public void SlotItem(GameObject itemToSlot, PickupItemData itemToSlotData)
{
if (itemToSlot == null)
return;
itemToSlot.SetActive(false);
itemToSlot.transform.SetParent(null);
SetSlottedObject(itemToSlot);
_currentlySlottedItemData = itemToSlotData;
UpdateSlottedSprite();
FollowerController.ClearHeldItem();
}
}
}

View File

@@ -1,5 +1,7 @@
using UnityEngine;
using System;
using Input;
using Interactions;
/// <summary>
/// MonoBehaviour that immediately completes an interaction when started.
@@ -13,7 +15,7 @@ public class OneClickInteraction : MonoBehaviour
interactable = GetComponent<Interactable>();
if (interactable != null)
{
interactable.StartedInteraction += OnStartedInteraction;
interactable.interactionStarted.AddListener(OnInteractionStarted);
}
}
@@ -21,15 +23,15 @@ public class OneClickInteraction : MonoBehaviour
{
if (interactable != null)
{
interactable.StartedInteraction -= OnStartedInteraction;
interactable.interactionStarted.RemoveListener(OnInteractionStarted);
}
}
private void OnStartedInteraction()
private void OnInteractionStarted(PlayerTouchController playerRef, FollowerController followerRef)
{
if (interactable != null)
{
interactable.CompleteInteraction(true);
interactable.BroadcastInteractionComplete(true);
}
}
}

View File

@@ -1,115 +1,95 @@
using Input;
using UnityEngine;
public class Pickup : MonoBehaviour
namespace Interactions
{
/// <summary>
/// Data for the pickup item (icon, name, etc).
/// </summary>
public PickupItemData itemData;
/// <summary>
/// Renderer for the pickup icon.
/// </summary>
public SpriteRenderer iconRenderer;
private Interactable interactable;
/// <summary>
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
/// </summary>
void Awake()
[RequireComponent(typeof(Interactable))]
public class Pickup : MonoBehaviour
{
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
interactable = GetComponent<Interactable>();
if (interactable != null)
{
interactable.StartedInteraction += OnStartedInteraction;
interactable.InteractionComplete += OnInteractionComplete;
}
ApplyItemData();
}
public PickupItemData itemData;
public SpriteRenderer iconRenderer;
protected Interactable Interactable;
private PlayerTouchController _playerRef;
protected FollowerController FollowerController;
/// <summary>
/// Unity OnDestroy callback. Cleans up event handlers.
/// </summary>
void OnDestroy()
{
if (interactable != null)
/// <summary>
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
/// </summary>
void Awake()
{
interactable.StartedInteraction -= OnStartedInteraction;
interactable.InteractionComplete -= OnInteractionComplete;
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
Interactable = GetComponent<Interactable>();
if (Interactable != null)
{
Interactable.interactionStarted.AddListener(OnInteractionStarted);
Interactable.characterArrived.AddListener(OnCharacterArrived);
}
ApplyItemData();
}
/// <summary>
/// Unity OnDestroy callback. Cleans up event handlers.
/// </summary>
void OnDestroy()
{
if (Interactable != null)
{
Interactable.interactionStarted.RemoveListener(OnInteractionStarted);
Interactable.characterArrived.RemoveListener(OnCharacterArrived);
}
}
}
#if UNITY_EDITOR
/// <summary>
/// Unity OnValidate callback. Ensures icon and data are up to date in editor.
/// </summary>
void OnValidate()
{
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
ApplyItemData();
}
/// <summary>
/// Draws gizmos for pickup interaction range in the editor.
/// </summary>
void OnDrawGizmos()
{
float playerStopDistance;
if (Application.isPlaying)
/// <summary>
/// Unity OnValidate callback. Ensures icon and data are up to date in editor.
/// </summary>
void OnValidate()
{
playerStopDistance = GameManager.Instance.PlayerStopDistance;
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
ApplyItemData();
}
else
{
// Load settings directly from asset path in editor
var settings = UnityEditor.AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Data/Settings/DefaultSettings.asset");
playerStopDistance = settings != null ? settings.playerStopDistance : 1.0f;
}
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, playerStopDistance);
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null)
{
Vector3 stopPoint = transform.position + (playerObj.transform.position - transform.position).normalized * playerStopDistance;
Gizmos.color = Color.cyan;
Gizmos.DrawSphere(stopPoint, 0.15f);
}
}
#endif
/// <summary>
/// Applies the item data to the pickup (icon, name, etc).
/// </summary>
public void ApplyItemData()
{
if (itemData != null)
/// <summary>
/// Applies the item data to the pickup (icon, name, etc).
/// </summary>
public void ApplyItemData()
{
if (iconRenderer != null && itemData.mapSprite != null)
if (itemData != null)
{
iconRenderer.sprite = itemData.mapSprite;
if (iconRenderer != null && itemData.mapSprite != null)
{
iconRenderer.sprite = itemData.mapSprite;
}
gameObject.name = itemData.itemName;
}
gameObject.name = itemData.itemName;
}
/// <summary>
/// Handles the start of an interaction (for feedback/UI only).
/// </summary>
private void OnInteractionStarted(PlayerTouchController playerRef, FollowerController followerRef)
{
_playerRef = playerRef;
FollowerController = followerRef;
}
protected virtual void OnCharacterArrived()
{
var combinationResult = FollowerController.TryCombineItems(this, out var combinationResultItem);
if (combinationResultItem != null)
{
Interactable.BroadcastInteractionComplete(true);
return;
}
FollowerController?.TryPickupItem(gameObject, itemData);
Interactable.BroadcastInteractionComplete(combinationResult == FollowerController.CombinationResult.NotApplicable);
}
}
/// <summary>
/// Handles the start of an interaction (for feedback/UI only).
/// </summary>
private void OnStartedInteraction()
{
// Optionally, add pickup-specific feedback here (e.g., highlight, sound).
}
/// <summary>
/// Handles completion of the interaction (e.g., after pickup is done).
/// </summary>
/// <param name="success">Whether the interaction was successful.</param>
private void OnInteractionComplete(bool success)
{
if (!success) return;
// Optionally, add logic to disable the pickup or provide feedback
}
}

View File

@@ -1,41 +0,0 @@
using UnityEngine;
/// <summary>
/// Interaction requirement that checks if the follower is holding a specific required item.
/// </summary>
[RequireComponent(typeof(Interactable))]
public class RequiresItemBehavior : InteractionRequirementBase
{
[Header("Required Item")]
public PickupItemData requiredItem;
/// <summary>
/// Attempts to interact, succeeds only if the follower is holding the required item.
/// </summary>
/// <param name="follower">The follower attempting the interaction.</param>
/// <returns>True if the interaction was successful, false otherwise.</returns>
public override bool TryInteract(FollowerController follower)
{
var heldItem = follower.CurrentlyHeldItem;
if (heldItem == requiredItem)
{
OnSuccess?.Invoke();
return true;
}
else
{
string requiredName = requiredItem != null ? requiredItem.itemName : "required item";
if (heldItem == null)
{
DebugUIMessage.Show($"You need {requiredName} to interact.");
}
else
{
string heldName = heldItem.itemName ?? "an item";
DebugUIMessage.Show($"You need {requiredName}, but you are holding {heldName}.");
}
OnFailure?.Invoke();
return false;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 31103d67032c44a9b95ec014babe2c62
timeCreated: 1756981777

View File

@@ -1,131 +0,0 @@
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
/// <summary>
/// Interaction requirement that allows slotting, swapping, or picking up items in a slot.
/// </summary>
[RequireComponent(typeof(Interactable))]
[RequireComponent(typeof(Pickup))]
public class SlotItemBehavior : InteractionRequirementBase
{
[Header("Slot State")]
/// <summary>
/// The item currently slotted in this slot.
/// </summary>
public PickupItemData currentlySlottedItem;
/// <summary>
/// The renderer for the slotted item's sprite.
/// </summary>
public SpriteRenderer slottedItemRenderer;
private GameObject _cachedSlottedObject = null;
public GameObject GetSlottedObject()
{
return _cachedSlottedObject;
}
public void SetSlottedObject(GameObject obj)
{
_cachedSlottedObject = obj;
if (_cachedSlottedObject != null)
{
_cachedSlottedObject.SetActive(false);
}
}
/// <summary>
/// Attempts to interact with the slot, handling slotting, swapping, or picking up items.
/// </summary>
/// <param name="follower">The follower attempting the interaction.</param>
/// <returns>True if the interaction was successful, false otherwise.</returns>
public override bool TryInteract(FollowerController follower)
{
var heldItem = follower.CurrentlyHeldItem;
var heldObj = follower.GetHeldPickupObject();
var pickup = GetComponent<Pickup>();
var slotItem = pickup != null ? pickup.itemData : null;
var config = GameManager.Instance.GetSlotItemConfig(slotItem);
var allowed = config?.allowedItems ?? new List<PickupItemData>();
var forbidden = config?.forbiddenItems ?? new List<PickupItemData>();
// CASE 1: No held item, slot has item -> pick up slotted item
if (heldItem == null && _cachedSlottedObject != null)
{
InteractionOrchestrator.Instance.PickupItem(follower, _cachedSlottedObject);
_cachedSlottedObject = null;
currentlySlottedItem = null;
UpdateSlottedSprite();
return true;
}
// CASE 2: Held item, slot has item -> swap
if (heldItem != null && _cachedSlottedObject != null)
{
InteractionOrchestrator.Instance.SwapItems(follower, this);
currentlySlottedItem = heldItem;
UpdateSlottedSprite();
return true;
}
// CASE 3: Held item, slot empty -> slot the held item
if (heldItem != null && _cachedSlottedObject == null)
{
if (forbidden.Contains(heldItem))
{
DebugUIMessage.Show("Can't place that here.");
return false;
}
InteractionOrchestrator.Instance.SlotItem(this, heldObj);
currentlySlottedItem = heldItem;
UpdateSlottedSprite();
follower.ClearHeldItem();
if (allowed.Contains(heldItem))
{
OnSuccess?.Invoke();
return true;
}
else
{
DebugUIMessage.Show("I'm not sure this works.");
OnFailure?.Invoke();
return true;
}
}
// CASE 4: No held item, slot empty -> show warning
if (heldItem == null && _cachedSlottedObject == null)
{
DebugUIMessage.Show("This requires an item.");
return false;
}
return false;
}
/// <summary>
/// Updates the sprite and scale for the currently slotted item.
/// </summary>
private void UpdateSlottedSprite()
{
if (slottedItemRenderer != null && currentlySlottedItem != null && currentlySlottedItem.mapSprite != null)
{
slottedItemRenderer.sprite = currentlySlottedItem.mapSprite;
// Scale sprite to desired height, preserve aspect ratio, compensate for parent scale
float desiredHeight = GameManager.Instance.HeldIconDisplayHeight;
var sprite = currentlySlottedItem.mapSprite;
float spriteHeight = sprite.bounds.size.y;
float spriteWidth = sprite.bounds.size.x;
Vector3 parentScale = slottedItemRenderer.transform.parent != null
? slottedItemRenderer.transform.parent.localScale
: Vector3.one;
if (spriteHeight > 0f)
{
float uniformScale = desiredHeight / spriteHeight;
float scale = uniformScale / Mathf.Max(parentScale.x, parentScale.y);
slottedItemRenderer.transform.localScale = new Vector3(scale, scale, 1f);
}
}
else if (slottedItemRenderer != null)
{
slottedItemRenderer.sprite = null;
}
}
}

View File

@@ -1,4 +1,6 @@
using System;
using Input;
using Interactions;
using UnityEngine;
/// <summary>
@@ -26,7 +28,7 @@ public class LevelSwitch : MonoBehaviour
_interactable = GetComponent<Interactable>();
if (_interactable != null)
{
_interactable.StartedInteraction += OnStartedInteraction;
_interactable.characterArrived.AddListener(OnCharacterArrived);
}
ApplySwitchData();
}
@@ -38,7 +40,7 @@ public class LevelSwitch : MonoBehaviour
{
if (_interactable != null)
{
_interactable.StartedInteraction -= OnStartedInteraction;
_interactable.characterArrived.RemoveListener(OnCharacterArrived);
}
}
@@ -71,7 +73,7 @@ public class LevelSwitch : MonoBehaviour
/// <summary>
/// Handles the start of an interaction (shows confirmation menu and switches the level if confirmed).
/// </summary>
private void OnStartedInteraction()
private void OnCharacterArrived()
{
if (switchData == null || string.IsNullOrEmpty(switchData.targetLevelSceneName) || !_isActive)
return;

View File

@@ -1,4 +1,5 @@
using UnityEngine;
using Interactions;
using UnityEngine;
using Pathfinding;
using UnityEngine.SceneManagement;
using Utils;
@@ -6,7 +7,7 @@ using Utils;
/// <summary>
/// Controls the follower character, including following the player, handling pickups, and managing held items.
/// </summary>
public class FollowerController : Character
public class FollowerController: MonoBehaviour
{
[Header("Follower Settings")]
public bool debugDrawTarget = true;
@@ -29,10 +30,13 @@ public class FollowerController : Character
private float _currentSpeed = 0f;
private Animator _animator;
private Transform _artTransform;
private SpriteRenderer spriteRenderer;
private SpriteRenderer _spriteRenderer;
private PickupItemData _currentlyHeldItemData;
public PickupItemData CurrentlyHeldItemData => _currentlyHeldItemData;
private GameObject _cachedPickupObject = null;
public bool justCombined = false;
private PickupItemData _currentlyHeldItem;
public PickupItemData CurrentlyHeldItem => _currentlyHeldItem;
/// <summary>
/// Renderer for the held item icon.
/// </summary>
@@ -54,27 +58,9 @@ public class FollowerController : Character
/// </summary>
public event FollowerPickupHandler OnPickupReturned;
private Coroutine _pickupCoroutine;
private bool _lastInteractionSuccess = true;
/// <summary>
/// Cache for the currently picked-up GameObject (hidden while held).
/// </summary>
private GameObject _cachedPickupObject = null;
public bool justCombined = false;
/// <summary>
/// Caches the given pickup object as the currently held item, hides it, and parents it to the follower.
/// </summary>
public void CacheHeldPickupObject(GameObject obj)
{
// Do not destroy the previous object; just replace and hide
_cachedPickupObject = obj;
if (_cachedPickupObject != null)
{
_cachedPickupObject.SetActive(false);
}
}
void Awake()
{
@@ -84,12 +70,12 @@ public class FollowerController : Character
if (_artTransform != null)
{
_animator = _artTransform.GetComponent<Animator>();
spriteRenderer = _artTransform.GetComponent<SpriteRenderer>();
_spriteRenderer = _artTransform.GetComponent<SpriteRenderer>();
}
else
{
_animator = GetComponentInChildren<Animator>(); // fallback
spriteRenderer = GetComponentInChildren<SpriteRenderer>();
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
}
}
@@ -109,37 +95,6 @@ public class FollowerController : Character
FindPlayerReference();
}
void UpdateFollowTarget()
{
if (_playerTransform == null)
{
FindPlayerReference();
if (_playerTransform == null)
return;
}
if (_isManualFollowing)
{
Vector3 playerPos = _playerTransform.position;
Vector3 moveDir = Vector3.zero;
if (_playerAIPath != null && _playerAIPath.velocity.magnitude > 0.01f)
{
moveDir = _playerAIPath.velocity.normalized;
_lastMoveDir = moveDir;
}
else
{
moveDir = _lastMoveDir;
}
// Use GameSettings for followDistance
_targetPoint = playerPos - moveDir * GameManager.Instance.FollowDistance;
_targetPoint.z = 0;
if (_aiPath != null)
{
_aiPath.enabled = false;
}
}
}
void Update()
{
if (_playerTransform == null)
@@ -185,12 +140,12 @@ public class FollowerController : Character
}
Vector3 dir = (_targetPoint - transform.position).normalized;
// Sprite flipping based on movement direction
if (spriteRenderer != null && dir.sqrMagnitude > 0.001f)
if (_spriteRenderer != null && dir.sqrMagnitude > 0.001f)
{
if (dir.x > 0.01f)
spriteRenderer.flipX = false;
_spriteRenderer.flipX = false;
else if (dir.x < -0.01f)
spriteRenderer.flipX = true;
_spriteRenderer.flipX = true;
}
transform.position += dir * _currentSpeed * Time.deltaTime;
}
@@ -214,12 +169,12 @@ public class FollowerController : Character
{
normalizedSpeed = _aiPath.velocity.magnitude / _followerMaxSpeed;
// Sprite flipping for pathfinding mode
if (spriteRenderer != null && _aiPath.velocity.sqrMagnitude > 0.001f)
if (_spriteRenderer != null && _aiPath.velocity.sqrMagnitude > 0.001f)
{
if (_aiPath.velocity.x > 0.01f)
spriteRenderer.flipX = false;
_spriteRenderer.flipX = false;
else if (_aiPath.velocity.x < -0.01f)
spriteRenderer.flipX = true;
_spriteRenderer.flipX = true;
}
}
_animator.SetFloat("Speed", Mathf.Clamp01(normalizedSpeed));
@@ -247,6 +202,43 @@ public class FollowerController : Character
}
}
#region Movement
/// <summary>
/// Updates the follower's target point to follow the player at a specified distance,
/// using the player's current movement direction if available. Disables pathfinding
/// when in manual following mode.
/// </summary>
void UpdateFollowTarget()
{
if (_playerTransform == null)
{
FindPlayerReference();
if (_playerTransform == null)
return;
}
if (_isManualFollowing)
{
Vector3 playerPos = _playerTransform.position;
Vector3 moveDir = Vector3.zero;
if (_playerAIPath != null && _playerAIPath.velocity.magnitude > 0.01f)
{
moveDir = _playerAIPath.velocity.normalized;
_lastMoveDir = moveDir;
}
else
{
moveDir = _lastMoveDir;
}
// Use GameSettings for followDistance
_targetPoint = playerPos - moveDir * GameManager.Instance.FollowDistance;
_targetPoint.z = 0;
if (_aiPath != null)
{
_aiPath.enabled = false;
}
}
}
// Command follower to go to a specific point (pathfinding mode)
/// <summary>
/// Command follower to go to a specific point (pathfinding mode).
@@ -278,6 +270,98 @@ public class FollowerController : Character
_pickupCoroutine = StartCoroutine(PickupSequence(itemPosition, playerTransform));
}
private System.Collections.IEnumerator PickupSequence(Vector2 itemPosition, Transform playerTransform)
{
_isManualFollowing = false;
_isReturningToPlayer = false;
if (_aiPath != null)
{
_aiPath.enabled = true;
_aiPath.maxSpeed = _followerMaxSpeed;
_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)
{
yield return null;
}
OnPickupArrived?.Invoke();
// Wait briefly, then return to player
yield return new WaitForSeconds(0.2f);
if (_aiPath != null && playerTransform != null)
{
_aiPath.maxSpeed = _followerMaxSpeed;
_aiPath.destination = playerTransform.position;
}
_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)
{
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
public void TryPickupItem(GameObject itemObject, PickupItemData itemData)
{
if (_currentlyHeldItemData != null && _cachedPickupObject != null)
{
// Drop the currently held item at the current position
DropHeldItemAt(transform.position);
}
// Pick up the new item
SetHeldItem(itemData, itemObject.GetComponent<SpriteRenderer>());
_cachedPickupObject = itemObject;
_cachedPickupObject.SetActive(false);
}
public enum CombinationResult
{
Successful,
Unsuccessful,
NotApplicable
}
public CombinationResult TryCombineItems(Pickup pickupA, out GameObject newItem)
{
newItem = null;
if (_cachedPickupObject == null)
{
return CombinationResult.NotApplicable;
}
Pickup pickupB = _cachedPickupObject.GetComponent<Pickup>();
if (pickupA == null || pickupB == null)
{
return CombinationResult.NotApplicable;
}
var rule = GameManager.Instance.GetCombinationRule(pickupA.itemData, pickupB.itemData);
Vector3 spawnPos = pickupA.gameObject.transform.position;
if (rule != null && rule.resultPrefab != null)
{
newItem = Instantiate(rule.resultPrefab, spawnPos, Quaternion.identity);
PickupItemData itemData = newItem.GetComponent<Pickup>().itemData;
Destroy(pickupA.gameObject);
Destroy(pickupB.gameObject);
TryPickupItem(newItem,itemData);
return CombinationResult.Successful;
}
// If no combination found, return Unsuccessful
return CombinationResult.Unsuccessful;
}
/// <summary>
/// Set the item held by the follower, copying all visual properties from the Pickup's SpriteRenderer.
/// </summary>
@@ -285,10 +369,10 @@ public class FollowerController : Character
/// <param name="pickupRenderer">The SpriteRenderer from the Pickup to copy appearance from.</param>
public void SetHeldItem(PickupItemData itemData, SpriteRenderer pickupRenderer = null)
{
_currentlyHeldItem = itemData;
_currentlyHeldItemData = itemData;
if (heldObjectRenderer != null)
{
if (_currentlyHeldItem != null && pickupRenderer != null)
if (_currentlyHeldItemData != null && pickupRenderer != null)
{
AppleHillsUtils.CopySpriteRendererProperties(pickupRenderer, heldObjectRenderer);
}
@@ -300,15 +384,6 @@ public class FollowerController : Character
}
}
/// <summary>
/// Set the result of the last interaction (success or failure).
/// </summary>
/// <param name="success">True if the last interaction was successful, false otherwise.</param>
public void SetInteractionResult(bool success)
{
_lastInteractionSuccess = success;
}
public GameObject GetHeldPickupObject()
{
return _cachedPickupObject;
@@ -336,7 +411,7 @@ public class FollowerController : Character
public void ClearHeldItem()
{
_cachedPickupObject = null;
_currentlyHeldItem = null;
_currentlyHeldItemData = null;
if (heldObjectRenderer != null)
{
heldObjectRenderer.sprite = null;
@@ -344,83 +419,26 @@ public class FollowerController : Character
}
}
public void DropItem(FollowerController follower, Vector3 position)
{
var item = follower.GetHeldPickupObject();
if (item == null) return;
item.transform.position = position;
item.transform.SetParent(null);
item.SetActive(true);
follower.ClearHeldItem();
// Optionally: fire event, update UI, etc.
}
public void DropHeldItemAt(Vector3 position)
{
InteractionOrchestrator.Instance.DropItem(this, position);
DropItem(this, position);
}
private System.Collections.IEnumerator PickupSequence(Vector2 itemPosition, Transform playerTransform)
{
_isManualFollowing = false;
_isReturningToPlayer = false;
if (_aiPath != null)
{
_aiPath.enabled = true;
_aiPath.maxSpeed = _followerMaxSpeed;
_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)
{
yield return null;
}
OnPickupArrived?.Invoke();
// Only perform pickup/swap logic if interaction succeeded
if (_lastInteractionSuccess && heldObjectRenderer != null)
{
Collider2D[] hits = Physics2D.OverlapCircleAll(itemPosition, 0.2f);
foreach (var hit in hits)
{
var pickup = hit.GetComponent<Pickup>();
if (pickup != null)
{
var slotBehavior = pickup.GetComponent<SlotItemBehavior>();
if (slotBehavior != null)
{
// Slot item: orchestrator handles slotting
break;
}
if (justCombined)
{
InteractionOrchestrator.Instance.CombineItems(pickup.gameObject, _cachedPickupObject);
justCombined = false;
break;
}
// Swap logic: if holding an item, drop it here
if (_currentlyHeldItem != null && _cachedPickupObject != null)
{
InteractionOrchestrator.Instance.DropItem(this, pickup.transform.position);
}
InteractionOrchestrator.Instance.PickupItem(this, pickup.gameObject);
break;
}
}
}
// Wait briefly, then return to player
yield return new WaitForSeconds(0.2f);
if (_aiPath != null && playerTransform != null)
{
_aiPath.maxSpeed = _followerMaxSpeed;
_aiPath.destination = playerTransform.position;
}
_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)
{
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 ItemInteractions
#if UNITY_EDITOR
void OnDrawGizmos()
{
if (debugDrawTarget && Application.isPlaying)
@@ -431,4 +449,5 @@ public class FollowerController : Character
Gizmos.DrawLine(transform.position, _targetPoint);
}
}
#endif
}

View File

@@ -1,93 +1,97 @@

using Input;
using Interactions;
using UnityEngine;
/// <summary>
/// Manages the state and interactions for a single puzzle step, including unlock/lock logic and event handling.
/// </summary>
[RequireComponent(typeof(Interactable))]
public class ObjectiveStepBehaviour : MonoBehaviour
namespace PuzzleS
{
/// <summary>
/// The data object representing this puzzle step.
/// Manages the state and interactions for a single puzzle step, including unlock/lock logic and event handling.
/// </summary>
public PuzzleStepSO stepData;
private Interactable interactable;
private bool isUnlocked = false;
void Awake()
[RequireComponent(typeof(Interactable))]
public class ObjectiveStepBehaviour : MonoBehaviour
{
interactable = GetComponent<Interactable>();
}
/// <summary>
/// The data object representing this puzzle step.
/// </summary>
public PuzzleStepSO stepData;
private Interactable _interactable;
private bool _isUnlocked = false;
void OnEnable()
{
if (interactable == null)
interactable = GetComponent<Interactable>();
if (interactable != null)
void Awake()
{
interactable.StartedInteraction += OnStartedInteraction;
interactable.InteractionComplete += OnInteractionComplete;
_interactable = GetComponent<Interactable>();
}
PuzzleManager.Instance?.RegisterStepBehaviour(this);
}
void OnDisable()
{
if (interactable != null)
void OnEnable()
{
interactable.StartedInteraction -= OnStartedInteraction;
interactable.InteractionComplete -= OnInteractionComplete;
if (_interactable == null)
_interactable = GetComponent<Interactable>();
if (_interactable != null)
{
_interactable.interactionStarted.AddListener(OnInteractionStarted);
_interactable.interactionComplete.AddListener(OnInteractionComplete);
}
PuzzleManager.Instance?.RegisterStepBehaviour(this);
}
PuzzleManager.Instance?.UnregisterStepBehaviour(this);
}
/// <summary>
/// Unlocks this puzzle step, allowing interaction.
/// </summary>
public void UnlockStep()
{
isUnlocked = true;
Debug.Log($"[Puzzles] Step unlocked: {stepData?.stepId} on {gameObject.name}");
// Optionally, show visual feedback for unlocked state
}
/// <summary>
/// Locks this puzzle step, preventing interaction.
/// </summary>
public void LockStep()
{
isUnlocked = false;
Debug.Log($"[Puzzles] Step locked: {stepData?.stepId} on {gameObject.name}");
// Optionally, show visual feedback for locked state
}
/// <summary>
/// Returns whether this step is currently unlocked.
/// </summary>
public bool IsStepUnlocked()
{
return isUnlocked;
}
/// <summary>
/// Handles the start of an interaction (can be used for visual feedback).
/// </summary>
private void OnStartedInteraction()
{
// Optionally handle started interaction (e.g. visual feedback)
}
/// <summary>
/// Handles completion of the interaction, notifies PuzzleManager if successful and unlocked.
/// </summary>
/// <param name="success">Whether the interaction was successful.</param>
private void OnInteractionComplete(bool success)
{
if (!isUnlocked) return;
if (success)
void OnDestroy()
{
Debug.Log($"[Puzzles] Step interacted: {stepData?.stepId} on {gameObject.name}");
PuzzleManager.Instance?.OnStepCompleted(stepData);
if (_interactable != null)
{
_interactable.interactionStarted.RemoveListener(OnInteractionStarted);
_interactable.interactionComplete.RemoveListener(OnInteractionComplete);
}
PuzzleManager.Instance?.UnregisterStepBehaviour(this);
}
/// <summary>
/// Unlocks this puzzle step, allowing interaction.
/// </summary>
public void UnlockStep()
{
_isUnlocked = true;
Debug.Log($"[Puzzles] Step unlocked: {stepData?.stepId} on {gameObject.name}");
// Optionally, show visual feedback for unlocked state
}
/// <summary>
/// Locks this puzzle step, preventing interaction.
/// </summary>
public void LockStep()
{
_isUnlocked = false;
Debug.Log($"[Puzzles] Step locked: {stepData?.stepId} on {gameObject.name}");
// Optionally, show visual feedback for locked state
}
/// <summary>
/// Returns whether this step is currently unlocked.
/// </summary>
public bool IsStepUnlocked()
{
return _isUnlocked;
}
/// <summary>
/// Handles the start of an interaction (can be used for visual feedback).
/// </summary>
private void OnInteractionStarted(PlayerTouchController playerRef, FollowerController followerRef)
{
// Optionally handle started interaction (e.g. visual feedback)
}
/// <summary>
/// Handles completion of the interaction, notifies PuzzleManager if successful and unlocked.
/// </summary>
/// <param name="success">Whether the interaction was successful.</param>
private void OnInteractionComplete(bool success)
{
if (!_isUnlocked) return;
if (success)
{
Debug.Log($"[Puzzles] Step interacted: {stepData?.stepId} on {gameObject.name}");
PuzzleManager.Instance?.OnStepCompleted(stepData);
}
}
}
}

View File

@@ -1,5 +1,6 @@
using UnityEngine;
using System.Collections.Generic;
using PuzzleS;
/// <summary>
/// Manages puzzle step registration, dependency management, and step completion for the puzzle system.