597 lines
20 KiB
C#
597 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Inventory;
|
|
using Interaction.Conditions;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
|
|
namespace Interaction
|
|
{
|
|
public class HydroponicsGrowRack : InteractConditionBehaviour, IInteractable
|
|
{
|
|
[Serializable]
|
|
private class ItemAmount
|
|
{
|
|
public ItemData item;
|
|
[Min(1)]
|
|
public int amount = 1;
|
|
}
|
|
|
|
private enum RackState
|
|
{
|
|
Empty = 0,
|
|
Growing = 1,
|
|
Mature = 2
|
|
}
|
|
|
|
private enum PendingAction
|
|
{
|
|
None = 0,
|
|
Plant = 1,
|
|
Harvest = 2
|
|
}
|
|
|
|
[Header("物品配置")]
|
|
[SerializeField] private List<ItemAmount> plantCosts = new List<ItemAmount>();
|
|
[SerializeField] private List<ItemAmount> harvestRewards = new List<ItemAmount>();
|
|
|
|
[Header("事件")]
|
|
[SerializeField] private UnityEvent onHarvested;
|
|
|
|
[Header("成长配置")]
|
|
[Tooltip("按顺序填写各阶段子物体(例如 S1,S2,S3,S4)。脚本会在运行时只启用当前阶段。")]
|
|
[SerializeField] private List<GameObject> growthStages = new List<GameObject>();
|
|
|
|
[Tooltip("若填写且长度等于阶段数,则按每个阶段持续时间驱动;否则使用 totalGrowSeconds 均分。")]
|
|
[SerializeField] private List<float> stageDurationsSeconds = new List<float>();
|
|
|
|
[Min(0.01f)]
|
|
[SerializeField] private float totalGrowSeconds = 120f;
|
|
|
|
[Header("状态(运行时)")]
|
|
[SerializeField] private RackState state = RackState.Empty;
|
|
|
|
[SerializeField] private int currentStageIndex = -1;
|
|
[SerializeField] private float plantedAtTime = -1f;
|
|
|
|
private PendingAction pendingAction = PendingAction.None;
|
|
private RackState stateBeforeAction;
|
|
private int stageBeforeAction;
|
|
private float plantedAtBeforeAction;
|
|
|
|
private float[] cumulativeStageEnds;
|
|
private float totalDurationCached;
|
|
|
|
private void Awake()
|
|
{
|
|
RebuildDurationsCache();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
if (growthStages == null) growthStages = new List<GameObject>();
|
|
if (growthStages.Count == 0) AutoFillGrowthStagesFromChildrenRuntime();
|
|
|
|
RebuildDurationsCache();
|
|
|
|
int stageCount = growthStages == null ? 0 : growthStages.Count;
|
|
if (stageCount > 0)
|
|
{
|
|
int activeIndex = -1;
|
|
for (int i = 0; i < stageCount; i++)
|
|
{
|
|
var go = growthStages[i];
|
|
if (go == null) continue;
|
|
if (!go.activeSelf) continue;
|
|
activeIndex = i;
|
|
break;
|
|
}
|
|
|
|
if (activeIndex >= 0)
|
|
{
|
|
currentStageIndex = Mathf.Clamp(activeIndex, 0, stageCount - 1);
|
|
ApplyVisuals(currentStageIndex);
|
|
|
|
if (currentStageIndex >= stageCount - 1)
|
|
{
|
|
state = RackState.Mature;
|
|
plantedAtTime = Time.time - totalDurationCached;
|
|
}
|
|
else
|
|
{
|
|
state = RackState.Growing;
|
|
float stageStartElapsed = currentStageIndex <= 0 ? 0f : cumulativeStageEnds[currentStageIndex - 1];
|
|
plantedAtTime = Time.time - stageStartElapsed;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (state == RackState.Empty)
|
|
{
|
|
currentStageIndex = -1;
|
|
plantedAtTime = -1f;
|
|
ApplyVisuals(-1);
|
|
return;
|
|
}
|
|
|
|
if (growthStages == null || growthStages.Count == 0)
|
|
{
|
|
currentStageIndex = -1;
|
|
ApplyVisuals(-1);
|
|
return;
|
|
}
|
|
|
|
if (state == RackState.Growing)
|
|
{
|
|
if (plantedAtTime < 0f) plantedAtTime = Time.time;
|
|
currentStageIndex = Mathf.Clamp(currentStageIndex, 0, growthStages.Count - 1);
|
|
if (currentStageIndex < 0) currentStageIndex = 0;
|
|
ApplyVisuals(currentStageIndex);
|
|
return;
|
|
}
|
|
|
|
if (state == RackState.Mature)
|
|
{
|
|
currentStageIndex = Mathf.Clamp(growthStages.Count - 1, 0, growthStages.Count - 1);
|
|
ApplyVisuals(currentStageIndex);
|
|
}
|
|
}
|
|
|
|
private void OnValidate()
|
|
{
|
|
if (totalGrowSeconds < 0.01f) totalGrowSeconds = 0.01f;
|
|
|
|
if (growthStages == null) growthStages = new List<GameObject>();
|
|
if (stageDurationsSeconds == null) stageDurationsSeconds = new List<float>();
|
|
if (plantCosts == null) plantCosts = new List<ItemAmount>();
|
|
if (harvestRewards == null) harvestRewards = new List<ItemAmount>();
|
|
|
|
for (int i = 0; i < plantCosts.Count; i++)
|
|
{
|
|
if (plantCosts[i] == null) continue;
|
|
if (plantCosts[i].amount < 1) plantCosts[i].amount = 1;
|
|
}
|
|
|
|
for (int i = 0; i < harvestRewards.Count; i++)
|
|
{
|
|
if (harvestRewards[i] == null) continue;
|
|
if (harvestRewards[i].amount < 1) harvestRewards[i].amount = 1;
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (state != RackState.Growing) return;
|
|
if (growthStages == null || growthStages.Count == 0) return;
|
|
if (plantedAtTime == -1f) return;
|
|
|
|
float elapsed = Time.time - plantedAtTime;
|
|
int stageIndex = GetStageIndexByElapsed(elapsed);
|
|
if (stageIndex != currentStageIndex)
|
|
{
|
|
currentStageIndex = stageIndex;
|
|
ApplyVisuals(currentStageIndex);
|
|
}
|
|
|
|
if (elapsed >= totalDurationCached)
|
|
{
|
|
state = RackState.Mature;
|
|
currentStageIndex = Mathf.Clamp(growthStages.Count - 1, 0, growthStages.Count - 1);
|
|
ApplyVisuals(currentStageIndex);
|
|
}
|
|
}
|
|
|
|
public override bool CanInteract(GameObject interactor, out string failReason)
|
|
{
|
|
failReason = string.Empty;
|
|
|
|
if (InventorySystem.Instance == null)
|
|
{
|
|
failReason = "未找到玩家背包";
|
|
return false;
|
|
}
|
|
|
|
if (plantCosts == null || plantCosts.Count == 0 || harvestRewards == null || harvestRewards.Count == 0)
|
|
{
|
|
failReason = "未配置种植消耗或收获产出";
|
|
return false;
|
|
}
|
|
|
|
if (state == RackState.Growing)
|
|
{
|
|
failReason = "正在生长";
|
|
return false;
|
|
}
|
|
|
|
if (state == RackState.Empty)
|
|
{
|
|
for (int i = 0; i < plantCosts.Count; i++)
|
|
{
|
|
var cost = plantCosts[i];
|
|
if (cost == null || cost.item == null)
|
|
{
|
|
failReason = "未配置种植消耗";
|
|
return false;
|
|
}
|
|
|
|
int have = GetItemCount(InventorySystem.Instance, cost.item);
|
|
if (have < Mathf.Max(1, cost.amount))
|
|
{
|
|
failReason = "缺少种植材料";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (state == RackState.Mature)
|
|
{
|
|
if (!CanAddAll(InventorySystem.Instance, harvestRewards))
|
|
{
|
|
failReason = "背包已满";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
failReason = "状态错误";
|
|
return false;
|
|
}
|
|
|
|
public override void OnInteractSucceeded(GameObject interactor)
|
|
{
|
|
if (InventorySystem.Instance == null)
|
|
{
|
|
pendingAction = PendingAction.None;
|
|
return;
|
|
}
|
|
|
|
if (pendingAction == PendingAction.Plant)
|
|
{
|
|
if (plantCosts == null || plantCosts.Count == 0)
|
|
{
|
|
RestoreBeforePending();
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < plantCosts.Count; i++)
|
|
{
|
|
var cost = plantCosts[i];
|
|
if (cost == null || cost.item == null)
|
|
{
|
|
RestoreBeforePending();
|
|
pendingAction = PendingAction.None;
|
|
return;
|
|
}
|
|
|
|
int amount = Mathf.Max(1, cost.amount);
|
|
if (GetItemCount(InventorySystem.Instance, cost.item) < amount)
|
|
{
|
|
RestoreBeforePending();
|
|
pendingAction = PendingAction.None;
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < plantCosts.Count; i++)
|
|
{
|
|
var cost = plantCosts[i];
|
|
if (cost == null || cost.item == null) continue;
|
|
InventorySystem.Instance.Remove(cost.item, Mathf.Max(1, cost.amount));
|
|
}
|
|
}
|
|
}
|
|
else if (pendingAction == PendingAction.Harvest)
|
|
{
|
|
if (harvestRewards == null || harvestRewards.Count == 0)
|
|
{
|
|
RestoreBeforePending();
|
|
}
|
|
else
|
|
{
|
|
if (!CanAddAll(InventorySystem.Instance, harvestRewards))
|
|
{
|
|
RestoreBeforePending();
|
|
}
|
|
else
|
|
{
|
|
bool allAdded = true;
|
|
for (int i = 0; i < harvestRewards.Count; i++)
|
|
{
|
|
var reward = harvestRewards[i];
|
|
if (reward == null || reward.item == null)
|
|
{
|
|
allAdded = false;
|
|
break;
|
|
}
|
|
|
|
if (!InventorySystem.Instance.Add(reward.item, Mathf.Max(1, reward.amount)))
|
|
{
|
|
allAdded = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (allAdded)
|
|
{
|
|
onHarvested?.Invoke();
|
|
}
|
|
else
|
|
{
|
|
RestoreBeforePending();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pendingAction = PendingAction.None;
|
|
}
|
|
|
|
public void Interact()
|
|
{
|
|
if (pendingAction != PendingAction.None) return;
|
|
|
|
if (state == RackState.Empty)
|
|
{
|
|
BeginPending(PendingAction.Plant);
|
|
StartGrowing();
|
|
return;
|
|
}
|
|
|
|
if (state == RackState.Mature)
|
|
{
|
|
BeginPending(PendingAction.Harvest);
|
|
ResetToEmpty();
|
|
}
|
|
}
|
|
|
|
[ContextMenu("Auto Fill Growth Stages From Children")]
|
|
private void AutoFillGrowthStagesFromChildren()
|
|
{
|
|
var children = new List<Transform>();
|
|
for (int i = 0; i < transform.childCount; i++)
|
|
{
|
|
children.Add(transform.GetChild(i));
|
|
}
|
|
|
|
children.Sort((a, b) => string.CompareOrdinal(a.name, b.name));
|
|
|
|
growthStages.Clear();
|
|
for (int i = 0; i < children.Count; i++)
|
|
{
|
|
if (children[i] == null) continue;
|
|
if (children[i].name.IndexOf("_S", StringComparison.OrdinalIgnoreCase) < 0) continue;
|
|
growthStages.Add(children[i].gameObject);
|
|
}
|
|
|
|
RebuildDurationsCache();
|
|
ApplyVisuals(state == RackState.Empty ? -1 : currentStageIndex);
|
|
}
|
|
|
|
private void AutoFillGrowthStagesFromChildrenRuntime()
|
|
{
|
|
var children = new List<Transform>();
|
|
for (int i = 0; i < transform.childCount; i++)
|
|
{
|
|
children.Add(transform.GetChild(i));
|
|
}
|
|
|
|
children.Sort((a, b) => string.CompareOrdinal(a.name, b.name));
|
|
|
|
growthStages.Clear();
|
|
for (int i = 0; i < children.Count; i++)
|
|
{
|
|
if (children[i] == null) continue;
|
|
if (children[i].name.IndexOf("_S", StringComparison.OrdinalIgnoreCase) < 0) continue;
|
|
growthStages.Add(children[i].gameObject);
|
|
}
|
|
}
|
|
|
|
private void BeginPending(PendingAction action)
|
|
{
|
|
pendingAction = action;
|
|
stateBeforeAction = state;
|
|
stageBeforeAction = currentStageIndex;
|
|
plantedAtBeforeAction = plantedAtTime;
|
|
}
|
|
|
|
private void RestoreBeforePending()
|
|
{
|
|
state = stateBeforeAction;
|
|
currentStageIndex = stageBeforeAction;
|
|
plantedAtTime = plantedAtBeforeAction;
|
|
ApplyVisuals(state == RackState.Empty ? -1 : currentStageIndex);
|
|
}
|
|
|
|
private void StartGrowing()
|
|
{
|
|
RebuildDurationsCache();
|
|
plantedAtTime = Time.time;
|
|
state = RackState.Growing;
|
|
currentStageIndex = 0;
|
|
ApplyVisuals(currentStageIndex);
|
|
}
|
|
|
|
private void ResetToEmpty()
|
|
{
|
|
state = RackState.Empty;
|
|
currentStageIndex = -1;
|
|
plantedAtTime = -1f;
|
|
ApplyVisuals(-1);
|
|
}
|
|
|
|
private void ApplyVisuals(int activeStageIndex)
|
|
{
|
|
if (growthStages == null) return;
|
|
|
|
for (int i = 0; i < growthStages.Count; i++)
|
|
{
|
|
var go = growthStages[i];
|
|
if (go == null) continue;
|
|
go.SetActive(i == activeStageIndex);
|
|
}
|
|
}
|
|
|
|
private void RebuildDurationsCache()
|
|
{
|
|
int stageCount = growthStages == null ? 0 : growthStages.Count;
|
|
cumulativeStageEnds = stageCount > 0 ? new float[stageCount] : Array.Empty<float>();
|
|
|
|
if (stageCount <= 0)
|
|
{
|
|
totalDurationCached = 0f;
|
|
return;
|
|
}
|
|
|
|
bool usePerStage = stageDurationsSeconds != null && stageDurationsSeconds.Count == stageCount;
|
|
float running = 0f;
|
|
for (int i = 0; i < stageCount; i++)
|
|
{
|
|
float dur = usePerStage ? Mathf.Max(0.01f, stageDurationsSeconds[i]) : Mathf.Max(0.01f, totalGrowSeconds / stageCount);
|
|
running += dur;
|
|
cumulativeStageEnds[i] = running;
|
|
}
|
|
|
|
totalDurationCached = Mathf.Max(0.01f, running);
|
|
}
|
|
|
|
private int GetStageIndexByElapsed(float elapsed)
|
|
{
|
|
if (cumulativeStageEnds == null || cumulativeStageEnds.Length == 0) return 0;
|
|
|
|
float t = Mathf.Clamp(elapsed, 0f, totalDurationCached);
|
|
for (int i = 0; i < cumulativeStageEnds.Length; i++)
|
|
{
|
|
if (t < cumulativeStageEnds[i]) return i;
|
|
}
|
|
|
|
return Mathf.Clamp(cumulativeStageEnds.Length - 1, 0, cumulativeStageEnds.Length - 1);
|
|
}
|
|
|
|
private static int GetItemCount(InventorySystem inv, ItemData data)
|
|
{
|
|
if (inv == null || data == null || inv.inventory == null) return 0;
|
|
|
|
int count = 0;
|
|
for (int i = 0; i < inv.inventory.Count; i++)
|
|
{
|
|
var slot = inv.inventory[i];
|
|
if (slot == null) continue;
|
|
if (slot.data != data) continue;
|
|
count += Mathf.Max(0, slot.stackSize);
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private static bool CanAddAll(InventorySystem inv, List<ItemAmount> rewards)
|
|
{
|
|
if (inv == null || inv.inventory == null) return false;
|
|
if (rewards == null || rewards.Count == 0) return true;
|
|
|
|
var slots = new List<SlotSim>(inv.inventory.Count);
|
|
for (int i = 0; i < inv.inventory.Count; i++)
|
|
{
|
|
var s = inv.inventory[i];
|
|
if (s == null || s.data == null || s.stackSize <= 0)
|
|
{
|
|
slots.Add(new SlotSim(null, 0));
|
|
}
|
|
else
|
|
{
|
|
slots.Add(new SlotSim(s.data, s.stackSize));
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < rewards.Count; i++)
|
|
{
|
|
var reward = rewards[i];
|
|
if (reward == null || reward.item == null) return false;
|
|
if (!TrySimulateAdd(slots, reward.item, Mathf.Max(1, reward.amount))) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private readonly struct SlotSim
|
|
{
|
|
public readonly ItemData data;
|
|
public readonly int stackSize;
|
|
|
|
public SlotSim(ItemData data, int stackSize)
|
|
{
|
|
this.data = data;
|
|
this.stackSize = stackSize;
|
|
}
|
|
}
|
|
|
|
private static bool TrySimulateAdd(List<SlotSim> slots, ItemData data, int amount)
|
|
{
|
|
if (data == null) return false;
|
|
if (amount <= 0) return true;
|
|
if (slots == null) return false;
|
|
|
|
int remaining = amount;
|
|
|
|
if (data.isStackable)
|
|
{
|
|
int maxStack = Mathf.Max(1, data.maxStackSize);
|
|
|
|
for (int i = 0; i < slots.Count; i++)
|
|
{
|
|
var s = slots[i];
|
|
if (s.data != data) continue;
|
|
if (s.stackSize >= maxStack) continue;
|
|
|
|
int space = maxStack - s.stackSize;
|
|
int take = Mathf.Min(remaining, space);
|
|
remaining -= take;
|
|
slots[i] = new SlotSim(s.data, s.stackSize + take);
|
|
if (remaining <= 0) return true;
|
|
}
|
|
|
|
while (remaining > 0)
|
|
{
|
|
int emptyIndex = -1;
|
|
for (int i = 0; i < slots.Count; i++)
|
|
{
|
|
if (slots[i].data != null) continue;
|
|
emptyIndex = i;
|
|
break;
|
|
}
|
|
|
|
if (emptyIndex < 0) return false;
|
|
|
|
int addCount = Mathf.Min(remaining, maxStack);
|
|
slots[emptyIndex] = new SlotSim(data, addCount);
|
|
remaining -= addCount;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
while (remaining > 0)
|
|
{
|
|
int emptyIndex = -1;
|
|
for (int i = 0; i < slots.Count; i++)
|
|
{
|
|
if (slots[i].data != null) continue;
|
|
emptyIndex = i;
|
|
break;
|
|
}
|
|
|
|
if (emptyIndex < 0) return false;
|
|
|
|
slots[emptyIndex] = new SlotSim(data, 1);
|
|
remaining -= 1;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|