874 lines
29 KiB
C#
874 lines
29 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
|
|
/// <summary>
|
|
/// 인벤토리에서 관리할 아이템 종류입니다.
|
|
/// 아이템을 추가하려면 여기에 enum 값을 추가하고, InventoryManager의 itemDefinitions에도 데이터를 추가하세요.
|
|
/// </summary>
|
|
public enum InventoryItemType
|
|
{
|
|
Fish,
|
|
OldCompass,
|
|
Trash,
|
|
Bottle,
|
|
MemoryPiece
|
|
}
|
|
|
|
/// <summary>
|
|
/// 인벤토리 UI 필터/정렬에 사용할 간단한 카테고리입니다.
|
|
/// </summary>
|
|
public enum InventoryItemCategory
|
|
{
|
|
All,
|
|
Consumable,
|
|
Quest,
|
|
Collectible,
|
|
KeyItem,
|
|
Material,
|
|
Other
|
|
}
|
|
|
|
[Serializable]
|
|
public class InventoryItemStack
|
|
{
|
|
public InventoryItemType itemType;
|
|
[Min(0)] public int count;
|
|
}
|
|
|
|
[Serializable]
|
|
public class InventoryItemDefinition
|
|
{
|
|
[Header("Identity")]
|
|
public InventoryItemType itemType;
|
|
public string displayName;
|
|
[TextArea(2, 5)] public string description;
|
|
[TextArea(1, 3)] public string goalHint;
|
|
|
|
[Header("Visuals")]
|
|
public Sprite icon;
|
|
public Sprite slotBackground;
|
|
public bool importantItem;
|
|
|
|
[Header("Rules")]
|
|
public InventoryItemCategory category = InventoryItemCategory.Other;
|
|
[Tooltip("0 이하이면 InventoryManager의 기본 최대 소지 수량을 사용합니다.")]
|
|
public int maxCount = 99;
|
|
public bool usable = false;
|
|
public bool consumeOnUse = true;
|
|
public bool requireUseConfirmation = false;
|
|
public string useLabel = "사용";
|
|
public string useSuccessMessage;
|
|
public string useFailMessage;
|
|
|
|
[Header("Audio")]
|
|
public AudioClip acquisitionClip;
|
|
public AudioClip useClip;
|
|
|
|
[Header("Slot Display Override")]
|
|
public bool overrideSlotDisplaySettings = false;
|
|
public bool hideWhenZero = false;
|
|
public bool dimWhenZero = true;
|
|
[Range(0f, 1f)] public float zeroAlpha = 0.35f;
|
|
[Range(0f, 1f)] public float ownedAlpha = 1f;
|
|
|
|
public string SafeDisplayName => string.IsNullOrWhiteSpace(displayName) ? itemType.ToString() : displayName;
|
|
}
|
|
|
|
[Serializable]
|
|
public class InventoryLogEntry
|
|
{
|
|
public InventoryItemType itemType;
|
|
public int amount;
|
|
public string action;
|
|
public string displayName;
|
|
public string timeText;
|
|
|
|
public InventoryLogEntry() { }
|
|
|
|
public InventoryLogEntry(InventoryItemType itemType, int amount, string action, string displayName)
|
|
{
|
|
this.itemType = itemType;
|
|
this.amount = amount;
|
|
this.action = action;
|
|
this.displayName = displayName;
|
|
timeText = DateTime.Now.ToString("HH:mm:ss");
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
string amountText = amount > 0 ? $" x{amount}" : string.Empty;
|
|
return $"[{timeText}] {displayName}{amountText} {action}";
|
|
}
|
|
}
|
|
|
|
[Serializable] public class InventoryItemChangedEvent : UnityEvent<InventoryItemType, int> { }
|
|
[Serializable] public class InventoryItemAmountEvent : UnityEvent<InventoryItemType, int, int> { }
|
|
[Serializable] public class InventoryStringEvent : UnityEvent<string> { }
|
|
[Serializable] public class InventoryProgressEvent : UnityEvent<int, int> { }
|
|
|
|
/// <summary>
|
|
/// 실제 아이템 개수를 관리하는 중심 스크립트입니다.
|
|
/// 기능: 싱글톤, 저장/불러오기, 최대 소지 수량, 사용, 부족 안내, 획득 로그, 기억의 조각 진행도 이벤트.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
public class InventoryManager : MonoBehaviour
|
|
{
|
|
public static InventoryManager Instance { get; private set; }
|
|
|
|
[Header("Singleton")]
|
|
[SerializeField] private bool useSingleton = true;
|
|
[SerializeField] private bool dontDestroyOnLoad = true;
|
|
[SerializeField] private bool destroyDuplicateManagers = true;
|
|
|
|
[Header("Initial Items")]
|
|
[SerializeField] private List<InventoryItemStack> initialItems = new List<InventoryItemStack>();
|
|
|
|
[Header("Item Definitions")]
|
|
[SerializeField] private List<InventoryItemDefinition> itemDefinitions = new List<InventoryItemDefinition>()
|
|
{
|
|
new InventoryItemDefinition { itemType = InventoryItemType.Fish, displayName = "생선", description = "고양이 합창단이 좋아합니다.", goalHint = "고양이에게 줄 수 있습니다.", category = InventoryItemCategory.Consumable, maxCount = 99, usable = true, consumeOnUse = true, useSuccessMessage = "생선을 사용했습니다." },
|
|
new InventoryItemDefinition { itemType = InventoryItemType.OldCompass, displayName = "낡은 나침반", description = "미로에서 길을 찾는 데 도움이 됩니다.", goalHint = "미로에서 힌트를 볼 때 필요합니다.", category = InventoryItemCategory.KeyItem, maxCount = 1, usable = true, consumeOnUse = false, importantItem = true, useSuccessMessage = "나침반이 방향을 가리킵니다." },
|
|
new InventoryItemDefinition { itemType = InventoryItemType.Trash, displayName = "쓰레기", description = "낚시터를 정화하는 데 필요합니다.", goalHint = "정화 장치에 넣을 수 있습니다.", category = InventoryItemCategory.Material, maxCount = 99, usable = true, consumeOnUse = true, useSuccessMessage = "쓰레기를 사용했습니다." },
|
|
new InventoryItemDefinition { itemType = InventoryItemType.Bottle, displayName = "마법병", description = "바다 속에서 발견한 수상한 병입니다.", goalHint = "특정 이벤트에서 사용할 수 있습니다.", category = InventoryItemCategory.KeyItem, maxCount = 1, usable = true, consumeOnUse = false, importantItem = true, requireUseConfirmation = true, useSuccessMessage = "마법병을 사용했습니다." },
|
|
new InventoryItemDefinition { itemType = InventoryItemType.MemoryPiece, displayName = "기억의 조각", description = "제페토를 구출하기 위한 중요한 조각입니다.", goalHint = "모든 조각을 모으면 중요한 이벤트가 열립니다.", category = InventoryItemCategory.Quest, maxCount = 5, usable = false, consumeOnUse = false, importantItem = true }
|
|
};
|
|
|
|
[Header("Limits")]
|
|
[Tooltip("0 이하이면 기본 최대 수량 제한을 사용하지 않습니다. 아이템 정의의 maxCount가 있으면 그것이 우선됩니다.")]
|
|
[SerializeField] private int defaultMaxCount = 999;
|
|
|
|
[Header("Memory Piece Progress")]
|
|
[SerializeField] private InventoryItemType memoryPieceItemType = InventoryItemType.MemoryPiece;
|
|
[Min(1)] [SerializeField] private int memoryPieceTargetCount = 5;
|
|
|
|
[Header("Save / Load")]
|
|
[SerializeField] private bool loadOnAwake = true;
|
|
[SerializeField] private bool saveOnChange = true;
|
|
[Tooltip("Persistent Pickup/Reward 완료 상태는 아이템 수량 자동 저장을 꺼도 저장하는 것을 권장합니다.")]
|
|
[SerializeField] private bool forceSavePersistentState = true;
|
|
[SerializeField] private string saveKey = "Inventory_SaveData";
|
|
|
|
[Header("Recent Log")]
|
|
[SerializeField] private bool keepRecentLogs = true;
|
|
[SerializeField] private int maxRecentLogCount = 5;
|
|
|
|
[Header("Events")]
|
|
public InventoryItemChangedEvent onItemCountChanged;
|
|
public InventoryItemAmountEvent onItemAdded;
|
|
public InventoryItemAmountEvent onItemRemoved;
|
|
public InventoryItemChangedEvent onItemUsed;
|
|
public InventoryStringEvent onMessageRequested;
|
|
public InventoryProgressEvent onMemoryPieceProgressChanged;
|
|
public UnityEvent onMemoryPieceCompleted;
|
|
public UnityEvent onInventoryChanged;
|
|
|
|
[Header("Debug")]
|
|
[SerializeField] private bool showDebugLog = true;
|
|
|
|
private readonly Dictionary<InventoryItemType, int> itemCounts = new Dictionary<InventoryItemType, int>();
|
|
private readonly HashSet<string> consumedPersistentKeys = new HashSet<string>();
|
|
private readonly List<InventoryLogEntry> recentLogs = new List<InventoryLogEntry>();
|
|
|
|
public event Action<InventoryItemType, int> ItemCountChanged;
|
|
public event Action<InventoryItemType, int, int> ItemAdded;
|
|
public event Action<InventoryItemType, int, int> ItemRemoved;
|
|
public event Action<InventoryItemType, int> ItemUsed;
|
|
public event Action InventoryChanged;
|
|
public event Action<string> MessageRequested;
|
|
public event Action<InventoryLogEntry> LogAdded;
|
|
public event Action<int, int> MemoryPieceProgressChanged;
|
|
public event Action MemoryPieceCompleted;
|
|
|
|
private bool memoryPieceCompletedNotified;
|
|
|
|
public int MemoryPieceTargetCount => memoryPieceTargetCount;
|
|
public InventoryItemType MemoryPieceItemType => memoryPieceItemType;
|
|
|
|
private void Awake()
|
|
{
|
|
if (useSingleton)
|
|
{
|
|
if (Instance != null && Instance != this)
|
|
{
|
|
if (showDebugLog)
|
|
Debug.LogWarning($"[InventoryManager] 중복 InventoryManager 발견: {name}");
|
|
|
|
if (destroyDuplicateManagers)
|
|
{
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Instance = this;
|
|
|
|
if (dontDestroyOnLoad)
|
|
DontDestroyOnLoad(gameObject);
|
|
}
|
|
}
|
|
|
|
EnsureItemDefinitions();
|
|
InitializeFromInspector();
|
|
|
|
if (loadOnAwake)
|
|
LoadInventory(false);
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
NotifyAllItemsChanged();
|
|
NotifyMemoryPieceProgress();
|
|
}
|
|
|
|
private void EnsureItemDefinitions()
|
|
{
|
|
if (itemDefinitions == null)
|
|
itemDefinitions = new List<InventoryItemDefinition>();
|
|
|
|
foreach (InventoryItemType itemType in Enum.GetValues(typeof(InventoryItemType)))
|
|
{
|
|
if (GetDefinitionInternal(itemType) == null)
|
|
{
|
|
itemDefinitions.Add(new InventoryItemDefinition
|
|
{
|
|
itemType = itemType,
|
|
displayName = itemType.ToString(),
|
|
maxCount = defaultMaxCount,
|
|
category = InventoryItemCategory.Other
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private void InitializeFromInspector()
|
|
{
|
|
itemCounts.Clear();
|
|
|
|
foreach (InventoryItemType itemType in Enum.GetValues(typeof(InventoryItemType)))
|
|
itemCounts[itemType] = 0;
|
|
|
|
for (int i = 0; i < initialItems.Count; i++)
|
|
{
|
|
InventoryItemStack stack = initialItems[i];
|
|
if (stack == null)
|
|
continue;
|
|
|
|
itemCounts[stack.itemType] = ClampCount(stack.itemType, stack.count);
|
|
}
|
|
}
|
|
|
|
public void AddItem(InventoryItemType itemType)
|
|
{
|
|
AddItem(itemType, 1);
|
|
}
|
|
|
|
public void AddItem(InventoryItemType itemType, int amount)
|
|
{
|
|
AddItemAndGetAddedAmount(itemType, amount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 실제로 추가된 개수를 반환합니다. 최대 소지 수량에 막히면 요청량보다 적을 수 있습니다.
|
|
/// </summary>
|
|
public int AddItemAndGetAddedAmount(InventoryItemType itemType, int amount)
|
|
{
|
|
if (amount <= 0)
|
|
return 0;
|
|
|
|
int previousCount = GetItemCount(itemType);
|
|
int newCount = ClampCount(itemType, previousCount + amount);
|
|
int addedAmount = Mathf.Max(0, newCount - previousCount);
|
|
|
|
if (addedAmount <= 0)
|
|
{
|
|
RequestMessage($"{GetDisplayName(itemType)}은(는) 더 이상 가질 수 없습니다.");
|
|
|
|
if (showDebugLog)
|
|
Debug.Log($"[InventoryManager] {itemType} 최대 소지 수량 도달: {previousCount}");
|
|
|
|
return 0;
|
|
}
|
|
|
|
itemCounts[itemType] = newCount;
|
|
NotifyItemChanged(itemType, newCount);
|
|
NotifyItemAdded(itemType, addedAmount, newCount);
|
|
AddLog(itemType, addedAmount, "획득");
|
|
|
|
if (saveOnChange)
|
|
SaveInventory();
|
|
|
|
if (showDebugLog)
|
|
Debug.Log($"[InventoryManager] {itemType} +{addedAmount} => {newCount}");
|
|
|
|
return addedAmount;
|
|
}
|
|
|
|
public bool RemoveItem(InventoryItemType itemType)
|
|
{
|
|
return RemoveItem(itemType, 1);
|
|
}
|
|
|
|
public bool RemoveItem(InventoryItemType itemType, int amount)
|
|
{
|
|
if (amount <= 0)
|
|
return false;
|
|
|
|
int current = GetItemCount(itemType);
|
|
|
|
if (current < amount)
|
|
{
|
|
RequestMessage(GetInsufficientMessage(itemType, amount));
|
|
|
|
if (showDebugLog)
|
|
Debug.LogWarning($"[InventoryManager] {itemType} 부족: 현재 {current}, 필요 {amount}");
|
|
|
|
return false;
|
|
}
|
|
|
|
int newCount = current - amount;
|
|
itemCounts[itemType] = newCount;
|
|
NotifyItemChanged(itemType, newCount);
|
|
NotifyItemRemoved(itemType, amount, newCount);
|
|
AddLog(itemType, amount, "소모");
|
|
|
|
if (saveOnChange)
|
|
SaveInventory();
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool UseItem(InventoryItemType itemType)
|
|
{
|
|
return UseItem(itemType, 1);
|
|
}
|
|
|
|
public bool UseItem(InventoryItemType itemType, int amount)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
bool consume = definition == null || definition.consumeOnUse;
|
|
return TryUseItem(itemType, amount, consume, true, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 아이템을 사용합니다. 실제 게임 효과는 성공 이벤트나 외부 스크립트에서 처리하세요.
|
|
/// consume=false이면 보유 여부만 확인하고 개수는 줄이지 않습니다.
|
|
/// showMessages=false이면 부족/성공 안내를 표시하지 않습니다.
|
|
/// </summary>
|
|
public bool TryUseItem(InventoryItemType itemType, int amount, bool consume, bool showMessages = true, bool addUseLog = true)
|
|
{
|
|
amount = Mathf.Max(1, amount);
|
|
|
|
if (!HasItem(itemType, amount))
|
|
{
|
|
if (showMessages)
|
|
RequestMessage(GetInsufficientMessage(itemType, amount));
|
|
|
|
return false;
|
|
}
|
|
|
|
if (consume && !RemoveItem(itemType, amount))
|
|
return false;
|
|
|
|
ItemUsed?.Invoke(itemType, GetItemCount(itemType));
|
|
onItemUsed?.Invoke(itemType, GetItemCount(itemType));
|
|
|
|
// consume=true인 경우 RemoveItem에서 이미 "소모" 로그가 남습니다.
|
|
// consume=false인 빠른 사용(예: 나침반 확인)은 별도 "사용" 로그를 남깁니다.
|
|
if (addUseLog && !consume)
|
|
AddLog(itemType, amount, "사용");
|
|
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
if (showMessages)
|
|
{
|
|
string message = definition != null && !string.IsNullOrWhiteSpace(definition.useSuccessMessage)
|
|
? definition.useSuccessMessage
|
|
: $"{GetDisplayName(itemType)}을(를) 사용했습니다.";
|
|
RequestMessage(message);
|
|
}
|
|
|
|
if (saveOnChange)
|
|
SaveInventory();
|
|
|
|
return true;
|
|
}
|
|
|
|
public void SetItemCount(InventoryItemType itemType, int count)
|
|
{
|
|
int clampedCount = ClampCount(itemType, count);
|
|
int previousCount = GetItemCount(itemType);
|
|
|
|
if (previousCount == clampedCount)
|
|
return;
|
|
|
|
itemCounts[itemType] = clampedCount;
|
|
NotifyItemChanged(itemType, clampedCount);
|
|
|
|
if (saveOnChange)
|
|
SaveInventory();
|
|
}
|
|
|
|
public int GetItemCount(InventoryItemType itemType)
|
|
{
|
|
if (itemCounts.TryGetValue(itemType, out int count))
|
|
return count;
|
|
|
|
return 0;
|
|
}
|
|
|
|
public bool HasItem(InventoryItemType itemType)
|
|
{
|
|
return GetItemCount(itemType) > 0;
|
|
}
|
|
|
|
public bool HasItem(InventoryItemType itemType, int requiredAmount)
|
|
{
|
|
return GetItemCount(itemType) >= Mathf.Max(1, requiredAmount);
|
|
}
|
|
|
|
public int GetMaxCount(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
|
|
if (definition != null && definition.maxCount > 0)
|
|
return definition.maxCount;
|
|
|
|
return Mathf.Max(0, defaultMaxCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 소지 수량과 최대 소지 수량을 기준으로 실제로 더 넣을 수 있는 개수를 반환합니다.
|
|
/// maxCount가 0 이하이면 제한 없음으로 취급합니다.
|
|
/// </summary>
|
|
public int GetAddableAmount(InventoryItemType itemType, int requestedAmount)
|
|
{
|
|
requestedAmount = Mathf.Max(0, requestedAmount);
|
|
if (requestedAmount <= 0)
|
|
return 0;
|
|
|
|
int maxCount = GetMaxCount(itemType);
|
|
if (maxCount <= 0)
|
|
return requestedAmount;
|
|
|
|
int current = GetItemCount(itemType);
|
|
return Mathf.Clamp(maxCount - current, 0, requestedAmount);
|
|
}
|
|
|
|
public bool CanAddItem(InventoryItemType itemType, int amount = 1, bool requireFullAmount = false)
|
|
{
|
|
amount = Mathf.Max(1, amount);
|
|
int addableAmount = GetAddableAmount(itemType, amount);
|
|
return requireFullAmount ? addableAmount >= amount : addableAmount > 0;
|
|
}
|
|
|
|
public InventoryItemDefinition GetDefinition(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinitionInternal(itemType);
|
|
return definition;
|
|
}
|
|
|
|
private InventoryItemDefinition GetDefinitionInternal(InventoryItemType itemType)
|
|
{
|
|
if (itemDefinitions == null)
|
|
return null;
|
|
|
|
for (int i = 0; i < itemDefinitions.Count; i++)
|
|
{
|
|
if (itemDefinitions[i] != null && itemDefinitions[i].itemType == itemType)
|
|
return itemDefinitions[i];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public string GetDisplayName(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
return definition != null && !string.IsNullOrWhiteSpace(definition.displayName)
|
|
? definition.displayName
|
|
: itemType.ToString();
|
|
}
|
|
|
|
public string GetDescription(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
return definition != null ? definition.description : string.Empty;
|
|
}
|
|
|
|
public string GetGoalHint(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
return definition != null ? definition.goalHint : string.Empty;
|
|
}
|
|
|
|
public Sprite GetIcon(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
return definition != null ? definition.icon : null;
|
|
}
|
|
|
|
public Sprite GetSlotBackground(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
return definition != null ? definition.slotBackground : null;
|
|
}
|
|
|
|
public AudioClip GetAcquisitionClip(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
return definition != null ? definition.acquisitionClip : null;
|
|
}
|
|
|
|
public AudioClip GetUseClip(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
return definition != null ? definition.useClip : null;
|
|
}
|
|
|
|
public bool IsImportantItem(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
return definition != null && definition.importantItem;
|
|
}
|
|
|
|
public InventoryItemCategory GetCategory(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
return definition != null ? definition.category : InventoryItemCategory.Other;
|
|
}
|
|
|
|
public bool IsUsable(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
return definition != null && definition.usable;
|
|
}
|
|
|
|
public bool RequiresUseConfirmation(InventoryItemType itemType)
|
|
{
|
|
InventoryItemDefinition definition = GetDefinition(itemType);
|
|
return definition != null && definition.requireUseConfirmation;
|
|
}
|
|
|
|
public string GetInsufficientMessage(InventoryItemType itemType, int requiredAmount)
|
|
{
|
|
int current = GetItemCount(itemType);
|
|
return $"{GetDisplayName(itemType)}이(가) 부족합니다. 필요: {Mathf.Max(1, requiredAmount)}개 / 현재: {current}개";
|
|
}
|
|
|
|
public IReadOnlyList<InventoryLogEntry> GetRecentLogs()
|
|
{
|
|
return new List<InventoryLogEntry>(recentLogs);
|
|
}
|
|
|
|
public IReadOnlyDictionary<InventoryItemType, int> GetAllItemCounts()
|
|
{
|
|
return new Dictionary<InventoryItemType, int>(itemCounts);
|
|
}
|
|
|
|
public void ClearInventory()
|
|
{
|
|
foreach (InventoryItemType itemType in Enum.GetValues(typeof(InventoryItemType)))
|
|
itemCounts[itemType] = 0;
|
|
|
|
NotifyAllItemsChanged();
|
|
NotifyMemoryPieceProgress();
|
|
|
|
if (saveOnChange)
|
|
SaveInventory();
|
|
}
|
|
|
|
public void ResetToInitialItems()
|
|
{
|
|
InitializeFromInspector();
|
|
NotifyAllItemsChanged();
|
|
NotifyMemoryPieceProgress();
|
|
|
|
if (saveOnChange)
|
|
SaveInventory();
|
|
}
|
|
|
|
public bool IsPersistentKeyConsumed(string key)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
return false;
|
|
|
|
return consumedPersistentKeys.Contains(key);
|
|
}
|
|
|
|
public void MarkPersistentKeyConsumed(string key)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
return;
|
|
|
|
if (consumedPersistentKeys.Add(key) && (saveOnChange || forceSavePersistentState))
|
|
SaveInventory();
|
|
}
|
|
|
|
public void ResetPersistentKey(string key)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
return;
|
|
|
|
if (consumedPersistentKeys.Remove(key) && (saveOnChange || forceSavePersistentState))
|
|
SaveInventory();
|
|
}
|
|
|
|
public void RequestMessage(string message)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(message))
|
|
return;
|
|
|
|
MessageRequested?.Invoke(message);
|
|
onMessageRequested?.Invoke(message);
|
|
|
|
if (showDebugLog)
|
|
Debug.Log($"[InventoryManager] Message: {message}");
|
|
}
|
|
|
|
private void AddLog(InventoryItemType itemType, int amount, string action)
|
|
{
|
|
if (!keepRecentLogs)
|
|
return;
|
|
|
|
InventoryLogEntry entry = new InventoryLogEntry(itemType, amount, action, GetDisplayName(itemType));
|
|
recentLogs.Insert(0, entry);
|
|
|
|
while (recentLogs.Count > Mathf.Max(1, maxRecentLogCount))
|
|
recentLogs.RemoveAt(recentLogs.Count - 1);
|
|
|
|
LogAdded?.Invoke(entry);
|
|
}
|
|
|
|
public void SaveInventory()
|
|
{
|
|
InventorySaveData saveData = new InventorySaveData();
|
|
|
|
foreach (KeyValuePair<InventoryItemType, int> pair in itemCounts)
|
|
{
|
|
saveData.items.Add(new InventorySavedItemCount
|
|
{
|
|
itemTypeName = pair.Key.ToString(),
|
|
count = Mathf.Max(0, pair.Value)
|
|
});
|
|
}
|
|
|
|
foreach (string key in consumedPersistentKeys)
|
|
saveData.consumedPersistentKeys.Add(key);
|
|
|
|
string json = JsonUtility.ToJson(saveData);
|
|
PlayerPrefs.SetString(saveKey, json);
|
|
PlayerPrefs.Save();
|
|
|
|
if (showDebugLog)
|
|
Debug.Log("[InventoryManager] 인벤토리 저장 완료");
|
|
}
|
|
|
|
public bool LoadInventory()
|
|
{
|
|
return LoadInventory(true);
|
|
}
|
|
|
|
private bool LoadInventory(bool notify)
|
|
{
|
|
if (!PlayerPrefs.HasKey(saveKey))
|
|
return false;
|
|
|
|
string json = PlayerPrefs.GetString(saveKey, string.Empty);
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
return false;
|
|
|
|
InventorySaveData saveData;
|
|
|
|
try
|
|
{
|
|
saveData = JsonUtility.FromJson<InventorySaveData>(json);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogWarning($"[InventoryManager] 저장 데이터 읽기 실패: {exception.Message}");
|
|
return false;
|
|
}
|
|
|
|
InitializeFromInspector();
|
|
consumedPersistentKeys.Clear();
|
|
|
|
if (saveData != null)
|
|
{
|
|
for (int i = 0; i < saveData.items.Count; i++)
|
|
{
|
|
InventorySavedItemCount savedItem = saveData.items[i];
|
|
if (savedItem == null || string.IsNullOrWhiteSpace(savedItem.itemTypeName))
|
|
continue;
|
|
|
|
if (Enum.TryParse(savedItem.itemTypeName, out InventoryItemType itemType))
|
|
itemCounts[itemType] = ClampCount(itemType, savedItem.count);
|
|
}
|
|
|
|
for (int i = 0; i < saveData.consumedPersistentKeys.Count; i++)
|
|
{
|
|
string key = saveData.consumedPersistentKeys[i];
|
|
if (!string.IsNullOrWhiteSpace(key))
|
|
consumedPersistentKeys.Add(key);
|
|
}
|
|
}
|
|
|
|
if (notify)
|
|
{
|
|
NotifyAllItemsChanged();
|
|
NotifyMemoryPieceProgress();
|
|
}
|
|
|
|
if (showDebugLog)
|
|
Debug.Log("[InventoryManager] 인벤토리 불러오기 완료");
|
|
|
|
return true;
|
|
}
|
|
|
|
public void DeleteSavedInventory()
|
|
{
|
|
PlayerPrefs.DeleteKey(saveKey);
|
|
PlayerPrefs.Save();
|
|
|
|
consumedPersistentKeys.Clear();
|
|
recentLogs.Clear();
|
|
memoryPieceCompletedNotified = false;
|
|
|
|
// ResetToInitialItems()를 호출하면 saveOnChange가 true일 때 삭제 직후 다시 저장 파일을 만들 수 있으므로
|
|
// 여기서는 직접 초기화와 알림만 수행합니다.
|
|
InitializeFromInspector();
|
|
NotifyAllItemsChanged();
|
|
NotifyMemoryPieceProgress();
|
|
|
|
RequestMessage("인벤토리 저장 데이터를 삭제했습니다.");
|
|
}
|
|
|
|
private int ClampCount(InventoryItemType itemType, int count)
|
|
{
|
|
int safeCount = Mathf.Max(0, count);
|
|
int maxCount = GetMaxCount(itemType);
|
|
|
|
if (maxCount > 0)
|
|
safeCount = Mathf.Min(safeCount, maxCount);
|
|
|
|
return safeCount;
|
|
}
|
|
|
|
private void NotifyItemChanged(InventoryItemType itemType, int count)
|
|
{
|
|
ItemCountChanged?.Invoke(itemType, count);
|
|
InventoryChanged?.Invoke();
|
|
|
|
onItemCountChanged?.Invoke(itemType, count);
|
|
onInventoryChanged?.Invoke();
|
|
|
|
if (itemType == memoryPieceItemType)
|
|
NotifyMemoryPieceProgress();
|
|
}
|
|
|
|
private void NotifyItemAdded(InventoryItemType itemType, int addedAmount, int totalCount)
|
|
{
|
|
ItemAdded?.Invoke(itemType, addedAmount, totalCount);
|
|
onItemAdded?.Invoke(itemType, addedAmount, totalCount);
|
|
}
|
|
|
|
private void NotifyItemRemoved(InventoryItemType itemType, int removedAmount, int totalCount)
|
|
{
|
|
ItemRemoved?.Invoke(itemType, removedAmount, totalCount);
|
|
onItemRemoved?.Invoke(itemType, removedAmount, totalCount);
|
|
}
|
|
|
|
private void NotifyAllItemsChanged()
|
|
{
|
|
foreach (KeyValuePair<InventoryItemType, int> pair in itemCounts)
|
|
{
|
|
ItemCountChanged?.Invoke(pair.Key, pair.Value);
|
|
onItemCountChanged?.Invoke(pair.Key, pair.Value);
|
|
}
|
|
|
|
InventoryChanged?.Invoke();
|
|
onInventoryChanged?.Invoke();
|
|
}
|
|
|
|
private void NotifyMemoryPieceProgress()
|
|
{
|
|
int current = GetItemCount(memoryPieceItemType);
|
|
int target = Mathf.Max(1, memoryPieceTargetCount);
|
|
|
|
MemoryPieceProgressChanged?.Invoke(current, target);
|
|
onMemoryPieceProgressChanged?.Invoke(current, target);
|
|
|
|
if (current < target)
|
|
{
|
|
memoryPieceCompletedNotified = false;
|
|
return;
|
|
}
|
|
|
|
if (!memoryPieceCompletedNotified)
|
|
{
|
|
memoryPieceCompletedNotified = true;
|
|
MemoryPieceCompleted?.Invoke();
|
|
onMemoryPieceCompleted?.Invoke();
|
|
}
|
|
}
|
|
|
|
// 개발용 버튼/UnityEvent 연결용 메서드입니다.
|
|
public void DebugAddFish() => AddItem(InventoryItemType.Fish, 1);
|
|
public void DebugAddCompass() => AddItem(InventoryItemType.OldCompass, 1);
|
|
public void DebugAddTrash() => AddItem(InventoryItemType.Trash, 1);
|
|
public void DebugAddBottle() => AddItem(InventoryItemType.Bottle, 1);
|
|
public void DebugAddMemoryPiece() => AddItem(InventoryItemType.MemoryPiece, 1);
|
|
public void DebugClearInventory() => ClearInventory();
|
|
public void DebugDeleteSave() => DeleteSavedInventory();
|
|
|
|
#if UNITY_EDITOR
|
|
private void OnValidate()
|
|
{
|
|
if (defaultMaxCount < 0)
|
|
defaultMaxCount = 0;
|
|
|
|
if (memoryPieceTargetCount < 1)
|
|
memoryPieceTargetCount = 1;
|
|
|
|
if (maxRecentLogCount < 1)
|
|
maxRecentLogCount = 1;
|
|
|
|
if (string.IsNullOrWhiteSpace(saveKey))
|
|
saveKey = "Inventory_SaveData";
|
|
|
|
if (initialItems != null)
|
|
{
|
|
for (int i = 0; i < initialItems.Count; i++)
|
|
{
|
|
if (initialItems[i] != null)
|
|
initialItems[i].count = Mathf.Max(0, initialItems[i].count);
|
|
}
|
|
}
|
|
|
|
if (itemDefinitions != null)
|
|
{
|
|
for (int i = 0; i < itemDefinitions.Count; i++)
|
|
{
|
|
if (itemDefinitions[i] != null)
|
|
itemDefinitions[i].maxCount = Mathf.Max(0, itemDefinitions[i].maxCount);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
[Serializable]
|
|
private class InventorySaveData
|
|
{
|
|
public List<InventorySavedItemCount> items = new List<InventorySavedItemCount>();
|
|
public List<string> consumedPersistentKeys = new List<string>();
|
|
}
|
|
|
|
[Serializable]
|
|
private class InventorySavedItemCount
|
|
{
|
|
public string itemTypeName;
|
|
public int count;
|
|
}
|
|
}
|