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 plantCosts = new List(); [SerializeField] private List harvestRewards = new List(); [Header("事件")] [SerializeField] private UnityEvent onHarvested; [Header("成长配置")] [Tooltip("按顺序填写各阶段子物体(例如 S1,S2,S3,S4)。脚本会在运行时只启用当前阶段。")] [SerializeField] private List growthStages = new List(); [Tooltip("若填写且长度等于阶段数,则按每个阶段持续时间驱动;否则使用 totalGrowSeconds 均分。")] [SerializeField] private List stageDurationsSeconds = new List(); [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(); 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(); if (stageDurationsSeconds == null) stageDurationsSeconds = new List(); if (plantCosts == null) plantCosts = new List(); if (harvestRewards == null) harvestRewards = new List(); 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(); 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(); 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(); 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 rewards) { if (inv == null || inv.inventory == null) return false; if (rewards == null || rewards.Count == 0) return true; var slots = new List(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 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; } } }