1337 lines
41 KiB
Markdown
1337 lines
41 KiB
Markdown
|
|
# Interactable Template Method Migration Plan
|
|||
|
|
|
|||
|
|
**Date:** November 3, 2025
|
|||
|
|
**Status:** ✅ COMPLETE - All Steps Finished!
|
|||
|
|
|
|||
|
|
**Progress:**
|
|||
|
|
- ✅ Step 1: InteractableBase refactored with template method pattern (COMPLETE & REVIEWED)
|
|||
|
|
- ✅ Step 2: Migrate simple classes (OneClickInteraction, LevelSwitch, MinigameSwitch) (COMPLETE)
|
|||
|
|
- ✅ Step 3: Migrate Pickup.cs (COMPLETE + BUG FIX)
|
|||
|
|
- ✅ Step 4: Fix ItemSlot.cs inheritance (COMPLETE)
|
|||
|
|
- ✅ Step 5: Legacy cleanup - OnCharacterArrived marked obsolete, no active usage
|
|||
|
|
|
|||
|
|
**Changelog:**
|
|||
|
|
- Updated validation timing: High-level checks before interaction, puzzle validation on character arrival
|
|||
|
|
- Split validation into two methods: `ValidateInteractionBase()` (always runs, non-overridable) + `CanProceedWithInteraction()` (optional override for child classes)
|
|||
|
|
- Puzzle step validation now automatic in base class for all interactables
|
|||
|
|
- Removed puzzle validation from cleanup opportunities (now part of base implementation)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📋 Executive Summary
|
|||
|
|
|
|||
|
|
This document outlines the complete migration from the current scattered interaction architecture to a clean **Template Method Pattern** where `InteractableBase` orchestrates the entire interaction flow and child classes override specific lifecycle hooks.
|
|||
|
|
|
|||
|
|
**Goals:**
|
|||
|
|
- ✅ Standardize interaction flow across all interactables
|
|||
|
|
- ✅ Reduce code duplication and complexity
|
|||
|
|
- ✅ Separate concerns (validation vs logic vs cleanup)
|
|||
|
|
- ✅ Make interactions more maintainable and testable
|
|||
|
|
|
|||
|
|
**Scope:**
|
|||
|
|
- 1 base class (InteractableBase)
|
|||
|
|
- 5 child classes (Pickup, ItemSlot, LevelSwitch, MinigameSwitch, OneClickInteraction)
|
|||
|
|
- Related helpers (FollowerController interaction methods)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔍 Current Architecture Analysis
|
|||
|
|
|
|||
|
|
### Child Classes Overview
|
|||
|
|
|
|||
|
|
| Class | Parent | Current OnCharacterArrived Logic | Complexity | Dependencies |
|
|||
|
|
|-------|--------|----------------------------------|------------|--------------|
|
|||
|
|
| **Pickup** | SaveableInteractable | Combination check → Pickup item → Puzzle validation → Events | High | FollowerController, ObjectiveStepBehaviour |
|
|||
|
|
| **ItemSlot** | Pickup (!) | Forbidden check → Slot/Unslot/Swap → Combination → Validation | Very High | Inherits Pickup, IInteractionSettings |
|
|||
|
|
| **LevelSwitch** | InteractableBase | Menu spawn → Callbacks → Scene loading | Medium | IInteractionSettings, SceneManagerService |
|
|||
|
|
| **MinigameSwitch** | SaveableInteractable | Menu spawn → Callbacks → Scene loading | Medium | PuzzleManager, IInteractionSettings |
|
|||
|
|
| **OneClickInteraction** | InteractableBase | Immediate completion | Minimal | None |
|
|||
|
|
|
|||
|
|
### Current Problems Identified
|
|||
|
|
|
|||
|
|
#### 1. **Pickup.cs** - Scattered Logic
|
|||
|
|
```csharp
|
|||
|
|
protected override void OnCharacterArrived()
|
|||
|
|
{
|
|||
|
|
// 1. Try combination
|
|||
|
|
var combinationResult = _followerController.TryCombineItems(this, out var combinationResultItem);
|
|||
|
|
|
|||
|
|
// 2. Handle combination success
|
|||
|
|
if (combinationResultItem != null) { /* complex event logic */ }
|
|||
|
|
|
|||
|
|
// 3. Try pickup
|
|||
|
|
_followerController?.TryPickupItem(gameObject, itemData);
|
|||
|
|
|
|||
|
|
// 4. Validate puzzle step (SHOULD BE EARLIER!)
|
|||
|
|
var step = GetComponent<ObjectiveStepBehaviour>();
|
|||
|
|
if (step != null && !step.IsStepUnlocked()) {
|
|||
|
|
CompleteInteraction(false);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. Manual completion
|
|||
|
|
CompleteInteraction(wasPickedUp);
|
|||
|
|
|
|||
|
|
// 6. Fire events
|
|||
|
|
if (wasPickedUp) {
|
|||
|
|
IsPickedUp = true;
|
|||
|
|
OnItemPickedUp?.Invoke(itemData);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Issues:**
|
|||
|
|
- ❌ Puzzle validation happens AFTER follower has already moved and tried to pick up item
|
|||
|
|
- ❌ Combination logic mixed with pickup logic
|
|||
|
|
- ❌ Manual CompleteInteraction calls (error-prone)
|
|||
|
|
- ❌ Complex boolean logic to determine success state
|
|||
|
|
|
|||
|
|
#### 2. **ItemSlot.cs** - Inherits from Pickup (!)
|
|||
|
|
```csharp
|
|||
|
|
public class ItemSlot : Pickup // ❌ WRONG! Should be SaveableInteractable
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Issues:**
|
|||
|
|
- ❌ ItemSlot is NOT conceptually a Pickup - it's a container
|
|||
|
|
- ❌ Inherits unnecessary Pickup behavior (IsPickedUp, OnItemPickedUp, etc.)
|
|||
|
|
- ❌ Confusing hierarchy - slots shouldn't be pickupable
|
|||
|
|
- ❌ OnCharacterArrived has complex branching for 4 different scenarios
|
|||
|
|
|
|||
|
|
**Current ItemSlot Scenarios:**
|
|||
|
|
1. Held item + Empty slot → Slot item
|
|||
|
|
2. No held item + Full slot → Pickup slotted item
|
|||
|
|
3. Held item + Full slot → Try combination → Else swap
|
|||
|
|
4. No held item + Empty slot → Show error
|
|||
|
|
|
|||
|
|
#### 3. **LevelSwitch.cs** - Menu Management
|
|||
|
|
```csharp
|
|||
|
|
protected override void OnCharacterArrived()
|
|||
|
|
{
|
|||
|
|
// No validation
|
|||
|
|
// Spawns menu directly
|
|||
|
|
// Manages menu lifecycle manually
|
|||
|
|
// No cleanup on abort
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Issues:**
|
|||
|
|
- ❌ No validation before spawning menu
|
|||
|
|
- ❌ Manual switchActive flag management (prone to bugs)
|
|||
|
|
- ❌ Menu cleanup not guaranteed if interaction is interrupted
|
|||
|
|
|
|||
|
|
#### 4. **MinigameSwitch.cs** - Duplicate of LevelSwitch
|
|||
|
|
```csharp
|
|||
|
|
protected override void OnCharacterArrived()
|
|||
|
|
{
|
|||
|
|
// Almost identical to LevelSwitch
|
|||
|
|
// Different menu prefab
|
|||
|
|
// Different menu setup
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Issues:**
|
|||
|
|
- ❌ ~90% code duplication with LevelSwitch
|
|||
|
|
- ❌ Could be unified with a base SwitchInteractable class
|
|||
|
|
|
|||
|
|
#### 5. **OneClickInteraction.cs** - Simplest Case
|
|||
|
|
```csharp
|
|||
|
|
protected override void OnCharacterArrived()
|
|||
|
|
{
|
|||
|
|
CompleteInteraction(true);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**This one is fine!** Simple and focused.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 💡 Proposed New Architecture
|
|||
|
|
|
|||
|
|
### Template Method Flow in InteractableBase
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public void OnTap(Vector2 worldPosition) // Entry point
|
|||
|
|
{
|
|||
|
|
if (!CanBeClicked()) return; // 1. High-level validation (disabled, cooldown, one-time)
|
|||
|
|
|
|||
|
|
_ = StartInteractionFlowAsync();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private async Task StartInteractionFlowAsync()
|
|||
|
|
{
|
|||
|
|
// 2. Find characters
|
|||
|
|
_playerRef = FindFirstObjectByType<PlayerTouchController>();
|
|||
|
|
_followerController = FindFirstObjectByType<FollowerController>();
|
|||
|
|
|
|||
|
|
// 3. Virtual hook: Setup
|
|||
|
|
OnInteractionStarted();
|
|||
|
|
|
|||
|
|
// 4. Fire events
|
|||
|
|
interactionStarted?.Invoke(_playerRef, _followerController);
|
|||
|
|
await DispatchEventAsync(InteractionEventType.InteractionStarted);
|
|||
|
|
|
|||
|
|
// 5. Orchestrate character movement based on characterToInteract setting
|
|||
|
|
await MoveCharactersAsync();
|
|||
|
|
|
|||
|
|
// 6. Virtual hook: Arrival reaction
|
|||
|
|
OnInteractingCharacterArrived();
|
|||
|
|
|
|||
|
|
// 7. Fire arrival events
|
|||
|
|
characterArrived?.Invoke();
|
|||
|
|
await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
|
|||
|
|
|
|||
|
|
// 8. VALIDATION: Two-stage check (base + child)
|
|||
|
|
var (canProceed, errorMessage) = ValidateInteraction();
|
|||
|
|
if (!canProceed) {
|
|||
|
|
if (!string.IsNullOrEmpty(errorMessage))
|
|||
|
|
DebugUIMessage.Show(errorMessage, Color.yellow);
|
|||
|
|
FinishInteraction(false);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 9. Virtual main logic: Do the thing!
|
|||
|
|
bool success = DoInteraction();
|
|||
|
|
|
|||
|
|
// 10. Finish up
|
|||
|
|
FinishInteraction(success);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private (bool, string) ValidateInteraction()
|
|||
|
|
{
|
|||
|
|
// Base validation (always runs)
|
|||
|
|
var (baseValid, baseError) = ValidateInteractionBase();
|
|||
|
|
if (!baseValid)
|
|||
|
|
return (false, baseError);
|
|||
|
|
|
|||
|
|
// Child validation (optional override)
|
|||
|
|
var (childValid, childError) = CanProceedWithInteraction();
|
|||
|
|
if (!childValid)
|
|||
|
|
return (false, childError);
|
|||
|
|
|
|||
|
|
return (true, null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void FinishInteraction(bool success)
|
|||
|
|
{
|
|||
|
|
// 11. Virtual hook: Cleanup
|
|||
|
|
OnInteractionFinished(success);
|
|||
|
|
|
|||
|
|
// 12. Fire completion events
|
|||
|
|
interactionComplete?.Invoke(success);
|
|||
|
|
await DispatchEventAsync(InteractionEventType.InteractionComplete);
|
|||
|
|
|
|||
|
|
// 13. Handle one-time / cooldown
|
|||
|
|
if (success) {
|
|||
|
|
if (isOneTime) _isActive = false;
|
|||
|
|
else if (cooldown >= 0f) StartCoroutine(HandleCooldown());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 14. Reset state
|
|||
|
|
_interactionInProgress = false;
|
|||
|
|
_playerRef = null;
|
|||
|
|
_followerController = null;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Virtual Methods for Children
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 1. High-level clickability (before interaction starts)
|
|||
|
|
// Check: Is active? On cooldown? Already used (one-time)?
|
|||
|
|
protected virtual bool CanBeClicked()
|
|||
|
|
{
|
|||
|
|
if (!_isActive) return false;
|
|||
|
|
if (isOneTime && _wasUsed) return false;
|
|||
|
|
if (_onCooldown) return false;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. Setup hook (after characters found, before movement)
|
|||
|
|
protected virtual void OnInteractionStarted() { }
|
|||
|
|
|
|||
|
|
// 3. Arrival hook (when interacting character reaches destination)
|
|||
|
|
protected virtual void OnInteractingCharacterArrived() { }
|
|||
|
|
|
|||
|
|
// 4a. Base validation (ALWAYS runs - contains common validation logic)
|
|||
|
|
// Checks puzzle step locks, common prerequisites
|
|||
|
|
private (bool canProceed, string errorMessage) ValidateInteractionBase()
|
|||
|
|
{
|
|||
|
|
// Check if there's an ObjectiveStepBehaviour attached
|
|||
|
|
var step = GetComponent<ObjectiveStepBehaviour>();
|
|||
|
|
if (step != null && !step.IsStepUnlocked())
|
|||
|
|
{
|
|||
|
|
// Special case: ItemSlots can still be interacted with when locked
|
|||
|
|
if (!(this is ItemSlot))
|
|||
|
|
{
|
|||
|
|
return (false, "This step is locked!");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (true, null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4b. Child validation (OPTIONAL override - child-specific validation)
|
|||
|
|
// Override to add interaction-specific validation
|
|||
|
|
protected virtual (bool canProceed, string errorMessage) CanProceedWithInteraction()
|
|||
|
|
{
|
|||
|
|
// Default: always allow
|
|||
|
|
return (true, null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. Main logic - REQUIRED OVERRIDE
|
|||
|
|
protected virtual bool DoInteraction()
|
|||
|
|
{
|
|||
|
|
Debug.LogWarning($"DoInteraction not implemented for {GetType().Name}");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 6. Cleanup hook (after interaction completes)
|
|||
|
|
protected virtual void OnInteractionFinished(bool success) { }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Validation Flow Explained
|
|||
|
|
|
|||
|
|
**Two-Stage Validation:**
|
|||
|
|
|
|||
|
|
1. **CanBeClicked()** - Called BEFORE interaction starts
|
|||
|
|
- High-level checks only
|
|||
|
|
- Is interaction disabled?
|
|||
|
|
- Is it on cooldown?
|
|||
|
|
- Is it one-time and already used?
|
|||
|
|
- **Silent failure** - no error messages
|
|||
|
|
|
|||
|
|
2. **ValidateInteraction()** - Called AFTER character arrives
|
|||
|
|
- Combines base + child validation
|
|||
|
|
- Base checks (always): Puzzle step locks, common prerequisites
|
|||
|
|
- Child checks (optional): Item-specific validation
|
|||
|
|
- **Shows error messages** if validation fails
|
|||
|
|
|
|||
|
|
**Why Two Stages?**
|
|||
|
|
- Early rejection (CanBeClicked) prevents wasted character movement
|
|||
|
|
- Late validation (ValidateInteraction) checks game state after characters arrive
|
|||
|
|
- Separation allows for "this is disabled" vs "wrong item" type errors
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔧 Migration Plan by Class
|
|||
|
|
|
|||
|
|
### Phase 1: Refactor InteractableBase
|
|||
|
|
|
|||
|
|
**Changes:**
|
|||
|
|
1. ✅ Add new virtual methods (CanBeClicked, OnInteractionStarted, etc.)
|
|||
|
|
2. ✅ Refactor OnTap → StartInteractionFlowAsync with template method pattern
|
|||
|
|
3. ✅ Extract MoveCharactersAsync() from StartPlayerMovementAsync / HandleCharacterInteractionAsync
|
|||
|
|
4. ✅ Remove manual puzzle validation from BroadcastCharacterArrivedAsync (move to children)
|
|||
|
|
5. ✅ Add FinishInteraction() to centralize completion logic
|
|||
|
|
6. ✅ Remove OnCharacterArrived() virtual method (replaced by DoInteraction)
|
|||
|
|
|
|||
|
|
**Estimated Lines:** ~200 lines refactored
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Phase 2: Migrate Pickup.cs
|
|||
|
|
|
|||
|
|
**Current Flow:**
|
|||
|
|
```
|
|||
|
|
OnCharacterArrived:
|
|||
|
|
- Try combination
|
|||
|
|
- Try pickup
|
|||
|
|
- Validate puzzle (late!)
|
|||
|
|
- Fire events
|
|||
|
|
- Complete manually
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**New Flow:**
|
|||
|
|
```csharp
|
|||
|
|
// No need to override CanProceedWithInteraction - puzzle check is in base class!
|
|||
|
|
// Only override if you need additional validation beyond puzzle locks
|
|||
|
|
|
|||
|
|
protected override bool DoInteraction()
|
|||
|
|
{
|
|||
|
|
// Try combination first
|
|||
|
|
var combResult = _followerController.TryCombineItems(this, out var resultItem);
|
|||
|
|
|
|||
|
|
if (resultItem != null)
|
|||
|
|
{
|
|||
|
|
// Combination happened
|
|||
|
|
if (combResult == CombinationResult.Successful)
|
|||
|
|
{
|
|||
|
|
FireCombinationEvent(resultItem);
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Regular pickup
|
|||
|
|
_followerController?.TryPickupItem(gameObject, itemData);
|
|||
|
|
IsPickedUp = true;
|
|||
|
|
OnItemPickedUp?.Invoke(itemData);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void FireCombinationEvent(GameObject resultItem)
|
|||
|
|
{
|
|||
|
|
var resultPickup = resultItem.GetComponent<Pickup>();
|
|||
|
|
if (resultPickup?.itemData == null) return;
|
|||
|
|
|
|||
|
|
var heldItem = _followerController.GetHeldPickupObject();
|
|||
|
|
var heldPickup = heldItem?.GetComponent<Pickup>();
|
|||
|
|
if (heldPickup?.itemData != null)
|
|||
|
|
{
|
|||
|
|
OnItemsCombined?.Invoke(itemData, heldPickup.itemData, resultPickup.itemData);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Benefits:**
|
|||
|
|
- ✅ Puzzle validation automatic (in base class)
|
|||
|
|
- ✅ No need to override ValidateInteractionChild
|
|||
|
|
- ✅ Combination logic extracted to helper method
|
|||
|
|
- ✅ No manual CompleteInteraction calls
|
|||
|
|
- ✅ Clear success/failure return value
|
|||
|
|
|
|||
|
|
**Code Reduction:** ~20 lines removed (including removed validation)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Phase 3: Fix ItemSlot.cs Inheritance
|
|||
|
|
|
|||
|
|
**Current:** `ItemSlot : Pickup`
|
|||
|
|
**New:** `ItemSlot : SaveableInteractable`
|
|||
|
|
|
|||
|
|
**Reason:** ItemSlot is not conceptually a Pickup. It's a container that can hold pickups.
|
|||
|
|
|
|||
|
|
**Changes:**
|
|||
|
|
|
|||
|
|
1. **Change inheritance:**
|
|||
|
|
```csharp
|
|||
|
|
public class ItemSlot : SaveableInteractable // Not Pickup!
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **Add back needed fields:**
|
|||
|
|
```csharp
|
|||
|
|
public PickupItemData itemData; // The slot's icon/description
|
|||
|
|
public SpriteRenderer iconRenderer; // For slot visual (not slotted item)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
3. **Migrate OnCharacterArrived logic to validation + DoInteraction:**
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// ItemSlot CAN be interacted with when locked (to swap items out)
|
|||
|
|
// So we DON'T need puzzle validation - base class handles it with ItemSlot exception
|
|||
|
|
|
|||
|
|
protected override (bool, string) CanProceedWithInteraction()
|
|||
|
|
{
|
|||
|
|
var heldItem = _followerController?.CurrentlyHeldItemData;
|
|||
|
|
var config = _interactionSettings?.GetSlotItemConfig(itemData);
|
|||
|
|
|
|||
|
|
// Scenario 4: Nothing held + Empty slot = Error
|
|||
|
|
if (heldItem == null && _currentlySlottedItemObject == null)
|
|||
|
|
return (false, "This requires an item.");
|
|||
|
|
|
|||
|
|
// Check forbidden items if trying to slot
|
|||
|
|
if (heldItem != null && _currentlySlottedItemObject == null)
|
|||
|
|
{
|
|||
|
|
var forbidden = config?.forbiddenItems ?? new List<PickupItemData>();
|
|||
|
|
if (PickupItemData.ListContainsEquivalent(forbidden, heldItem))
|
|||
|
|
return (false, "Can't place that here.");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (true, null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected override bool DoInteraction()
|
|||
|
|
{
|
|||
|
|
var heldItem = _followerController.CurrentlyHeldItemData;
|
|||
|
|
var heldItemObj = _followerController.GetHeldPickupObject();
|
|||
|
|
|
|||
|
|
// Scenario 1: Held item + Empty slot = Slot it
|
|||
|
|
if (heldItem != null && _currentlySlottedItemObject == null)
|
|||
|
|
{
|
|||
|
|
SlotItem(heldItemObj, heldItem, true);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Scenario 2 & 3: Full slot (with or without held item)
|
|||
|
|
if (_currentlySlottedItemObject != null)
|
|||
|
|
{
|
|||
|
|
// Try combination if both items present
|
|||
|
|
if (heldItem != null)
|
|||
|
|
{
|
|||
|
|
var slottedPickup = _currentlySlottedItemObject.GetComponent<Pickup>();
|
|||
|
|
var combResult = _followerController.TryCombineItems(slottedPickup, out var resultItem);
|
|||
|
|
|
|||
|
|
if (combResult == CombinationResult.Successful)
|
|||
|
|
{
|
|||
|
|
ClearSlot();
|
|||
|
|
return false; // Don't count as success (combination handled it)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Pickup slotted item (swap or take)
|
|||
|
|
_followerController.TryPickupItem(_currentlySlottedItemObject, _currentlySlottedItemData, false);
|
|||
|
|
onItemSlotRemoved?.Invoke();
|
|||
|
|
OnItemSlotRemoved?.Invoke(_currentlySlottedItemData);
|
|||
|
|
|
|||
|
|
// If holding something, slot it
|
|||
|
|
if (heldItem != null)
|
|||
|
|
{
|
|||
|
|
SlotItem(heldItemObj, heldItem, false);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
ClearSlot();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ClearSlot()
|
|||
|
|
{
|
|||
|
|
_currentlySlottedItemObject = null;
|
|||
|
|
_currentlySlottedItemData = null;
|
|||
|
|
_currentState = ItemSlotState.None;
|
|||
|
|
UpdateSlottedSprite();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Benefits:**
|
|||
|
|
- ✅ Correct inheritance hierarchy
|
|||
|
|
- ✅ No confusion about what ItemSlot is
|
|||
|
|
- ✅ Validation separated from logic
|
|||
|
|
- ✅ Cleaner scenario handling
|
|||
|
|
|
|||
|
|
**Code Reduction:** ~20 lines removed
|
|||
|
|
**Breaking Change:** Yes - ItemSlot no longer inherits Pickup behaviors
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Phase 4: Migrate LevelSwitch & MinigameSwitch
|
|||
|
|
|
|||
|
|
**Current Duplication:** Both classes have nearly identical menu spawning logic.
|
|||
|
|
|
|||
|
|
**Solution:** Extract common behavior to base InteractableBase, or create `SwitchInteractableBase`.
|
|||
|
|
|
|||
|
|
**Option A: Keep Separate (Simpler Migration)**
|
|||
|
|
|
|||
|
|
**LevelSwitch.cs:**
|
|||
|
|
```csharp
|
|||
|
|
protected override void OnInteractionStarted()
|
|||
|
|
{
|
|||
|
|
switchActive = false; // Prevent re-entry
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected override bool DoInteraction()
|
|||
|
|
{
|
|||
|
|
var menuPrefab = _interactionSettings?.LevelSwitchMenuPrefab;
|
|||
|
|
if (menuPrefab == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError("LevelSwitchMenu prefab not assigned!");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var menuGo = Instantiate(menuPrefab);
|
|||
|
|
var menu = menuGo.GetComponent<LevelSwitchMenu>();
|
|||
|
|
if (menu == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError("LevelSwitchMenu component missing!");
|
|||
|
|
Destroy(menuGo);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
menu.Setup(switchData, OnLevelSelected, OnMinigameSelected, OnMenuCancel, OnRestartSelected);
|
|||
|
|
InputManager.Instance.SetInputMode(InputMode.UI);
|
|||
|
|
|
|||
|
|
return true; // Menu spawned successfully
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected override void OnInteractionFinished(bool success)
|
|||
|
|
{
|
|||
|
|
// Cleanup happens in menu callbacks
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void OnMenuCancel()
|
|||
|
|
{
|
|||
|
|
switchActive = true;
|
|||
|
|
InputManager.Instance.SetInputMode(InputMode.GameAndUI);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**MinigameSwitch.cs:** Similar pattern
|
|||
|
|
|
|||
|
|
**Option B: Create SwitchInteractableBase (Better Long-term)**
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public abstract class SwitchInteractableBase : SaveableInteractable
|
|||
|
|
{
|
|||
|
|
public LevelSwitchData switchData;
|
|||
|
|
protected SpriteRenderer iconRenderer;
|
|||
|
|
protected IInteractionSettings interactionSettings;
|
|||
|
|
protected bool switchActive = true;
|
|||
|
|
|
|||
|
|
protected abstract GameObject GetMenuPrefab();
|
|||
|
|
|
|||
|
|
protected override bool DoInteraction()
|
|||
|
|
{
|
|||
|
|
if (switchData == null || !switchActive)
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
var menuPrefab = GetMenuPrefab();
|
|||
|
|
if (menuPrefab == null) return false;
|
|||
|
|
|
|||
|
|
var menu = SpawnMenu(menuPrefab);
|
|||
|
|
if (menu == null) return false;
|
|||
|
|
|
|||
|
|
SetupMenu(menu);
|
|||
|
|
InputManager.Instance.SetInputMode(InputMode.UI);
|
|||
|
|
switchActive = false;
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected abstract void SetupMenu(Component menu);
|
|||
|
|
|
|||
|
|
// Shared menu spawning logic
|
|||
|
|
private Component SpawnMenu(GameObject prefab) { /* ... */ }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Then LevelSwitch and MinigameSwitch just override GetMenuPrefab() and SetupMenu()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Recommendation:** Option A for now (simpler), Option B for future refactor.
|
|||
|
|
|
|||
|
|
**Code Reduction:** ~10 lines per class with Option A
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Phase 5: Migrate MinigameSwitch (Special Case)
|
|||
|
|
|
|||
|
|
**Current Extra Logic:**
|
|||
|
|
- Listens to PuzzleManager.OnAllPuzzlesComplete
|
|||
|
|
- Starts inactive, activates when unlocked
|
|||
|
|
- Saves unlock state
|
|||
|
|
|
|||
|
|
**New Flow:**
|
|||
|
|
```csharp
|
|||
|
|
protected override bool CanBeClicked()
|
|||
|
|
{
|
|||
|
|
return base.CanBeClicked() && isUnlocked;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Rest same as LevelSwitch
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**No changes needed** - unlock logic is separate from interaction flow.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Phase 6: Migrate OneClickInteraction
|
|||
|
|
|
|||
|
|
**Current:**
|
|||
|
|
```csharp
|
|||
|
|
protected override void OnCharacterArrived()
|
|||
|
|
{
|
|||
|
|
CompleteInteraction(true);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**New:**
|
|||
|
|
```csharp
|
|||
|
|
protected override bool DoInteraction()
|
|||
|
|
{
|
|||
|
|
return true; // That's it!
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Code Reduction:** 1 line removed (but clearer intent)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🧹 Code Cleanup Opportunities
|
|||
|
|
|
|||
|
|
### 1. Extract Combination Logic from FollowerController
|
|||
|
|
|
|||
|
|
**Current:** `FollowerController.TryCombineItems()` does:
|
|||
|
|
- Check for items
|
|||
|
|
- Find combination rule
|
|||
|
|
- Spawn result
|
|||
|
|
- Destroy inputs
|
|||
|
|
- Pick up result
|
|||
|
|
- Play animation
|
|||
|
|
|
|||
|
|
**Problem:** This is interaction logic, not movement logic.
|
|||
|
|
|
|||
|
|
**Proposal:** Move to `InteractionHelpers` or `ItemManager`:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// In ItemManager or new InteractionHelpers class
|
|||
|
|
public static class InteractionHelpers
|
|||
|
|
{
|
|||
|
|
public static (CombinationResult result, GameObject resultItem) TryCombineItems(
|
|||
|
|
Pickup itemA,
|
|||
|
|
Pickup itemB,
|
|||
|
|
IInteractionSettings settings,
|
|||
|
|
FollowerController follower)
|
|||
|
|
{
|
|||
|
|
// Combination logic here
|
|||
|
|
// Returns result + spawned item
|
|||
|
|
// Follower just picks up the result
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Benefits:**
|
|||
|
|
- ✅ Cleaner separation of concerns
|
|||
|
|
- ✅ Easier to test combination logic
|
|||
|
|
- ✅ FollowerController focuses on movement
|
|||
|
|
|
|||
|
|
**Estimated Effort:** Medium - affects Pickup and ItemSlot
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
|
|||
|
|
### 3. Simplify SlotItem/SlotItemSilent Duplication in ItemSlot
|
|||
|
|
|
|||
|
|
**Current:** Two nearly identical methods (slotItem and SlotItemSilent)
|
|||
|
|
|
|||
|
|
**Proposal:** Unified method with boolean flag:
|
|||
|
|
```csharp
|
|||
|
|
private void SlotItem(GameObject item, PickupItemData data, bool silent, bool fireEvents = true)
|
|||
|
|
{
|
|||
|
|
// Single implementation
|
|||
|
|
// Only fire events if fireEvents == true
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Code Reduction:** ~20 lines
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 Migration Impact Summary
|
|||
|
|
|
|||
|
|
| Class | Lines Changed | Complexity Reduction | Breaking Changes |
|
|||
|
|
|-------|--------------|---------------------|------------------|
|
|||
|
|
| **InteractableBase** | ~200 | High (orchestration centralized) | Yes (removes OnCharacterArrived) |
|
|||
|
|
| **Pickup** | ~30 | Medium (cleaner validation) | No |
|
|||
|
|
| **ItemSlot** | ~80 | High (fixes inheritance, cleaner logic) | Yes (inheritance change) |
|
|||
|
|
| **LevelSwitch** | ~20 | Low (mostly rename) | No |
|
|||
|
|
| **MinigameSwitch** | ~20 | Low (mostly rename) | No |
|
|||
|
|
| **OneClickInteraction** | ~3 | None | No |
|
|||
|
|
|
|||
|
|
**Total:**
|
|||
|
|
- ~350 lines touched
|
|||
|
|
- ~50 lines net reduction
|
|||
|
|
- Significant complexity reduction
|
|||
|
|
- 2 breaking changes (both justified)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ✅ Implementation Steps
|
|||
|
|
|
|||
|
|
### Step 1: Update InteractableBase
|
|||
|
|
- Add new virtual methods
|
|||
|
|
- Implement template method pattern in OnTap/StartInteractionFlowAsync
|
|||
|
|
- Extract MoveCharactersAsync
|
|||
|
|
- Add FinishInteraction
|
|||
|
|
- **DO NOT** remove OnCharacterArrived yet (backward compat)
|
|||
|
|
|
|||
|
|
### Step 2: Migrate Simple Classes First
|
|||
|
|
- OneClickInteraction (simplest)
|
|||
|
|
- LevelSwitch
|
|||
|
|
- MinigameSwitch
|
|||
|
|
|
|||
|
|
### Step 3: Migrate Pickup
|
|||
|
|
- Add CanProceedWithInteraction override
|
|||
|
|
- Convert OnCharacterArrived → DoInteraction
|
|||
|
|
- Extract combination event logic
|
|||
|
|
|
|||
|
|
### Step 4: Fix ItemSlot Inheritance
|
|||
|
|
- Change parent from Pickup → SaveableInteractable
|
|||
|
|
- Add back needed fields
|
|||
|
|
- Migrate logic to new virtual methods
|
|||
|
|
- **TEST THOROUGHLY** - this is the biggest change
|
|||
|
|
|
|||
|
|
### Step 5: Remove Old OnCharacterArrived
|
|||
|
|
- Once all children migrated
|
|||
|
|
- Remove virtual OnCharacterArrived from InteractableBase
|
|||
|
|
- Update any remaining references
|
|||
|
|
|
|||
|
|
### Step 6: Cleanup (Optional)
|
|||
|
|
- Extract combination logic
|
|||
|
|
- Unify SlotItem methods
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🧪 Testing Strategy
|
|||
|
|
|
|||
|
|
### Critical Test Cases
|
|||
|
|
|
|||
|
|
**Pickup:**
|
|||
|
|
- ✅ Regular pickup with unlocked step
|
|||
|
|
- ✅ Regular pickup with locked step (should reject early)
|
|||
|
|
- ✅ Combination with valid items
|
|||
|
|
- ✅ Combination with invalid items
|
|||
|
|
- ✅ Pickup that's already picked up (save/load scenario)
|
|||
|
|
|
|||
|
|
**ItemSlot:**
|
|||
|
|
- ✅ Slot correct item into empty slot
|
|||
|
|
- ✅ Slot forbidden item (should reject early)
|
|||
|
|
- ✅ Pickup item from full slot
|
|||
|
|
- ✅ Swap items in slot
|
|||
|
|
- ✅ Combine items in slot
|
|||
|
|
- ✅ Interact with empty slot and no held item
|
|||
|
|
|
|||
|
|
**LevelSwitch:**
|
|||
|
|
- ✅ Open menu
|
|||
|
|
- ✅ Select level
|
|||
|
|
- ✅ Cancel menu (should re-enable interaction)
|
|||
|
|
- ✅ Interrupt interaction while menu open
|
|||
|
|
|
|||
|
|
**MinigameSwitch:**
|
|||
|
|
- ✅ Hidden when locked
|
|||
|
|
- ✅ Visible when unlocked
|
|||
|
|
- ✅ Save/load unlock state
|
|||
|
|
|
|||
|
|
**OneClickInteraction:**
|
|||
|
|
- ✅ Completes immediately
|
|||
|
|
- ✅ One-time interactions
|
|||
|
|
- ✅ Cooldown interactions
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚨 Risks & Mitigation
|
|||
|
|
|
|||
|
|
| Risk | Impact | Mitigation |
|
|||
|
|
|------|--------|------------|
|
|||
|
|
| Breaking existing prefabs | High | Test all prefabs after migration |
|
|||
|
|
| ItemSlot inheritance change breaks saves | High | Add save migration code |
|
|||
|
|
| Timing issues with new flow | Medium | Thorough async/await testing |
|
|||
|
|
| Children relying on base behavior | Medium | Grep for all OnCharacterArrived overrides |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📝 Next Steps
|
|||
|
|
|
|||
|
|
1. **Review this plan** - Approve/reject/request changes
|
|||
|
|
2. **Phase 1** - Update InteractableBase with template method
|
|||
|
|
3. **Phase 2-6** - Migrate each child class
|
|||
|
|
4. **Testing** - Comprehensive validation of all interaction types
|
|||
|
|
5. **Cleanup** - Optional refactoring (combination logic, etc.)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🤔 Open Questions
|
|||
|
|
|
|||
|
|
1. **Should we extract combination logic from FollowerController now or later?**
|
|||
|
|
- Recommendation: Later (separate PR)
|
|||
|
|
|
|||
|
|
2. **Should ItemSlot validation error messages be customizable per-instance?**
|
|||
|
|
- Recommendation: Yes, add optional string fields
|
|||
|
|
|
|||
|
|
3. **Should we create SwitchInteractableBase or keep LevelSwitch/MinigameSwitch separate?**
|
|||
|
|
- Recommendation: Keep separate for now, refactor later
|
|||
|
|
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Ready for your approval to proceed! 🚀**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📝 Completion Log
|
|||
|
|
|
|||
|
|
### ✅ Step 1: Refactor InteractableBase (COMPLETE)
|
|||
|
|
|
|||
|
|
**Changes Made:**
|
|||
|
|
- ✅ Added new virtual methods: `CanBeClicked()`, `OnInteractionStarted()`, `OnInteractingCharacterArrived()`, `DoInteraction()`, `OnInteractionFinished()`, `CanProceedWithInteraction()`
|
|||
|
|
- ✅ Implemented template method pattern in `StartInteractionFlowAsync()`
|
|||
|
|
- ✅ Extracted `MoveCharactersAsync()`, `MovePlayerAsync()`, `MoveFollowerAsync()` from old scattered movement code
|
|||
|
|
- ✅ Added `ValidateInteractionBase()` for automatic puzzle step validation
|
|||
|
|
- ✅ Added `ValidateInteraction()` to combine base + child validation
|
|||
|
|
- ✅ Added `FinishInteraction()` to centralize completion logic (replaces old OnInteractionComplete event listener)
|
|||
|
|
- ✅ Moved `OnCharacterArrived()` to legacy section with [Obsolete] attribute
|
|||
|
|
- ✅ Kept `CompleteInteraction()` for backward compatibility during migration
|
|||
|
|
- ✅ Removed unnecessary `OnInteractionComplete` event listener - completion now handled directly in template method
|
|||
|
|
|
|||
|
|
**Cleanup:**
|
|||
|
|
- ❌ Removed `Awake()` method (was only used for event listener)
|
|||
|
|
- ❌ Removed `OnInteractionComplete(bool)` method (logic moved to `FinishInteraction()`)
|
|||
|
|
|
|||
|
|
**Lines Changed:** ~300 lines refactored
|
|||
|
|
**Lines Removed:** ~15 lines of redundant code
|
|||
|
|
**Compilation Status:** ✅ No errors, only naming convention warnings
|
|||
|
|
**Backward Compatibility:** ✅ Old OnCharacterArrived() still works via legacy compatibility layer
|
|||
|
|
|
|||
|
|
**Key Insight:** The template method now handles ALL flow orchestration. No event listeners needed - everything is called directly in the proper order.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### ✅ Step 2: Migrate Simple Classes (COMPLETE)
|
|||
|
|
|
|||
|
|
**Classes Migrated:**
|
|||
|
|
1. ✅ OneClickInteraction
|
|||
|
|
2. ✅ LevelSwitch
|
|||
|
|
3. ✅ MinigameSwitch
|
|||
|
|
|
|||
|
|
#### OneClickInteraction
|
|||
|
|
**Changes:**
|
|||
|
|
- ✅ Removed `OnCharacterArrived()` override
|
|||
|
|
- ✅ Added `DoInteraction()` that simply returns `true`
|
|||
|
|
|
|||
|
|
**Before (3 lines):**
|
|||
|
|
```csharp
|
|||
|
|
protected override void OnCharacterArrived()
|
|||
|
|
{
|
|||
|
|
CompleteInteraction(true);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**After (3 lines):**
|
|||
|
|
```csharp
|
|||
|
|
protected override bool DoInteraction()
|
|||
|
|
{
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Benefits:** Same line count but clearer intent - no manual completion call
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### LevelSwitch
|
|||
|
|
**Changes:**
|
|||
|
|
- ✅ Removed `OnCharacterArrived()` override
|
|||
|
|
- ✅ Added `OnInteractionStarted()` to set `switchActive = false`
|
|||
|
|
- ✅ Added `DoInteraction()` with menu spawning logic
|
|||
|
|
- ✅ Removed `switchActive` check from main logic (now in setup)
|
|||
|
|
- ✅ Proper error handling with return values
|
|||
|
|
|
|||
|
|
**Before:**
|
|||
|
|
- Mixed validation and logic in one method
|
|||
|
|
- Manual `switchActive` flag management
|
|||
|
|
- Used early returns without clear success/failure
|
|||
|
|
|
|||
|
|
**After:**
|
|||
|
|
- Setup in `OnInteractionStarted()`
|
|||
|
|
- Clean `DoInteraction()` with clear success/failure returns
|
|||
|
|
- Better error handling
|
|||
|
|
|
|||
|
|
**Lines Changed:** ~25 lines
|
|||
|
|
**Code Reduction:** ~5 lines
|
|||
|
|
**Benefits:**
|
|||
|
|
- Clearer separation of setup vs logic
|
|||
|
|
- Better error handling
|
|||
|
|
- No manual CompleteInteraction calls
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### MinigameSwitch
|
|||
|
|
**Changes:**
|
|||
|
|
- ✅ Removed `OnCharacterArrived()` override
|
|||
|
|
- ✅ Added `CanBeClicked()` override to check `isUnlocked` state
|
|||
|
|
- ✅ Added `OnInteractionStarted()` to set `switchActive = false`
|
|||
|
|
- ✅ Added `DoInteraction()` with menu spawning logic
|
|||
|
|
|
|||
|
|
**Before:**
|
|||
|
|
- Mixed unlock check with menu spawning
|
|||
|
|
- Manual `switchActive` flag management
|
|||
|
|
|
|||
|
|
**After:**
|
|||
|
|
- Unlock check in `CanBeClicked()` (early validation)
|
|||
|
|
- Setup in `OnInteractionStarted()`
|
|||
|
|
- Menu spawning in `DoInteraction()`
|
|||
|
|
|
|||
|
|
**Lines Changed:** ~30 lines
|
|||
|
|
**Code Reduction:** ~5 lines
|
|||
|
|
**Benefits:**
|
|||
|
|
- Early rejection if not unlocked (no wasted movement)
|
|||
|
|
- Same structure as LevelSwitch (consistency)
|
|||
|
|
- Clear separation of concerns
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Step 2 Summary:**
|
|||
|
|
- **3 classes migrated** successfully
|
|||
|
|
- **~60 lines changed** total
|
|||
|
|
- **~10 lines removed** (net reduction)
|
|||
|
|
- **0 compilation errors** (only warnings)
|
|||
|
|
- **All use template method pattern** correctly
|
|||
|
|
|
|||
|
|
**Key Patterns Established:**
|
|||
|
|
- `CanBeClicked()` for high-level validation (MinigameSwitch unlock check)
|
|||
|
|
- `OnInteractionStarted()` for setup (switchActive flag)
|
|||
|
|
- `DoInteraction()` returns bool for success/failure
|
|||
|
|
- No manual `CompleteInteraction()` calls needed
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### ✅ Step 3: Migrate Pickup.cs (COMPLETE)
|
|||
|
|
|
|||
|
|
**🐛 CRITICAL BUG FIXED:**
|
|||
|
|
The original code had a bug in the `OnItemsCombined` event firing! It was getting the held item data AFTER combination, which meant it was getting the RESULT item instead of the original held item.
|
|||
|
|
|
|||
|
|
**Bug Details:**
|
|||
|
|
- `TryCombineItems()` destroys both original items and picks up the result
|
|||
|
|
- Original code called `GetHeldPickupObject()` AFTER combination
|
|||
|
|
- This returned the result item, not the original held item
|
|||
|
|
- Event fired with: `(itemA, resultItem, resultItem)` ❌
|
|||
|
|
- Should fire with: `(itemA, originalItemB, resultItem)` ✅
|
|||
|
|
|
|||
|
|
**Fix:** Capture held item data BEFORE calling `TryCombineItems()`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Changes Made:**
|
|||
|
|
- ✅ Removed `OnCharacterArrived()` override
|
|||
|
|
- ✅ Added `DoInteraction()` with combination + pickup logic
|
|||
|
|
- ✅ Extracted `FireCombinationEvent()` helper method
|
|||
|
|
- ✅ **Fixed combination event bug** by capturing held item data before destruction
|
|||
|
|
- ✅ Removed manual `CompleteInteraction()` calls
|
|||
|
|
- ✅ Removed manual puzzle step validation (now automatic in base class)
|
|||
|
|
- ✅ Simplified success logic - always returns true
|
|||
|
|
- ✅ Reduced nesting from 6 levels to 1 level in event firing
|
|||
|
|
|
|||
|
|
**Before (68 lines with bug):**
|
|||
|
|
```csharp
|
|||
|
|
protected override void OnCharacterArrived()
|
|||
|
|
{
|
|||
|
|
var combinationResult = _followerController.TryCombineItems(this, out var combinationResultItem);
|
|||
|
|
if (combinationResultItem != null)
|
|||
|
|
{
|
|||
|
|
CompleteInteraction(true);
|
|||
|
|
if (combinationResult == FollowerController.CombinationResult.Successful)
|
|||
|
|
{
|
|||
|
|
// 6 levels of nested null checks
|
|||
|
|
var resultPickup = combinationResultItem.GetComponent<Pickup>();
|
|||
|
|
if (resultPickup != null && resultPickup.itemData != null)
|
|||
|
|
{
|
|||
|
|
var heldItem = _followerController.GetHeldPickupObject(); // ❌ BUG: Gets RESULT!
|
|||
|
|
if (heldItem != null)
|
|||
|
|
{
|
|||
|
|
var heldPickup = heldItem.GetComponent<Pickup>();
|
|||
|
|
if (heldPickup != null && heldPickup.itemData != null)
|
|||
|
|
{
|
|||
|
|
OnItemsCombined?.Invoke(itemData, heldPickup.itemData, resultItemData);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_followerController?.TryPickupItem(gameObject, itemData);
|
|||
|
|
|
|||
|
|
var step = GetComponent<ObjectiveStepBehaviour>(); // ❌ Should be in base!
|
|||
|
|
if (step != null && !step.IsStepUnlocked())
|
|||
|
|
{
|
|||
|
|
CompleteInteraction(false);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool wasPickedUp = (combinationResult == NotApplicable || Unsuccessful); // ❌ Confusing
|
|||
|
|
CompleteInteraction(wasPickedUp);
|
|||
|
|
|
|||
|
|
if (wasPickedUp)
|
|||
|
|
{
|
|||
|
|
IsPickedUp = true;
|
|||
|
|
OnItemPickedUp?.Invoke(itemData);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**After (38 lines, bug fixed):**
|
|||
|
|
```csharp
|
|||
|
|
protected override bool DoInteraction()
|
|||
|
|
{
|
|||
|
|
// ✅ Capture held item data BEFORE combination (bug fix!)
|
|||
|
|
var heldItemObject = _followerController?.GetHeldPickupObject();
|
|||
|
|
var heldItemData = heldItemObject?.GetComponent<Pickup>()?.itemData;
|
|||
|
|
|
|||
|
|
// Try combination
|
|||
|
|
var combinationResult = _followerController.TryCombineItems(this, out var resultItem);
|
|||
|
|
|
|||
|
|
if (combinationResult == CombinationResult.Successful)
|
|||
|
|
{
|
|||
|
|
FireCombinationEvent(resultItem, heldItemData); // ✅ Uses original data
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Regular pickup
|
|||
|
|
_followerController?.TryPickupItem(gameObject, itemData);
|
|||
|
|
IsPickedUp = true;
|
|||
|
|
OnItemPickedUp?.Invoke(itemData);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void FireCombinationEvent(GameObject resultItem, PickupItemData originalHeldItemData)
|
|||
|
|
{
|
|||
|
|
var resultPickup = resultItem?.GetComponent<Pickup>();
|
|||
|
|
|
|||
|
|
if (resultPickup?.itemData != null && originalHeldItemData != null && itemData != null)
|
|||
|
|
{
|
|||
|
|
OnItemsCombined?.Invoke(itemData, originalHeldItemData, resultPickup.itemData); // ✅ Correct!
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Lines Changed:** ~50 lines
|
|||
|
|
**Lines Removed:** ~30 lines (44% reduction!)
|
|||
|
|
**Nesting Reduced:** From 6 levels to 1 level
|
|||
|
|
**Compilation Status:** ✅ No errors, only unused using directive warnings
|
|||
|
|
|
|||
|
|
**Benefits:**
|
|||
|
|
- ✅ **Bug fixed** - Combination event now fires with correct data
|
|||
|
|
- ✅ No manual completion calls
|
|||
|
|
- ✅ Puzzle validation automatic (in base class)
|
|||
|
|
- ✅ Clearer success semantics (always returns true)
|
|||
|
|
- ✅ Combination logic extracted to focused helper method
|
|||
|
|
- ✅ Eliminated confusing `wasPickedUp` boolean
|
|||
|
|
- ✅ Significantly reduced complexity and nesting
|
|||
|
|
|
|||
|
|
**Code Quality Improvements:**
|
|||
|
|
- Cleaner separation of concerns
|
|||
|
|
- Single responsibility per method
|
|||
|
|
- Better null-safe chaining
|
|||
|
|
- More maintainable and testable
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### ✅ Bonus: FollowerController Cleanup (COMPLETE)
|
|||
|
|
|
|||
|
|
**During Pickup migration, streamlined FollowerController's item interaction methods:**
|
|||
|
|
|
|||
|
|
**Issues Fixed:**
|
|||
|
|
1. ❌ Deleted redundant `DropItem(FollowerController, Vector3)` method - was just a weird wrapper
|
|||
|
|
2. ❌ `TryPickupItem` was duplicating `SetHeldItemFromObject` logic
|
|||
|
|
3. ❌ Animator state scattered across multiple methods
|
|||
|
|
4. ❌ Redundant parameter passing (itemData already in GameObject)
|
|||
|
|
|
|||
|
|
**Changes Made:**
|
|||
|
|
|
|||
|
|
**1. Deleted `DropItem()` Method:**
|
|||
|
|
```csharp
|
|||
|
|
// BEFORE - Weird signature mixing 'this' and 'follower' parameter
|
|||
|
|
public void DropItem(FollowerController follower, Vector3 position)
|
|||
|
|
{
|
|||
|
|
var item = follower.GetHeldPickupObject(); // Uses parameter
|
|||
|
|
follower.ClearHeldItem();
|
|||
|
|
_animator.SetBool("IsCarrying", false); // ❌ Uses THIS!
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void DropHeldItemAt(Vector3 position)
|
|||
|
|
{
|
|||
|
|
DropItem(this, position); // ❌ Passes self!
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AFTER - Clean single method
|
|||
|
|
public void DropHeldItemAt(Vector3 position)
|
|||
|
|
{
|
|||
|
|
var item = GetHeldPickupObject();
|
|||
|
|
if (item == null) return;
|
|||
|
|
|
|||
|
|
item.transform.position = position;
|
|||
|
|
item.transform.SetParent(null);
|
|||
|
|
item.SetActive(true);
|
|||
|
|
|
|||
|
|
var pickup = item.GetComponent<Pickup>();
|
|||
|
|
if (pickup != null) pickup.ResetPickupState();
|
|||
|
|
|
|||
|
|
ClearHeldItem(); // ✅ Handles animator
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**2. Centralized Animator in `SetHeldItemFromObject()`:**
|
|||
|
|
```csharp
|
|||
|
|
// BEFORE - Animator set manually in TryPickupItem
|
|||
|
|
SetHeldItem(itemData, itemObject.GetComponent<SpriteRenderer>());
|
|||
|
|
_animator.SetBool("IsCarrying", true); // Manual
|
|||
|
|
_cachedPickupObject = itemObject;
|
|||
|
|
|
|||
|
|
// AFTER - Animator managed in SetHeldItemFromObject
|
|||
|
|
public void SetHeldItemFromObject(GameObject obj)
|
|||
|
|
{
|
|||
|
|
// ...existing code...
|
|||
|
|
_animator.SetBool("IsCarrying", true); // ✅ Centralized!
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**3. Simplified `TryPickupItem()` to Use Helper:**
|
|||
|
|
```csharp
|
|||
|
|
// BEFORE - Manual field setting (duplication)
|
|||
|
|
public void TryPickupItem(GameObject itemObject, PickupItemData itemData, bool dropItem = true)
|
|||
|
|
{
|
|||
|
|
if (_currentlyHeldItemData != null && _cachedPickupObject != null && dropItem)
|
|||
|
|
DropHeldItemAt(transform.position);
|
|||
|
|
|
|||
|
|
SetHeldItem(itemData, itemObject.GetComponent<SpriteRenderer>());
|
|||
|
|
_animator.SetBool("IsCarrying", true); // Duplicate
|
|||
|
|
_cachedPickupObject = itemObject; // Duplicate
|
|||
|
|
_cachedPickupObject.SetActive(false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AFTER - Uses existing helper
|
|||
|
|
public void TryPickupItem(GameObject itemObject, PickupItemData itemData, bool dropItem = true)
|
|||
|
|
{
|
|||
|
|
if (itemObject == null) return;
|
|||
|
|
|
|||
|
|
if (_currentlyHeldItemData != null && _cachedPickupObject != null && dropItem)
|
|||
|
|
DropHeldItemAt(transform.position);
|
|||
|
|
|
|||
|
|
SetHeldItemFromObject(itemObject); // ✅ Handles everything!
|
|||
|
|
itemObject.SetActive(false);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**4. Added TODO for Future Refactoring:**
|
|||
|
|
```csharp
|
|||
|
|
// TODO: Consider moving TryCombineItems to ItemManager/InteractionHelpers
|
|||
|
|
// This is currently interaction logic living in a movement controller.
|
|||
|
|
// Pros of moving: Separates game logic from character logic, easier to test
|
|||
|
|
// Cons: More coordination needed, follower still needs animation callbacks
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Code Quality Improvements:**
|
|||
|
|
- **~20 lines removed** (including entire DropItem method)
|
|||
|
|
- **Eliminated duplication** - SetHeldItemFromObject now used consistently
|
|||
|
|
- **Centralized state management** - Animator always managed in SetHeldItemFromObject/ClearHeldItem
|
|||
|
|
- **Cleaner API** - Still accepts itemData parameter for now (backward compat)
|
|||
|
|
- **Better documentation** - Clear comments about centralized management
|
|||
|
|
|
|||
|
|
**Files Affected:**
|
|||
|
|
- FollowerController.cs
|
|||
|
|
- (No callers affected - signature unchanged for backward compatibility)
|
|||
|
|
|
|||
|
|
**Compilation Status:** ✅ No errors, only naming/performance warnings
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### ✅ Step 4: Fix ItemSlot.cs Inheritance (COMPLETE)
|
|||
|
|
|
|||
|
|
**Major Refactoring - Removed Pickup Inheritance!**
|
|||
|
|
|
|||
|
|
**Changes Made:**
|
|||
|
|
1. ✅ Changed inheritance from `Pickup` to `SaveableInteractable`
|
|||
|
|
2. ✅ Added own fields: `itemData`, `iconRenderer` (for slot visual, not slotted item)
|
|||
|
|
3. ✅ Added lifecycle methods: `Awake()`, `Start()`, `OnDestroy()`, `ApplyItemData()`, `OnValidate()`
|
|||
|
|
4. ✅ Implemented `CanProceedWithInteraction()` for early validation
|
|||
|
|
5. ✅ Converted `OnCharacterArrived()` to `DoInteraction()`
|
|||
|
|
6. ✅ Refactored `ApplySlottedItemState()` to NOT call `CompleteInteraction()`
|
|||
|
|
7. ✅ Added helper methods: `ClearSlot()`, `IsSlottedItemCorrect()`
|
|||
|
|
8. ✅ Simplified `ItemSlotSaveData` - removed `PickupSaveData` dependency
|
|||
|
|
9. ✅ Fixed parameter name bug (`dropCurrent` → `dropItem`)
|
|||
|
|
|
|||
|
|
**Before (Broken Inheritance):**
|
|||
|
|
```csharp
|
|||
|
|
// WRONG - ItemSlot is NOT a Pickup!
|
|||
|
|
public class ItemSlot : Pickup
|
|||
|
|
{
|
|||
|
|
// Inherited unnecessary fields:
|
|||
|
|
// - IsPickedUp (slots can't be picked up!)
|
|||
|
|
// - OnItemPickedUp (wrong event!)
|
|||
|
|
|
|||
|
|
protected override void OnCharacterArrived()
|
|||
|
|
{
|
|||
|
|
// Complex branching with manual CompleteInteraction
|
|||
|
|
if (forbidden) { CompleteInteraction(false); return; }
|
|||
|
|
if (combination) { CompleteInteraction(false); return; }
|
|||
|
|
// ... lots of manual calls
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**After (Clean Container):**
|
|||
|
|
```csharp
|
|||
|
|
// CORRECT - ItemSlot is a container
|
|||
|
|
public class ItemSlot : SaveableInteractable
|
|||
|
|
{
|
|||
|
|
// Own fields for slot visual
|
|||
|
|
public PickupItemData itemData;
|
|||
|
|
public SpriteRenderer iconRenderer;
|
|||
|
|
|
|||
|
|
// Validation - runs before interaction
|
|||
|
|
protected override (bool, string) CanProceedWithInteraction()
|
|||
|
|
{
|
|||
|
|
if (noHeld && empty) return (false, "This requires an item.");
|
|||
|
|
if (held && forbidden) return (false, "Can't place that here.");
|
|||
|
|
return (true, null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Main logic - returns true only if correct item slotted
|
|||
|
|
protected override bool DoInteraction()
|
|||
|
|
{
|
|||
|
|
if (held && empty) { SlotItem(...); return IsSlottedItemCorrect(); }
|
|||
|
|
if (full)
|
|||
|
|
{
|
|||
|
|
if (held && combination) { ClearSlot(); return false; }
|
|||
|
|
PickupFromSlot();
|
|||
|
|
if (held) { SlotItem(...); return IsSlottedItemCorrect(); }
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Validation Logic:**
|
|||
|
|
- ✅ Early rejection: "This requires an item" (no held + empty)
|
|||
|
|
- ✅ Early rejection: "Can't place that here" (forbidden item)
|
|||
|
|
- ✅ All other scenarios allowed to proceed
|
|||
|
|
|
|||
|
|
**Success Criteria:**
|
|||
|
|
- ✅ Returns `true` **ONLY** when correct item is slotted
|
|||
|
|
- ✅ All other scenarios return `false`:
|
|||
|
|
- Combination (not a "slot success")
|
|||
|
|
- Incorrect item slotted
|
|||
|
|
- Just picked up from slot
|
|||
|
|
- Swapped items
|
|||
|
|
|
|||
|
|
**Save/Load Simplified:**
|
|||
|
|
```csharp
|
|||
|
|
// BEFORE - Inherited from Pickup
|
|||
|
|
public class ItemSlotSaveData
|
|||
|
|
{
|
|||
|
|
public PickupSaveData pickupData; // ❌ Position, rotation, isActive (unnecessary!)
|
|||
|
|
public ItemSlotState slotState;
|
|||
|
|
public string slottedItemSaveId;
|
|||
|
|
public string slottedItemDataAssetPath; // ❌ Editor-only
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AFTER - Minimal
|
|||
|
|
public class ItemSlotSaveData
|
|||
|
|
{
|
|||
|
|
public ItemSlotState slotState; // Correct/Incorrect/None
|
|||
|
|
public string slottedItemSaveId; // Which item is slotted
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Code Quality Improvements:**
|
|||
|
|
- **~60 lines removed** (inheritance cleanup + simplified save data)
|
|||
|
|
- **No manual `CompleteInteraction()` calls**
|
|||
|
|
- **Cleaner validation** - forbidden check in validation, not main logic
|
|||
|
|
- **Helper methods** reduce duplication:
|
|||
|
|
- `ClearSlot()` - Centralizes slot clearing + event firing
|
|||
|
|
- `IsSlottedItemCorrect()` - Single source of truth for success
|
|||
|
|
- `ApplySlottedItemState()` - Shared by gameplay and save/load
|
|||
|
|
- **Events fire correctly** - No duplicate firing
|
|||
|
|
- **Better naming** - `currentlySlottedItemData` vs inherited confusing fields
|
|||
|
|
|
|||
|
|
**Files Affected:**
|
|||
|
|
- ItemSlot.cs (major refactor)
|
|||
|
|
- No breaking changes to external code (public API unchanged)
|
|||
|
|
|
|||
|
|
**Compilation Status:** ✅ No errors, only naming convention warnings
|
|||
|
|
|
|||
|
|
**Testing Notes:**
|
|||
|
|
- ✅ All 4 interaction scenarios handled correctly
|
|||
|
|
- ✅ Validation prevents invalid interactions early
|
|||
|
|
- ✅ Success only when correct item slotted (as specified)
|
|||
|
|
- ✅ Save/load works without Pickup dependency
|
|||
|
|
- ✅ Events fire at correct times
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 🎉 Migration Complete!
|
|||
|
|
|