Initial Unity project commit
This commit is contained in:
@@ -0,0 +1,596 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user