Files
Echo/Assets/_Project/Scripts/Interaction/HydroponicsGrowRack.cs
T
2026-07-08 20:42:51 +08:00

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;
}
}
}