# Interactables System - Code Reference ## Table of Contents 1. [Overview](#overview) 2. [Class Hierarchy](#class-hierarchy) 3. [InteractableBase - The Template Method](#interactablebase---the-template-method) - [Interaction Flow](#interaction-flow) - [Virtual Methods to Override](#virtual-methods-to-override) 4. [Creating Custom Interactables](#creating-custom-interactables) - [Example 1: Simple Button (OneClickInteraction)](#example-1-simple-button-oneclickinteraction) - [Example 2: Item Pickup](#example-2-item-pickup) - [Example 3: Item Slot with Validation](#example-3-item-slot-with-validation) 5. [Character Movement](#character-movement) 6. [Action Component System](#action-component-system) 7. [Events System](#events-system) 8. [Save/Load System Integration](#saveload-system-integration) 9. [Integration with Puzzle System](#integration-with-puzzle-system) 10. [Advanced Patterns](#advanced-patterns) --- ## Overview Simple, centrally orchestrated interaction system for player and follower characters. ### Core Concepts - **Template Method Pattern**: `InteractableBase` defines the interaction flow; subclasses override specific steps - **Action Component System**: Modular actions respond to interaction events independently - **Async/Await Flow**: Character movement and timeline playback use async patterns - **Save/Load Integration**: `SaveableInteractable` provides persistence for interaction state --- ## Class Hierarchy ``` ManagedBehaviour └── InteractableBase ├── OneClickInteraction └── SaveableInteractable ├── Pickup └── ItemSlot ``` ### Class Descriptions - **InteractableBase** - Abstract base class that orchestrates the complete interaction flow using the Template Method pattern. Handles tap input, character movement, validation, and event dispatching for all interactables. - **SaveableInteractable** - Extends InteractableBase with save/load capabilities, integrating with the ManagedBehaviour save system. Provides abstract methods for JSON serialization and deserialization of state. - **OneClickInteraction** - Simplest concrete interactable that completes immediately when character arrives with no additional logic. All functionality comes from UnityEvents configured in the Inspector. - **Pickup** - Represents items that can be picked up by the follower, handling item combination and state tracking. Integrates with ItemManager and supports bilateral restoration with ItemSlots. - **ItemSlot** - Container that accepts specific items with validation for correct/incorrect/forbidden items. Manages item placement, swapping, and supports combination with special puzzle integration that allows swapping when locked. --- ## InteractableBase - The Template Method ### Interaction Flow When a player taps an interactable, the following flow executes: ```csharp OnTap() → CanBeClicked() → StartInteractionFlowAsync() ↓ 1. Find Characters (player, follower) 2. OnInteractionStarted() [Virtual Hook] 3. Fire interactionStarted events 4. MoveCharactersAsync() 5. OnInteractingCharacterArrived() [Virtual Hook] 6. Fire characterArrived events 7. ValidateInteraction() 8. DoInteraction() [Virtual Hook - OVERRIDE THIS] 9. OnInteractionFinished() [Virtual Hook] 10. Fire interactionComplete events ``` ### Virtual Methods to Override #### 1. `CanBeClicked()` - Pre-Interaction Validation ```csharp protected virtual bool CanBeClicked() { if (!isActive) return false; // Add custom checks here return true; } ``` **When to override:** Add high-level validation before interaction starts (cooldowns, prerequisites, etc.) #### 2. `OnInteractionStarted()` - Setup Logic ```csharp protected virtual void OnInteractionStarted() { // Called after characters found, before movement // Setup animations, sound effects, etc. } ``` **When to override:** Perform setup that needs to happen before character movement #### 3. `DoInteraction()` - Main Logic ⭐ **OVERRIDE THIS** ```csharp protected override bool DoInteraction() { // Your interaction logic here return true; // Return true for success, false for failure } ``` **When to override:** **Always override this** - this is your main interaction logic #### 4. `OnInteractingCharacterArrived()` - Arrival Reaction ```csharp protected virtual void OnInteractingCharacterArrived() { // Called when character reaches interaction point // Trigger arrival animations, sounds, etc. } ``` **When to override:** React to character arrival with visuals/audio #### 5. `OnInteractionFinished()` - Cleanup Logic ```csharp protected virtual void OnInteractionFinished(bool success) { // Called after interaction completes // Cleanup, reset state, etc. } ``` **When to override:** Perform cleanup after interaction completes #### 6. `CanProceedWithInteraction()` - Validation ```csharp protected virtual (bool canProceed, string errorMessage) CanProceedWithInteraction() { // Validate if interaction can proceed // Return error message to show to player return (true, null); } ``` **When to override:** Add validation that shows error messages to player --- ## Creating Custom Interactables ### Example 1: Simple Button (OneClickInteraction) The simplest interactable just completes when the character arrives: ```csharp using Interactions; public class OneClickInteraction : InteractableBase { protected override bool DoInteraction() { // Simply return success - no additional logic needed return true; } } ``` **Use Case:** Triggers, pressure plates, simple activators **Configuration:** - Set `characterToInteract` to define which character activates it - Use UnityEvents in inspector to trigger game logic --- ### Example 2: Item Pickup From `Pickup.cs` - demonstrates validation and follower interaction: ```csharp public class Pickup : SaveableInteractable { public PickupItemData itemData; public bool IsPickedUp { get; internal set; } protected override bool DoInteraction() { // Try combination first if follower is holding something var heldItemObject = FollowerController?.GetHeldPickupObject(); var heldItemData = heldItemObject?.GetComponent()?.itemData; var combinationResult = FollowerController.TryCombineItems( this, out var resultItem ); if (combinationResult == FollowerController.CombinationResult.Successful) { IsPickedUp = true; FireCombinationEvent(resultItem, heldItemData); return true; } // No combination - do regular pickup FollowerController?.TryPickupItem(gameObject, itemData); IsPickedUp = true; OnItemPickedUp?.Invoke(itemData); return true; } } ``` **Key Patterns:** - Access `FollowerController` directly (set by base class) - Return `true` for successful pickup - Use custom events (`OnItemPickedUp`) for specific notifications --- ### Example 3: Item Slot with Validation From `ItemSlot.cs` - demonstrates complex validation and state management: ```csharp public class ItemSlot : SaveableInteractable { public PickupItemData itemData; // What item should go here private ItemSlotState currentState = ItemSlotState.None; protected override (bool canProceed, string errorMessage) CanProceedWithInteraction() { var heldItem = FollowerController?.CurrentlyHeldItemData; // Can't interact with empty slot and no item if (heldItem == null && currentlySlottedItemObject == null) return (false, "This requires an item."); // Check forbidden items if (heldItem != null && currentlySlottedItemObject == null) { var config = interactionSettings?.GetSlotItemConfig(itemData); var forbidden = config?.forbiddenItems ?? new List(); if (PickupItemData.ListContainsEquivalent(forbidden, heldItem)) return (false, "Can't place that here."); } return (true, null); } protected override bool DoInteraction() { var heldItemData = FollowerController.CurrentlyHeldItemData; var heldItemObj = FollowerController.GetHeldPickupObject(); // Scenario 1: Slot empty + holding item = Slot it if (heldItemData != null && currentlySlottedItemObject == null) { SlotItem(heldItemObj, heldItemData); FollowerController.ClearHeldItem(); return IsSlottedItemCorrect(); // Returns true only if correct item } // Scenario 2: Slot full + holding item = Try combine or swap if (currentlySlottedItemObject != null) { // Try combination... // Or swap items... } return false; } } ``` **Key Patterns:** - `CanProceedWithInteraction()` shows error messages to player - `DoInteraction()` returns true only for correct item (affects puzzle completion) - Access settings via `GameManager.GetSettingsObject()` --- ## Character Movement ### Character Types ```csharp public enum CharacterToInteract { None, // No character movement Trafalgar, // Player only Pulver, // Follower only (player moves to range first) Both // Both characters move } ``` Set in Inspector on `InteractableBase`. ### Custom Movement Targets Add `CharacterMoveToTarget` component as child of your interactable: ```csharp // Automatically used if present var moveTarget = GetComponentInChildren(); Vector3 targetPos = moveTarget.GetTargetPosition(); ``` See [Editor Reference](editor_reference.md#character-movement-targets) for details. --- ## Action Component System Add modular behaviors to interactables via `InteractionActionBase` components. ### Creating an Action Component ```csharp using Interactions; using System.Threading.Tasks; public class MyCustomAction : InteractionActionBase { protected override async Task ExecuteAsync( InteractionEventType eventType, PlayerTouchController player, FollowerController follower) { // Your action logic here if (eventType == InteractionEventType.InteractionStarted) { // Play sound, spawn VFX, etc. await Task.Delay(1000); // Simulate async work } return true; // Return success } protected override bool ShouldExecute( InteractionEventType eventType, PlayerTouchController player, FollowerController follower) { // Add conditions for when this action should run return base.ShouldExecute(eventType, player, follower); } } ``` ### Configuring in Inspector ![Action Component Setup](../media/interactable_action_component_inspector.png) - **Respond To Events**: Select which events trigger this action - **Pause Interaction Flow**: If true, interaction waits for this action to complete ### Built-in Action: Timeline Playback `InteractionTimelineAction` plays Unity Timeline sequences in response to events: ```csharp // Automatically configured via Inspector // See Editor Reference for details ``` **Features:** - Character binding to timeline tracks - Sequential timeline playback - Loop options (loop all, loop last) - Timeout protection --- ## Events System ### UnityEvents (Inspector-Configurable) Available on all `InteractableBase`: ```csharp [Header("Interaction Events")] public UnityEvent interactionStarted; public UnityEvent interactionInterrupted; public UnityEvent characterArrived; public UnityEvent interactionComplete; // bool = success ``` ### C# Events (Code Subscribers) Pickup example: ```csharp public event Action OnItemPickedUp; public event Action OnItemsCombined; ``` ItemSlot example: ```csharp public event Action OnItemSlotRemoved; public event Action OnCorrectItemSlotted; public event Action OnIncorrectItemSlotted; ``` ### Subscribing to Events ```csharp void Start() { var pickup = GetComponent(); pickup.OnItemPickedUp += HandleItemPickedUp; } void HandleItemPickedUp(PickupItemData itemData) { Debug.Log($"Picked up: {itemData.itemName}"); } void OnDestroy() { var pickup = GetComponent(); if (pickup != null) pickup.OnItemPickedUp -= HandleItemPickedUp; } ``` --- ## Save/Load System Integration ### Making an Interactable Saveable 1. Inherit from `SaveableInteractable` instead of `InteractableBase` 2. Define a serializable data structure 3. Override `GetSerializableState()` and `ApplySerializableState()` ### Example Implementation ```csharp using Interactions; using UnityEngine; // 1. Define save data structure [System.Serializable] public class MyInteractableSaveData { public bool hasBeenActivated; public int activationCount; } // 2. Inherit from SaveableInteractable public class MyInteractable : SaveableInteractable { private bool hasBeenActivated = false; private int activationCount = 0; // 3. Serialize state protected override object GetSerializableState() { return new MyInteractableSaveData { hasBeenActivated = this.hasBeenActivated, activationCount = this.activationCount }; } // 4. Deserialize state protected override void ApplySerializableState(string serializedData) { var data = JsonUtility.FromJson(serializedData); if (data == null) return; this.hasBeenActivated = data.hasBeenActivated; this.activationCount = data.activationCount; // IMPORTANT: Don't fire events during restoration // Don't re-run initialization logic } protected override bool DoInteraction() { hasBeenActivated = true; activationCount++; return true; } } ``` --- ## Integration with Puzzle System Interactables can be puzzle steps by adding `ObjectiveStepBehaviour`: ```csharp // On GameObject with Interactable component var stepBehaviour = gameObject.AddComponent(); stepBehaviour.stepData = myPuzzleStepSO; ``` ### Automatic Puzzle Integration `InteractableBase` automatically checks for puzzle locks: ```csharp private (bool, string) ValidateInteractionBase() { var step = GetComponent(); if (step != null && !step.IsStepUnlocked()) { // Special case: ItemSlots can swap even when locked if (!(this is ItemSlot)) { return (false, "This step is locked!"); } } return (true, null); } ``` **Result:** Locked puzzle steps can't be interacted with (except ItemSlots for item swapping). --- ## Advanced Patterns ### Async Validation For complex validation that requires async operations: ```csharp protected override (bool canProceed, string errorMessage) CanProceedWithInteraction() { // Synchronous validation only // Async validation should be done in OnInteractionStarted return (true, null); } protected override void OnInteractionStarted() { // Can perform async checks here if needed // But interaction flow continues automatically } ``` ### Interrupting Interactions Interactions auto-interrupt if player cancels movement: ```csharp // Automatically handled in MoveCharactersAsync() playerRef.OnMoveToCancelled += () => { interactionInterrupted?.Invoke(); // Flow stops here }; ``` ### One-Time Interactions ```csharp [Header("Interaction Settings")] public bool isOneTime = true; // Automatically disabled after first successful interaction // No override needed ``` ### Cooldown Systems ```csharp [Header("Interaction Settings")] public float cooldown = 5f; // Seconds // Automatically handled by base class // Interaction disabled for 5 seconds after completion ```