From 03f63ac945932efc65e36adb518866ee15345882 Mon Sep 17 00:00:00 2001 From: Devin Date: Tue, 16 Nov 2021 19:47:57 +0100 Subject: [PATCH] Set up proper trading between inventories, plus transactions --- Assets/Scenes/NewShop.unity | 19 ++++++++ .../Shop/Components/InventoryMoneyDisplay.cs | 45 +++++++++++++++++++ .../Components/InventoryMoneyDisplay.cs.meta | 11 +++++ Assets/Scripts/Shop/Model/BuyModel.cs | 26 ++++++++++- Assets/Scripts/Shop/Model/Inventory.cs | 38 +++++++++++++++- Assets/Scripts/Shop/Model/SellModel.cs | 14 +++++- Assets/Scripts/Shop/Model/ShopModel.cs | 7 +++ .../Scripts/Shop/View/IShopModelObservable.cs | 1 + .../Scripts/Shop/View/IShopModelObserver.cs | 4 +- Assets/Scripts/Shop/View/ShopView.cs | 21 +++++++++ Assets/Scripts/Shop/View/ShopViewList.cs | 5 +++ Assets/Scripts/Shop/View/ViewItemContainer.cs | 7 ++- 12 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 Assets/Scripts/Shop/Components/InventoryMoneyDisplay.cs create mode 100644 Assets/Scripts/Shop/Components/InventoryMoneyDisplay.cs.meta diff --git a/Assets/Scenes/NewShop.unity b/Assets/Scenes/NewShop.unity index 6683239..a8b3b9d 100644 --- a/Assets/Scenes/NewShop.unity +++ b/Assets/Scenes/NewShop.unity @@ -3601,6 +3601,7 @@ MonoBehaviour: instructionText: {fileID: 360948192} layoutGroup: {fileID: 2049417198} ownModel: {fileID: 1237921498} + tradePartner: {fileID: 1237921497} --- !u!114 &336428457 MonoBehaviour: m_ObjectHideFlags: 0 @@ -8397,6 +8398,7 @@ MonoBehaviour: instructionText: {fileID: 1416129356} layoutGroup: {fileID: 1865708447} ownModel: {fileID: 132445007} + tradePartner: {fileID: 1237921497} infoPanel: {fileID: 78292608} --- !u!1 &831899927 GameObject: @@ -14665,6 +14667,7 @@ MonoBehaviour: instructionText: {fileID: 403716179} layoutGroup: {fileID: 981903572} ownModel: {fileID: 1237921498} + tradePartner: {fileID: 132445008} infoPanel: {fileID: 1800161527} --- !u!114 &1296916391 MonoBehaviour: @@ -15287,6 +15290,7 @@ MonoBehaviour: instructionText: {fileID: 1242189692} layoutGroup: {fileID: 312492670} ownModel: {fileID: 842666264} + tradePartner: {fileID: 1237921497} infoPanel: {fileID: 2013250540} --- !u!114 &1373665447 MonoBehaviour: @@ -15701,6 +15705,7 @@ MonoBehaviour: instructionText: {fileID: 1881459943} layoutGroup: {fileID: 1764603517} ownModel: {fileID: 842666264} + tradePartner: {fileID: 1237921497} --- !u!114 &1396837743 MonoBehaviour: m_ObjectHideFlags: 0 @@ -22708,6 +22713,7 @@ GameObject: - component: {fileID: 1947464187} - component: {fileID: 1947464189} - component: {fileID: 1947464188} + - component: {fileID: 1947464190} m_Layer: 5 m_Name: Number m_TagString: Untagged @@ -22855,6 +22861,18 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1947464186} m_CullTransparentMesh: 0 +--- !u!114 &1947464190 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1947464186} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1161ecc75d6ee4d44836c2efab2a0fd4, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!1 &1964503989 GameObject: m_ObjectHideFlags: 0 @@ -24649,6 +24667,7 @@ MonoBehaviour: instructionText: {fileID: 1352417142} layoutGroup: {fileID: 180905780} ownModel: {fileID: 132445007} + tradePartner: {fileID: 1237921497} --- !u!1 &2141857229 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Shop/Components/InventoryMoneyDisplay.cs b/Assets/Scripts/Shop/Components/InventoryMoneyDisplay.cs new file mode 100644 index 0000000..3a2db5f --- /dev/null +++ b/Assets/Scripts/Shop/Components/InventoryMoneyDisplay.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using TMPro; +using UnityEngine; + +/// +/// This class' only purpose is to set the class of an item view prototype. Could be the item itself, or the infobox, or anything! +/// +[RequireComponent(typeof(TextMeshProUGUI))] +public class InventoryMoneyDisplay : MonoBehaviour,IShopModelObserver +{ + private TextMeshProUGUI text; + private void Start() + { + text = GetComponent(); // Because of the meta tag, Unity will make sure this exists. No sanity checks + } + + public void SetMoney(int balance) + { + if(text == null) text = GetComponent(); // Just in case this gets called before Start() + text.text = balance.ToString(); + } + + // This is why it's probably better to split this into two interfaces, but oh well + public void OnSelected(Item item) + { + //throw new NotImplementedException(); + } + + public void OnRemoved(Item item) + { + //throw new NotImplementedException(); + } + + public void OnAdded(Item item) + { + //throw new NotImplementedException(); + } + + public void OnTransaction(int balance) + { + SetMoney(balance); + } +} diff --git a/Assets/Scripts/Shop/Components/InventoryMoneyDisplay.cs.meta b/Assets/Scripts/Shop/Components/InventoryMoneyDisplay.cs.meta new file mode 100644 index 0000000..592e482 --- /dev/null +++ b/Assets/Scripts/Shop/Components/InventoryMoneyDisplay.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1161ecc75d6ee4d44836c2efab2a0fd4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Shop/Model/BuyModel.cs b/Assets/Scripts/Shop/Model/BuyModel.cs index 69ffcf6..d46273a 100644 --- a/Assets/Scripts/Shop/Model/BuyModel.cs +++ b/Assets/Scripts/Shop/Model/BuyModel.cs @@ -1,11 +1,13 @@ using System; using System.Configuration; +using UnityEngine; /// /// This is a concrete, empty model for the buy state of the shop for you to implement /// public class BuyModel : ShopModel { + private Inventory tradePartner; public BuyModel(float pPriceModifier, int pItemCount, int pMoney) : base(pPriceModifier, pItemCount, pMoney) { @@ -27,9 +29,31 @@ public class BuyModel : ShopModel public override void ConfirmSelectedItem() { - OnRemove(GetSelectedItem()); // If there's a view subscribed, this will probably remove the item from it + if (tradePartner == null) + { + Debug.Assert(false,"Could not make trade because the shop has no trade partner"); + return; + } + + // if (tradePartner.Money < GetSelectedItem().basePrice * priceModifier) + // { + // var up = new NotEnoughMoneyException(); + // throw up; // If you find this, you can keep it! + // } + var item = GetSelectedItem(); + tradePartner.ChangeBalance((int) (-item.basePrice * priceModifier)); + OnRemove(item); // If there's a view subscribed, this will probably remove the item from it inventory.RemoveItemByIndex(selectedItemIndex); // Before removing the item from the model's actual inventory + tradePartner.AddItem(item); SelectItemByIndex(selectedItemIndex >= inventory.GetItemCount() ? --selectedItemIndex : selectedItemIndex); } + public override void SetTradePartner(Inventory tradePartner) + { + this.tradePartner = tradePartner; + } +} + +public class NotEnoughMoneyException : Exception +{ } diff --git a/Assets/Scripts/Shop/Model/Inventory.cs b/Assets/Scripts/Shop/Model/Inventory.cs index df3235d..8de24ae 100644 --- a/Assets/Scripts/Shop/Model/Inventory.cs +++ b/Assets/Scripts/Shop/Model/Inventory.cs @@ -5,10 +5,11 @@ using System.Collections.Generic; /// /// This class defines a basic inventory /// -public class Inventory +public class Inventory : IModelObservable // Borrowing the model observable here. Probably nothing will sub for items removed and such, but transactions are important! Lazy though, should split the observable { - public int Money { get; }//Getter for the money, the views need it to display the amount of money. + public int Money { get; private set; } //Getter for the money, the views need it to display the amount of money. private List itemList = new List(); //Items in the inventory + private List> observers; // Mostly here for things that want to be up to date on transactions //Set up the inventory with item count and money public Inventory(int pItemCount, int pMoney) : this(pMoney) @@ -19,6 +20,7 @@ public class Inventory public Inventory(int pMoney) { Money = pMoney; + observers = new List>(); } //------------------------------------------------------------------------------------------------------------------------ @@ -108,5 +110,37 @@ public class Inventory itemList.Add(item); } } + + public void ChangeBalance(int change) + { + if (Money + change < 0) + throw new NotEnoughMoneyException(); // We can't change the balance if it's too expensive + Money += change; + NotifyObservers(); + } //Think of other necessary functions for the inventory based on your design of the shop. Don't forget to unit test all the functions. + public IDisposable RegisterObserver(IShopModelObserver observer) + { + // Check whether observer is already registered. If not, add it + if (! observers.Contains(observer)) { + observers.Add(observer); + // Provide observer with existing data. + observer.OnTransaction(Money); + } + return new Unsubscriber(observers, observer); + } + + public void RemoveObserver(IShopModelObserver observer) + { + if (observers.Contains(observer)) + observers.Remove(observer); + } + + private void NotifyObservers() + { + foreach (var observer in observers) + { + observer.OnTransaction(Money); + } + } } diff --git a/Assets/Scripts/Shop/Model/SellModel.cs b/Assets/Scripts/Shop/Model/SellModel.cs index 99d2778..f5ca5de 100644 --- a/Assets/Scripts/Shop/Model/SellModel.cs +++ b/Assets/Scripts/Shop/Model/SellModel.cs @@ -1,11 +1,14 @@ using System; using System.Configuration; +using UnityEngine; /// -/// This is a concrete, empty model for the sell state of the shop for you to implement +/// This is a concrete, empty model for the sell state of the shop for you to implement. Technically this is the same as the buy model, as one inventory "buys" from the other. +/// Except the shop doesn't pay because we don't want it to run out of money! /// public class SellModel : ShopModel { + private Inventory tradePartner; public SellModel(float pPriceModifier, int pItemCount, int pMoney) : base(pPriceModifier, pItemCount, pMoney) { @@ -27,9 +30,16 @@ public class SellModel : ShopModel public override void ConfirmSelectedItem() { - OnRemove(GetSelectedItem()); // If there's a view subscribed, this will probably remove the item from it + var item = GetSelectedItem(); + inventory.ChangeBalance((int) (item.basePrice * priceModifier)); // We actually *add* the money to the player inventory... + OnRemove(item); // If there's a view subscribed, this will probably remove the item from it inventory.RemoveItemByIndex(selectedItemIndex); // Before removing the item from the model's actual inventory + tradePartner?.AddItem(item); // Unlike the shop, we don't actually need a trade partner here. There might be shops that don't resell! SelectItemByIndex(selectedItemIndex >= inventory.GetItemCount() ? --selectedItemIndex : selectedItemIndex); } + public override void SetTradePartner(Inventory tradePartner) + { + this.tradePartner = tradePartner; + } } diff --git a/Assets/Scripts/Shop/Model/ShopModel.cs b/Assets/Scripts/Shop/Model/ShopModel.cs index 4959b02..72607a5 100644 --- a/Assets/Scripts/Shop/Model/ShopModel.cs +++ b/Assets/Scripts/Shop/Model/ShopModel.cs @@ -93,6 +93,13 @@ public abstract class ShopModel : IModelObservable //------------------------------------------------------------------------------------------------------------------------ //Concrete classes to implement public abstract void ConfirmSelectedItem(); + + public void ChangeBalance(int change) + { + inventory.ChangeBalance(change); + } + + public abstract void SetTradePartner(Inventory tradePartner); // Observer pattern. Anything that wants to know what happens regarding item selection subscribes to this. // This could mean all items of this model and its views do it, or all views do it. diff --git a/Assets/Scripts/Shop/View/IShopModelObservable.cs b/Assets/Scripts/Shop/View/IShopModelObservable.cs index fcbeefc..9e709bb 100644 --- a/Assets/Scripts/Shop/View/IShopModelObservable.cs +++ b/Assets/Scripts/Shop/View/IShopModelObservable.cs @@ -10,6 +10,7 @@ public interface IShopModelObserver void OnSelected(T item); void OnRemoved(T item); void OnAdded(T item); + void OnTransaction(int balance); // Called when a transaction happens that changes the balance of the model, contains total new balance } // Unsubscriber, so the observer can self-unsubscribe from this observable without any coupling diff --git a/Assets/Scripts/Shop/View/IShopModelObserver.cs b/Assets/Scripts/Shop/View/IShopModelObserver.cs index d2cf93e..532f30a 100644 --- a/Assets/Scripts/Shop/View/IShopModelObserver.cs +++ b/Assets/Scripts/Shop/View/IShopModelObserver.cs @@ -4,12 +4,10 @@ using System.Collections.Generic; /// /// This interface defines an observable. Since registering those is fairly generic, we also just keep a generic one around. /// Works somewhat similar to IObservable, except that it's our own really. -/// While we use generics here, theoretically hardcoding Item types would do the job +/// While we use generics here, theoretically hardcoding Item types would do the job. Could make this 100% generic too... /// public interface IModelObservable { IDisposable RegisterObserver(IShopModelObserver observer); void RemoveObserver(IShopModelObserver observer); - void OnRemove(T val); - void OnSelect(T val); } diff --git a/Assets/Scripts/Shop/View/ShopView.cs b/Assets/Scripts/Shop/View/ShopView.cs index 4a8eb91..1ea634f 100644 --- a/Assets/Scripts/Shop/View/ShopView.cs +++ b/Assets/Scripts/Shop/View/ShopView.cs @@ -28,6 +28,7 @@ public abstract class ShopView : MonoBehaviour [SerializeField] protected LayoutGroup layoutGroup; // The layout group that represents this view visually [SerializeField] private ModelComponent ownModel; // Reference to the model this view technically belongs to + [SerializeField] private InventoryComponent tradePartner; // Reference to the model this view technically belongs to protected ShopModel model; // Model in MVC pattern protected ShopModel other; // Other model in MVC pattern (our own inventory) @@ -40,10 +41,12 @@ public abstract class ShopView : MonoBehaviour { //model = new BuyModel(2f, 16, 500); //Right now use magic values to set up the shop Debug.Assert(ownModel != null,"No shop model assigned!",this); + Debug.Assert(tradePartner != null,"No trade partner assigned!",this); model = ownModel.Model; shopController = gameObject.AddComponent().Initialize(model);//Set the default controller to be the mouse controller SetupItemIconView(); //Setup the grid view's properties InitializeButtons(); //Connect the buttons to the controller + //model.Subscribe(this); } @@ -51,12 +54,30 @@ public abstract class ShopView : MonoBehaviour { // sanity check if (model == null) model = ownModel.Model; + RegisterMoneyObserver(); // Make sure if this thing's got a money component somewhere we register it as an observer for the inventory we're looking at + model.SetTradePartner(tradePartner.Inventory); PopulateItemIconView(); //Display items } private void OnDisable() { ClearIconView(); + model.SetTradePartner(null); + UnregisterMoneyObserver(); + } + + // If view is generic, and shopview was just an implementation, this would be more flexible in theory. But for now it's enough to just manually assign "ourselves" as trade partner if we know the model works on just one inventory + private void RegisterMoneyObserver() + { + var moneyComp = GetComponentInChildren(); + if (moneyComp != null) tradePartner.Inventory.RegisterObserver(moneyComp); + } + + // Undo the above + private void UnregisterMoneyObserver() + { + var moneyComp = GetComponentInChildren(); + if (moneyComp != null) tradePartner.Inventory.RemoveObserver(moneyComp); } //------------------------------------------------------------------------------------------------------------------------ diff --git a/Assets/Scripts/Shop/View/ShopViewList.cs b/Assets/Scripts/Shop/View/ShopViewList.cs index 606a98f..b0df3e7 100644 --- a/Assets/Scripts/Shop/View/ShopViewList.cs +++ b/Assets/Scripts/Shop/View/ShopViewList.cs @@ -117,4 +117,9 @@ public class ShopViewList : ShopView, IShopModelObserver { throw new NotImplementedException(); } + + public void OnTransaction(int moneyDelta) + { + + } } diff --git a/Assets/Scripts/Shop/View/ViewItemContainer.cs b/Assets/Scripts/Shop/View/ViewItemContainer.cs index 5a48e38..1c762e7 100644 --- a/Assets/Scripts/Shop/View/ViewItemContainer.cs +++ b/Assets/Scripts/Shop/View/ViewItemContainer.cs @@ -79,7 +79,12 @@ public abstract class ViewItemContainer : MonoBehaviour, IItemContainer, IShopMo { throw new NotImplementedException(); } - + + public void OnTransaction(int cost) + { + //throw new NotImplementedException(); Irrelevant for the view containers + } + // The reason we do this in OnDestroy() is so we don't have a memory leak when this gets removed externally somehow private void OnDestroy() {