2026.06.24

This commit is contained in:
2026-06-24 17:13:40 +09:00
parent b1e85a5b89
commit d601161390
86 changed files with 10287 additions and 336 deletions

View File

@@ -1,76 +1,205 @@
using UnityEngine;
using UnityEngine.Events;
#if UNITY_EDITOR
using UnityEditor;
#endif
/// <summary>
/// 미니게임 성공, 퀘스트 완료, 버튼 이벤트 등에서 아이템을 지급할 때 사용합니다.
/// 예: 낚시 성공 이벤트 -> ItemReward.GiveReward()
/// 미니게임 성공, 퀘스트 완료, 버튼 이벤트, 아이템 사용 상호작용에 사용합니다.
/// 기능: 보상 지급, 한 번만 지급, 저장, 필요 아이템 검사, 필요 아이템 소모, 부족 메시지.
/// 예: 고양이 영입 = requireItemBeforeAction true + Fish 3개 소모 + giveRewardItem false + onActionCompleted에 고양이 합류 이벤트 연결.
/// 예: 쓰레기 정화 = requireItemBeforeAction true + Trash 5개 소모 + onActionCompleted에 정화 이벤트 연결.
/// </summary>
[DisallowMultipleComponent]
public class ItemReward : MonoBehaviour
{
[Header("Reference")]
[SerializeField] private InventoryManager inventoryManager;
[Header("Reward")]
[SerializeField] private InventoryItemType rewardItemType = InventoryItemType.Fish;
[SerializeField] private int rewardAmount = 1;
[SerializeField] private bool giveOnlyOnce = false;
[Header("Auto Find")]
[SerializeField] private bool autoFindManager = true;
[Header("Required Item / Interaction Cost")]
[Tooltip("true면 액션 전에 특정 아이템이 필요한지 검사합니다.")]
[SerializeField] private bool requireItemBeforeAction = false;
[SerializeField] private InventoryItemType requiredItemType = InventoryItemType.Fish;
[Min(1)] [SerializeField] private int requiredAmount = 1;
[SerializeField] private bool consumeRequiredItem = true;
[SerializeField] private string requirementFailMessage;
[SerializeField] private string requirementSuccessMessage;
[Header("Reward")]
[Tooltip("false면 보상 아이템을 지급하지 않고, 필요 아이템 소모와 이벤트만 실행합니다.")]
[SerializeField] private bool giveRewardItem = true;
[SerializeField] private InventoryItemType rewardItemType = InventoryItemType.Fish;
[Min(1)] [SerializeField] private int rewardAmount = 1;
[Tooltip("true이면 보상을 전부 받을 수 있을 때만 필요 아이템을 소모합니다. 보상 공간 부족으로 비용만 사라지는 상황을 방지합니다.")]
[SerializeField] private bool requireFullRewardCapacity = true;
[SerializeField] private bool giveOnlyOnce = false;
[Header("Persistent State")]
[Tooltip("true면 액션 완료 여부가 저장됩니다. 퀘스트/미니게임 보상 중복 방지에 사용하세요.")]
[SerializeField] private bool persistActionState = false;
[SerializeField] private string persistentActionKey;
[Header("Messages")]
[SerializeField] private bool showMessages = true;
[SerializeField] private string rewardGivenMessage;
[SerializeField] private string alreadyCompletedMessage = "이미 완료된 보상입니다.";
[Header("Events")]
public UnityEvent onActionCompleted;
public UnityEvent onRewardGiven;
public UnityEvent onRewardAlreadyGiven;
public UnityEvent onAlreadyCompleted;
public UnityEvent onRequirementMissing;
public UnityEvent onRequiredItemConsumed;
public UnityEvent onActionFailed;
[Header("Debug")]
[SerializeField] private bool showDebugLog = true;
private bool alreadyGiven;
private bool alreadyCompleted;
private void Awake()
{
if (inventoryManager == null && autoFindManager)
inventoryManager = FindFirstObjectByType<InventoryManager>();
ResolveManager();
ResolvePersistentKey();
}
private void Start()
{
if (persistActionState && ResolveManager() != null && inventoryManager.IsPersistentKeyConsumed(persistentActionKey))
alreadyCompleted = true;
}
public void GiveReward()
{
if (giveOnlyOnce && alreadyGiven)
ExecuteAction();
}
public void ExecuteAction()
{
InventoryManager manager = ResolveManager();
if (manager == null)
{
onRewardAlreadyGiven?.Invoke();
Debug.LogWarning("[ItemReward] InventoryManager가 연결되지 않았습니다.", this);
onActionFailed?.Invoke();
return;
}
if (giveOnlyOnce && alreadyCompleted)
{
if (showMessages)
manager.RequestMessage(alreadyCompletedMessage);
onAlreadyCompleted?.Invoke();
if (showDebugLog)
Debug.Log("[ItemReward] 이미 지급된 아이템 보상입니다.");
Debug.Log("[ItemReward] 이미 완료된 액션입니다.", this);
return;
}
if (inventoryManager == null)
int requestedRewardAmount = Mathf.Max(1, rewardAmount);
if (giveRewardItem)
{
Debug.LogWarning("[ItemReward] InventoryManager가 연결되지 않았습니다.");
return;
int addableAmount = manager.GetAddableAmount(rewardItemType, requestedRewardAmount);
bool rewardCapacityOk = requireFullRewardCapacity ? addableAmount >= requestedRewardAmount : addableAmount > 0;
if (!rewardCapacityOk)
{
if (showMessages)
manager.RequestMessage($"{manager.GetDisplayName(rewardItemType)}은(는) 더 이상 받을 수 없습니다.");
onActionFailed?.Invoke();
return;
}
}
int amount = Mathf.Max(1, rewardAmount);
inventoryManager.AddItem(rewardItemType, amount);
alreadyGiven = true;
if (requireItemBeforeAction)
{
if (!manager.HasItem(requiredItemType, requiredAmount))
{
string message = string.IsNullOrWhiteSpace(requirementFailMessage)
? manager.GetInsufficientMessage(requiredItemType, requiredAmount)
: requirementFailMessage;
onRewardGiven?.Invoke();
if (showMessages)
manager.RequestMessage(message);
onRequirementMissing?.Invoke();
onActionFailed?.Invoke();
return;
}
if (consumeRequiredItem)
{
bool consumed = manager.TryUseItem(requiredItemType, requiredAmount, true, false, true);
if (!consumed)
{
onActionFailed?.Invoke();
return;
}
onRequiredItemConsumed?.Invoke();
}
if (showMessages && !string.IsNullOrWhiteSpace(requirementSuccessMessage))
manager.RequestMessage(requirementSuccessMessage);
}
int actuallyGiven = 0;
if (giveRewardItem)
{
actuallyGiven = manager.AddItemAndGetAddedAmount(rewardItemType, requestedRewardAmount);
if (actuallyGiven <= 0)
{
onActionFailed?.Invoke();
return;
}
onRewardGiven?.Invoke();
}
alreadyCompleted = true;
if (persistActionState)
manager.MarkPersistentKeyConsumed(persistentActionKey);
if (showMessages)
{
if (!string.IsNullOrWhiteSpace(rewardGivenMessage))
manager.RequestMessage(rewardGivenMessage);
else if (!giveRewardItem)
manager.RequestMessage("완료되었습니다.");
}
onActionCompleted?.Invoke();
if (showDebugLog)
Debug.Log($"[ItemReward] {rewardItemType} +{amount}");
{
if (giveRewardItem)
Debug.Log($"[ItemReward] {rewardItemType} +{actuallyGiven}", this);
else
Debug.Log("[ItemReward] 아이템 지급 없이 액션 완료", this);
}
}
/// <summary>
/// 이번 호출에서만 amount를 바꿔 지급합니다. Inspector의 rewardAmount 값은 바꾸지 않습니다.
/// </summary>
public void GiveRewardAmount(int amount)
{
int previousAmount = rewardAmount;
rewardAmount = Mathf.Max(1, amount);
GiveReward();
ExecuteAction();
rewardAmount = previousAmount;
}
public void ResetRewardState()
public void ResetActionState()
{
alreadyGiven = false;
alreadyCompleted = false;
if (persistActionState && ResolveManager() != null)
inventoryManager.ResetPersistentKey(persistentActionKey);
}
public void SetRewardItemType(InventoryItemType itemType)
@@ -78,8 +207,67 @@ public void SetRewardItemType(InventoryItemType itemType)
rewardItemType = itemType;
}
public void SetRequiredItemType(InventoryItemType itemType)
{
requiredItemType = itemType;
}
public void SetInventoryManager(InventoryManager manager)
{
inventoryManager = manager;
}
private InventoryManager ResolveManager()
{
if (inventoryManager != null)
return inventoryManager;
if (InventoryManager.Instance != null)
inventoryManager = InventoryManager.Instance;
else if (autoFindManager)
inventoryManager = FindFirstObjectByType<InventoryManager>();
return inventoryManager;
}
private void ResolvePersistentKey()
{
if (!persistActionState)
return;
if (string.IsNullOrWhiteSpace(persistentActionKey))
persistentActionKey = $"Reward_{gameObject.scene.name}_{GetHierarchyPath(transform)}";
}
private string GetHierarchyPath(Transform target)
{
if (target == null)
return "Unknown";
string path = target.name;
Transform parent = target.parent;
while (parent != null)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
#if UNITY_EDITOR
private void OnValidate()
{
requiredAmount = Mathf.Max(1, requiredAmount);
rewardAmount = Mathf.Max(1, rewardAmount);
if (persistActionState && string.IsNullOrWhiteSpace(persistentActionKey))
{
string editorId = GlobalObjectId.GetGlobalObjectIdSlow(this).ToString();
persistentActionKey = string.IsNullOrWhiteSpace(editorId)
? $"Reward_{gameObject.scene.name}_{GetHierarchyPath(transform)}"
: $"Reward_{editorId}";
}
}
#endif
}