Merge branch origin/codex/add-economy-features-with-wallet-and-trading into main
This commit is contained in:
134
Assets/Scripts/Economy/Merchant.cs
Normal file
134
Assets/Scripts/Economy/Merchant.cs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using BlackRoad.Worldbuilder.Interaction;
|
||||||
|
using BlackRoad.Worldbuilder.Items;
|
||||||
|
|
||||||
|
namespace BlackRoad.Worldbuilder.Economy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Simple merchant that can buy and sell specific items from/to the player.
|
||||||
|
/// Currently uses a fixed price list and does a single transaction per interaction.
|
||||||
|
/// (You can expand this into a full UI later.)
|
||||||
|
/// </summary>
|
||||||
|
public class Merchant : Interactable
|
||||||
|
{
|
||||||
|
[System.Serializable]
|
||||||
|
public class TradeItem
|
||||||
|
{
|
||||||
|
public ItemDefinition item;
|
||||||
|
[Tooltip("Price in coins per unit when player BUYS.")]
|
||||||
|
public int buyPrice = 5;
|
||||||
|
[Tooltip("Price in coins per unit when player SELLS.")]
|
||||||
|
public int sellPrice = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Header("Inventory / Wallet")]
|
||||||
|
[SerializeField] private Inventory merchantInventory;
|
||||||
|
[SerializeField] private PlayerWallet merchantWallet;
|
||||||
|
[SerializeField] private TradeItem[] tradeItems;
|
||||||
|
|
||||||
|
[Header("Mode")]
|
||||||
|
[Tooltip("If true, this interaction attempts to SELL one unit of selected item to player.\nIf false, attempts to BUY one unit from player.")]
|
||||||
|
[SerializeField] private bool defaultSellToPlayer = true;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
// Set display info for Interactable UI
|
||||||
|
var so = new UnityEditor.SerializedObject(this);
|
||||||
|
so.FindProperty("displayName").stringValue = "Merchant";
|
||||||
|
so.FindProperty("verb").stringValue = "Trade";
|
||||||
|
so.ApplyModifiedPropertiesWithoutUndo();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Interact(GameObject interactor)
|
||||||
|
{
|
||||||
|
var playerInventory = interactor.GetComponent<Inventory>();
|
||||||
|
var playerWallet = interactor.GetComponent<PlayerWallet>();
|
||||||
|
|
||||||
|
if (playerInventory == null || playerWallet == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Merchant] Player missing Inventory or PlayerWallet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now we just trade the first configured item.
|
||||||
|
if (tradeItems == null || tradeItems.Length == 0)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Merchant] No trade items configured.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tradeItem = tradeItems[0];
|
||||||
|
if (tradeItem.item == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Merchant] Trade item has no ItemDefinition assigned.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultSellToPlayer)
|
||||||
|
{
|
||||||
|
SellToPlayer(tradeItem, playerInventory, playerWallet);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BuyFromPlayer(tradeItem, playerInventory, playerWallet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SellToPlayer(TradeItem trade, Inventory playerInventory, PlayerWallet playerWallet)
|
||||||
|
{
|
||||||
|
int price = trade.buyPrice;
|
||||||
|
if (price <= 0)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Merchant] Invalid buyPrice.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!playerWallet.CanAfford(price))
|
||||||
|
{
|
||||||
|
Debug.Log("[Merchant] Player cannot afford this item.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally check merchant inventory; for now assume infinite stock
|
||||||
|
int added = playerInventory.AddItem(trade.item, 1);
|
||||||
|
if (added > 0)
|
||||||
|
{
|
||||||
|
playerWallet.TrySpend(price);
|
||||||
|
if (merchantWallet != null)
|
||||||
|
merchantWallet.AddCoins(price);
|
||||||
|
|
||||||
|
Debug.Log($"[Merchant] Player bought 1x {trade.item.DisplayName} for {price} coins.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuyFromPlayer(TradeItem trade, Inventory playerInventory, PlayerWallet playerWallet)
|
||||||
|
{
|
||||||
|
int price = trade.sellPrice;
|
||||||
|
if (price <= 0)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Merchant] Invalid sellPrice.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = playerInventory.GetCount(trade.item);
|
||||||
|
if (count <= 0)
|
||||||
|
{
|
||||||
|
Debug.Log("[Merchant] Player has none of that item to sell.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int removed = playerInventory.RemoveItem(trade.item, 1);
|
||||||
|
if (removed > 0)
|
||||||
|
{
|
||||||
|
// Pay player
|
||||||
|
playerWallet.AddCoins(price);
|
||||||
|
if (merchantWallet != null && merchantWallet.CanAfford(price))
|
||||||
|
merchantWallet.TrySpend(price);
|
||||||
|
|
||||||
|
Debug.Log($"[Merchant] Player sold 1x {trade.item.DisplayName} for {price} coins.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Assets/Scripts/Economy/PlayerWallet.cs
Normal file
37
Assets/Scripts/Economy/PlayerWallet.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BlackRoad.Worldbuilder.Economy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Simple currency holder for the player.
|
||||||
|
/// </summary>
|
||||||
|
public class PlayerWallet : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private int startingCoins = 0;
|
||||||
|
|
||||||
|
public int Coins { get; private set; }
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
Coins = startingCoins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanAfford(int amount)
|
||||||
|
{
|
||||||
|
return amount >= 0 && Coins >= amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TrySpend(int amount)
|
||||||
|
{
|
||||||
|
if (!CanAfford(amount)) return false;
|
||||||
|
Coins -= amount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCoins(int amount)
|
||||||
|
{
|
||||||
|
if (amount > 0)
|
||||||
|
Coins += amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Assets/Scripts/Economy/ResourceProducer.cs
Normal file
101
Assets/Scripts/Economy/ResourceProducer.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using BlackRoad.Worldbuilder.Items;
|
||||||
|
|
||||||
|
namespace BlackRoad.Worldbuilder.Economy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Periodically produces items into an internal stockpile.
|
||||||
|
/// Merchants or players can later claim from this stockpile.
|
||||||
|
/// Example: farm producing berries, mine producing ore.
|
||||||
|
/// </summary>
|
||||||
|
public class ResourceProducer : MonoBehaviour
|
||||||
|
{
|
||||||
|
[System.Serializable]
|
||||||
|
public class Output
|
||||||
|
{
|
||||||
|
public ItemDefinition item;
|
||||||
|
[Tooltip("How many units produced per cycle.")]
|
||||||
|
public int amountPerCycle = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Header("Production")]
|
||||||
|
[SerializeField] private Output[] outputs;
|
||||||
|
[SerializeField] private float cycleSeconds = 30f;
|
||||||
|
[SerializeField] private int maxStoredPerItem = 50;
|
||||||
|
|
||||||
|
private readonly Dictionary<ItemDefinition, int> _stock = new Dictionary<ItemDefinition, int>();
|
||||||
|
private float _timer;
|
||||||
|
|
||||||
|
public int GetStock(ItemDefinition item)
|
||||||
|
{
|
||||||
|
return item != null && _stock.TryGetValue(item, out var count)
|
||||||
|
? count
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (outputs == null || outputs.Length == 0) return;
|
||||||
|
|
||||||
|
_timer += Time.deltaTime;
|
||||||
|
if (_timer >= cycleSeconds)
|
||||||
|
{
|
||||||
|
_timer -= cycleSeconds;
|
||||||
|
RunCycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunCycle()
|
||||||
|
{
|
||||||
|
foreach (var o in outputs)
|
||||||
|
{
|
||||||
|
if (o.item == null || o.amountPerCycle <= 0) continue;
|
||||||
|
|
||||||
|
if (!_stock.TryGetValue(o.item, out var count))
|
||||||
|
count = 0;
|
||||||
|
|
||||||
|
int newCount = Mathf.Min(count + o.amountPerCycle, maxStoredPerItem);
|
||||||
|
_stock[o.item] = newCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Consume up to amount from stock, returns actual removed.
|
||||||
|
/// </summary>
|
||||||
|
public int TakeFromStock(ItemDefinition item, int amount)
|
||||||
|
{
|
||||||
|
if (item == null || amount <= 0) return 0;
|
||||||
|
|
||||||
|
if (!_stock.TryGetValue(item, out var count) || count <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int taken = Mathf.Min(count, amount);
|
||||||
|
int remaining = count - taken;
|
||||||
|
if (remaining <= 0)
|
||||||
|
_stock.Remove(item);
|
||||||
|
else
|
||||||
|
_stock[item] = remaining;
|
||||||
|
|
||||||
|
return taken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional: give all current stock to a target inventory.
|
||||||
|
/// </summary>
|
||||||
|
public void TransferAllToInventory(Inventory inventory)
|
||||||
|
{
|
||||||
|
if (inventory == null) return;
|
||||||
|
|
||||||
|
var keys = new List<ItemDefinition>(_stock.Keys);
|
||||||
|
foreach (var item in keys)
|
||||||
|
{
|
||||||
|
int count = _stock[item];
|
||||||
|
if (count <= 0) continue;
|
||||||
|
|
||||||
|
int added = inventory.AddItem(item, count);
|
||||||
|
TakeFromStock(item, added);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user