2026.06.24
This commit is contained in:
@@ -3,6 +3,33 @@
|
||||
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
|
||||
{
|
||||
@@ -11,40 +38,209 @@ public class InventoryItemStack
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class InventoryItemChangedEvent : UnityEvent<InventoryItemType, int>
|
||||
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>
|
||||
/// 실제 아이템 개수를 관리하는 중심 스크립트입니다.
|
||||
/// UI를 직접 수정하지 않고, 아이템 개수 변경 이벤트만 알려줍니다.
|
||||
/// 기능: 싱글톤, 저장/불러오기, 최대 소지 수량, 사용, 부족 안내, 획득 로그, 기억의 조각 진행도 이벤트.
|
||||
/// </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()
|
||||
@@ -60,7 +256,7 @@ private void InitializeFromInspector()
|
||||
if (stack == null)
|
||||
continue;
|
||||
|
||||
itemCounts[stack.itemType] = Mathf.Max(0, stack.count);
|
||||
itemCounts[stack.itemType] = ClampCount(stack.itemType, stack.count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,14 +267,43 @@ public void AddItem(InventoryItemType itemType)
|
||||
|
||||
public void AddItem(InventoryItemType itemType, int amount)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
AddItemAndGetAddedAmount(itemType, amount);
|
||||
}
|
||||
|
||||
int newCount = GetItemCount(itemType) + amount;
|
||||
SetItemCount(itemType, newCount);
|
||||
/// <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} +{amount} => {newCount}");
|
||||
Debug.Log($"[InventoryManager] {itemType} +{addedAmount} => {newCount}");
|
||||
|
||||
return addedAmount;
|
||||
}
|
||||
|
||||
public bool RemoveItem(InventoryItemType itemType)
|
||||
@@ -95,19 +320,84 @@ public bool RemoveItem(InventoryItemType itemType, int amount)
|
||||
|
||||
if (current < amount)
|
||||
{
|
||||
RequestMessage(GetInsufficientMessage(itemType, amount));
|
||||
|
||||
if (showDebugLog)
|
||||
Debug.LogWarning($"[InventoryManager] {itemType} 부족: 현재 {current}, 필요 {amount}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
SetItemCount(itemType, current - amount);
|
||||
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 = Mathf.Max(0, count);
|
||||
int clampedCount = ClampCount(itemType, count);
|
||||
int previousCount = GetItemCount(itemType);
|
||||
|
||||
if (previousCount == clampedCount)
|
||||
@@ -115,6 +405,9 @@ public void SetItemCount(InventoryItemType itemType, int count)
|
||||
|
||||
itemCounts[itemType] = clampedCount;
|
||||
NotifyItemChanged(itemType, clampedCount);
|
||||
|
||||
if (saveOnChange)
|
||||
SaveInventory();
|
||||
}
|
||||
|
||||
public int GetItemCount(InventoryItemType itemType)
|
||||
@@ -135,17 +428,331 @@ 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 IReadOnlyDictionary<InventoryItemType, int> GetAllItemCounts()
|
||||
public void ResetToInitialItems()
|
||||
{
|
||||
return itemCounts;
|
||||
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)
|
||||
@@ -155,6 +762,21 @@ private void NotifyItemChanged(InventoryItemType itemType, int count)
|
||||
|
||||
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()
|
||||
@@ -169,17 +791,83 @@ private void NotifyAllItemsChanged()
|
||||
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 (initialItems == null)
|
||||
return;
|
||||
if (defaultMaxCount < 0)
|
||||
defaultMaxCount = 0;
|
||||
|
||||
for (int i = 0; i < initialItems.Count; i++)
|
||||
if (memoryPieceTargetCount < 1)
|
||||
memoryPieceTargetCount = 1;
|
||||
|
||||
if (maxRecentLogCount < 1)
|
||||
maxRecentLogCount = 1;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(saveKey))
|
||||
saveKey = "Inventory_SaveData";
|
||||
|
||||
if (initialItems != null)
|
||||
{
|
||||
if (initialItems[i] != null)
|
||||
initialItems[i].count = Mathf.Max(0, initialItems[i].count);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user