为 RepoForm 添加出售功能,ShopNode 只承载玩家购买组件的逻辑
This commit is contained in:
parent
dc2aa59d58
commit
2e54acbc85
|
|
@ -123,18 +123,7 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
private int ResolveRandomPrice(RarityType rarity, Random random)
|
||||
{
|
||||
for (int i = 0; i < _shopPriceRows.Count; i++)
|
||||
{
|
||||
DRShopPrice row = _shopPriceRows[i];
|
||||
if (row != null && row.Rarity == rarity)
|
||||
{
|
||||
int min = Mathf.Max(0, row.MinPrice);
|
||||
int max = Mathf.Max(min, row.MaxPrice);
|
||||
return random.Next(min, max + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return ShopPriceRuleService.ResolveRandomBuyPrice(_shopPriceRows, rarity, random);
|
||||
}
|
||||
|
||||
private static IconAreaContext BuildIconAreaContext(TowerCompItemData item)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ namespace GeometryTD.CustomComponent
|
|||
private PlayerInventoryCommandModel _commandModel;
|
||||
private PlayerInventoryTowerRosterService _towerRosterService;
|
||||
private PlayerInventoryTowerAssemblyService _towerAssemblyService;
|
||||
private PlayerInventoryTradeService _tradeService;
|
||||
|
||||
public int Gold
|
||||
{
|
||||
|
|
@ -100,6 +101,12 @@ namespace GeometryTD.CustomComponent
|
|||
return _commandModel.TryConsumeGold(costGold);
|
||||
}
|
||||
|
||||
public bool TryPurchaseComponent(TowerCompItemData item, int price)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TryPurchaseComponent(item, price);
|
||||
}
|
||||
|
||||
public void AddGold(int gainGold)
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
|
@ -138,6 +145,18 @@ namespace GeometryTD.CustomComponent
|
|||
return _towerRosterService.ReduceTowerEndurance(towerInstanceIds, enduranceLoss);
|
||||
}
|
||||
|
||||
public bool TryGetSaleCandidate(long itemId, out PlayerInventorySaleCandidate candidate)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TryGetSaleCandidate(itemId, out candidate);
|
||||
}
|
||||
|
||||
public bool TrySellItems(IReadOnlyCollection<long> itemIds, out PlayerInventorySaleResult result)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TrySellItems(itemIds, out result);
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_queryModel.IsInitialized)
|
||||
|
|
@ -155,6 +174,7 @@ namespace GeometryTD.CustomComponent
|
|||
_commandModel ??= new PlayerInventoryCommandModel(_state);
|
||||
_towerRosterService ??= new PlayerInventoryTowerRosterService(_queryModel, MaxParticipantTowerCount);
|
||||
_towerAssemblyService ??= new PlayerInventoryTowerAssemblyService(_queryModel, _commandModel);
|
||||
_tradeService ??= new PlayerInventoryTradeService(_queryModel, _commandModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,400 @@
|
|||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public enum PlayerInventorySaleFailureReason : byte
|
||||
{
|
||||
None = 0,
|
||||
InvalidSelection = 1,
|
||||
ItemNotFound = 2,
|
||||
AssembledComponent = 3,
|
||||
ParticipantTower = 4,
|
||||
MissingTowerComponent = 5
|
||||
}
|
||||
|
||||
public sealed class PlayerInventorySaleCandidate
|
||||
{
|
||||
public long ItemId;
|
||||
public bool IsSellable;
|
||||
public bool IsTower;
|
||||
public int Price;
|
||||
public PlayerInventorySaleFailureReason FailureReason;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventorySaleResult
|
||||
{
|
||||
public int GainedGold;
|
||||
public int SoldComponentCount;
|
||||
public int SoldTowerCount;
|
||||
public PlayerInventorySaleFailureReason FailureReason;
|
||||
|
||||
public bool IsSuccess => FailureReason == PlayerInventorySaleFailureReason.None;
|
||||
public int SoldItemCount => SoldComponentCount + SoldTowerCount;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventoryTradeService
|
||||
{
|
||||
private readonly PlayerInventoryQueryModel _queryModel;
|
||||
private readonly PlayerInventoryCommandModel _commandModel;
|
||||
private IDataTable<DRShopPrice> _shopPriceTable;
|
||||
|
||||
public PlayerInventoryTradeService(
|
||||
PlayerInventoryQueryModel queryModel,
|
||||
PlayerInventoryCommandModel commandModel,
|
||||
IDataTable<DRShopPrice> shopPriceTable = null)
|
||||
{
|
||||
_queryModel = queryModel;
|
||||
_commandModel = commandModel;
|
||||
_shopPriceTable = shopPriceTable;
|
||||
}
|
||||
|
||||
public bool TryPurchaseComponent(TowerCompItemData item, int price)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_commandModel.TryConsumeGold(price))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData inventoryDelta = WrapSingleItem(item);
|
||||
PlayerInventoryMergeSummary summary = _commandModel.MergeInventory(inventoryDelta);
|
||||
return summary.HasAnyGain;
|
||||
}
|
||||
|
||||
public bool TryGetSaleCandidate(long itemId, out PlayerInventorySaleCandidate candidate)
|
||||
{
|
||||
candidate = BuildSaleCandidate(itemId);
|
||||
return candidate != null;
|
||||
}
|
||||
|
||||
public bool TrySellItems(IReadOnlyCollection<long> itemIds, out PlayerInventorySaleResult result)
|
||||
{
|
||||
result = new PlayerInventorySaleResult();
|
||||
if (itemIds == null || itemIds.Count <= 0)
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.InvalidSelection;
|
||||
return false;
|
||||
}
|
||||
|
||||
HashSet<long> uniqueIds = new HashSet<long>();
|
||||
List<PlayerInventorySaleCandidate> candidates = new List<PlayerInventorySaleCandidate>(itemIds.Count);
|
||||
foreach (long itemId in itemIds)
|
||||
{
|
||||
if (itemId <= 0 || !uniqueIds.Add(itemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PlayerInventorySaleCandidate candidate = BuildSaleCandidate(itemId);
|
||||
if (candidate == null || !candidate.IsSellable)
|
||||
{
|
||||
result.FailureReason = candidate?.FailureReason ?? PlayerInventorySaleFailureReason.ItemNotFound;
|
||||
return false;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
|
||||
if (candidates.Count <= 0)
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.InvalidSelection;
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
PlayerInventorySaleCandidate candidate = candidates[i];
|
||||
if (candidate.IsTower)
|
||||
{
|
||||
if (!TryRemoveTower(inventory, candidate.ItemId))
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.MissingTowerComponent;
|
||||
return false;
|
||||
}
|
||||
|
||||
result.SoldTowerCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryRemoveComponent(inventory, candidate.ItemId))
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.ItemNotFound;
|
||||
return false;
|
||||
}
|
||||
|
||||
result.SoldComponentCount++;
|
||||
}
|
||||
|
||||
result.GainedGold += Mathf.Max(0, candidate.Price);
|
||||
}
|
||||
|
||||
_commandModel.AddGold(result.GainedGold);
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
private PlayerInventorySaleCandidate BuildSaleCandidate(long itemId)
|
||||
{
|
||||
if (itemId <= 0)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.InvalidSelection
|
||||
};
|
||||
}
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (inventory == null)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
if (_queryModel.TryGetTowerById(itemId, out TowerItemData tower) && tower != null)
|
||||
{
|
||||
if (inventory.ParticipantTowerInstanceIds != null && inventory.ParticipantTowerInstanceIds.Contains(itemId))
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
IsTower = true,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ParticipantTower
|
||||
};
|
||||
}
|
||||
|
||||
if (!ShopPriceRuleService.TryResolveTowerSalePrice(tower, inventory, out int towerPrice, EnsureShopPriceTable()))
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
IsTower = true,
|
||||
FailureReason = PlayerInventorySaleFailureReason.MissingTowerComponent
|
||||
};
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = true,
|
||||
IsTower = true,
|
||||
Price = towerPrice,
|
||||
FailureReason = PlayerInventorySaleFailureReason.None
|
||||
};
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.MuzzleComponents, itemId, out MuzzleCompItemData muzzleComp))
|
||||
{
|
||||
return BuildComponentCandidate(muzzleComp);
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.BearingComponents, itemId, out BearingCompItemData bearingComp))
|
||||
{
|
||||
return BuildComponentCandidate(bearingComp);
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.BaseComponents, itemId, out BaseCompItemData baseComp))
|
||||
{
|
||||
return BuildComponentCandidate(baseComp);
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
private PlayerInventorySaleCandidate BuildComponentCandidate(TowerCompItemData component)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
if (component.IsAssembledIntoTower)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = component.InstanceId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.AssembledComponent
|
||||
};
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = component.InstanceId,
|
||||
IsSellable = true,
|
||||
IsTower = false,
|
||||
Price = ShopPriceRuleService.ResolveComponentSalePrice(component, EnsureShopPriceTable()),
|
||||
FailureReason = PlayerInventorySaleFailureReason.None
|
||||
};
|
||||
}
|
||||
|
||||
private bool TryRemoveTower(BackpackInventoryData inventory, long towerId)
|
||||
{
|
||||
if (inventory?.Towers == null || towerId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TowerItemData targetTower = null;
|
||||
for (int i = 0; i < inventory.Towers.Count; i++)
|
||||
{
|
||||
TowerItemData tower = inventory.Towers[i];
|
||||
if (tower != null && tower.InstanceId == towerId)
|
||||
{
|
||||
targetTower = tower;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetTower == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ContainsInstanceId(inventory.MuzzleComponents, targetTower.MuzzleComponentInstanceId) ||
|
||||
!ContainsInstanceId(inventory.BearingComponents, targetTower.BearingComponentInstanceId) ||
|
||||
!ContainsInstanceId(inventory.BaseComponents, targetTower.BaseComponentInstanceId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool removedMuzzle = RemoveByInstanceId(inventory.MuzzleComponents, targetTower.MuzzleComponentInstanceId);
|
||||
bool removedBearing = RemoveByInstanceId(inventory.BearingComponents, targetTower.BearingComponentInstanceId);
|
||||
bool removedBase = RemoveByInstanceId(inventory.BaseComponents, targetTower.BaseComponentInstanceId);
|
||||
if (!removedMuzzle || !removedBearing || !removedBase)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
inventory.Towers.Remove(targetTower);
|
||||
inventory.ParticipantTowerInstanceIds?.Remove(towerId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryRemoveComponent(BackpackInventoryData inventory, long itemId)
|
||||
{
|
||||
return RemoveByInstanceId(inventory?.MuzzleComponents, itemId) ||
|
||||
RemoveByInstanceId(inventory?.BearingComponents, itemId) ||
|
||||
RemoveByInstanceId(inventory?.BaseComponents, itemId);
|
||||
}
|
||||
|
||||
private static bool RemoveByInstanceId<TItem>(List<TItem> items, long instanceId)
|
||||
where TItem : class
|
||||
{
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
switch (items[i])
|
||||
{
|
||||
case TowerCompItemData component when component.InstanceId == instanceId:
|
||||
items.RemoveAt(i);
|
||||
return true;
|
||||
case TowerItemData tower when tower.InstanceId == instanceId:
|
||||
items.RemoveAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ContainsInstanceId<TItem>(IReadOnlyList<TItem> items, long instanceId)
|
||||
where TItem : class
|
||||
{
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
switch (items[i])
|
||||
{
|
||||
case TowerCompItemData component when component.InstanceId == instanceId:
|
||||
return true;
|
||||
case TowerItemData tower when tower.InstanceId == instanceId:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetComponentById<TComp>(IReadOnlyList<TComp> items, long instanceId, out TComp result)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
result = null;
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
TComp item = items[i];
|
||||
if (item != null && item.InstanceId == instanceId)
|
||||
{
|
||||
result = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static BackpackInventoryData WrapSingleItem(TowerCompItemData item)
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData();
|
||||
switch (item)
|
||||
{
|
||||
case MuzzleCompItemData muzzleComp:
|
||||
inventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp));
|
||||
break;
|
||||
case BearingCompItemData bearingComp:
|
||||
inventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
|
||||
break;
|
||||
case BaseCompItemData baseComp:
|
||||
inventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
|
||||
break;
|
||||
}
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private IDataTable<DRShopPrice> EnsureShopPriceTable()
|
||||
{
|
||||
_shopPriceTable ??= GameEntry.DataTable.GetDataTable<DRShopPrice>();
|
||||
return _shopPriceTable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 298cf9214556453cb5e7ec7c0605c350
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
|
||||
namespace GeometryTD.CustomEvent
|
||||
{
|
||||
public sealed class RepoSellCancelRequestedEventArgs : GameEventArgs
|
||||
{
|
||||
public static int EventId => typeof(RepoSellCancelRequestedEventArgs).GetHashCode();
|
||||
|
||||
public override int Id => EventId;
|
||||
|
||||
public static RepoSellCancelRequestedEventArgs Create()
|
||||
{
|
||||
return ReferencePool.Acquire<RepoSellCancelRequestedEventArgs>();
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2cc34a22ea6347619b065c5874e0b715
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
|
||||
namespace GeometryTD.CustomEvent
|
||||
{
|
||||
public sealed class RepoSellConfirmRequestedEventArgs : GameEventArgs
|
||||
{
|
||||
public static int EventId => typeof(RepoSellConfirmRequestedEventArgs).GetHashCode();
|
||||
|
||||
public override int Id => EventId;
|
||||
|
||||
public static RepoSellConfirmRequestedEventArgs Create()
|
||||
{
|
||||
return ReferencePool.Acquire<RepoSellConfirmRequestedEventArgs>();
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 598b87b9cb8e4783bc73db5e255d31f6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
|
||||
namespace GeometryTD.CustomEvent
|
||||
{
|
||||
public sealed class RepoSellModeToggleRequestedEventArgs : GameEventArgs
|
||||
{
|
||||
public static int EventId => typeof(RepoSellModeToggleRequestedEventArgs).GetHashCode();
|
||||
|
||||
public override int Id => EventId;
|
||||
|
||||
public static RepoSellModeToggleRequestedEventArgs Create()
|
||||
{
|
||||
return ReferencePool.Acquire<RepoSellModeToggleRequestedEventArgs>();
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 44692273ce6a417a875dea9675663d82
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
using GeometryTD.UI;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
{
|
||||
public class RepoFormContext : UIContext
|
||||
{
|
||||
public string GoldText;
|
||||
public RepoFormState State;
|
||||
public bool ShowSellModeButton;
|
||||
public string SellModeButtonText;
|
||||
public bool ShowCombineArea;
|
||||
public bool ShowSellArea;
|
||||
public CombineAreaContext CombineAreaContext;
|
||||
public SellAreaContext SellAreaContext;
|
||||
public CompAreaContext CompAreaContext;
|
||||
public ParticipantAreaContext ParticipantAreaContext;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
namespace GeometryTD.UI
|
||||
{
|
||||
public enum RepoFormState
|
||||
{
|
||||
Assemble = 0,
|
||||
Sell = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9110fc764f3044bda40df4a8c02f320b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -11,6 +11,9 @@ namespace GeometryTD.UI
|
|||
public float EnduranceRate01;
|
||||
public RepoItemClickActionType ClickActionType;
|
||||
public TowerCompSlotType ComponentSlotType;
|
||||
public bool IsSellMode;
|
||||
public bool IsSellable = true;
|
||||
public bool IsSellSelected;
|
||||
public IconAreaContext IconAreaContext;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
namespace GeometryTD.UI
|
||||
{
|
||||
public class SellAreaContext
|
||||
{
|
||||
public string TotalPriceText;
|
||||
public bool CanConfirmSell;
|
||||
public RepoItemContext[] ComponentItems;
|
||||
public TowerRepoItemContext[] TowerItems;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f153d1a509df4bb69c6d231df5b60db9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -23,21 +23,44 @@ namespace GeometryTD.UI
|
|||
Dictionary<long, BearingCompItemData> bearingMap = BuildComponentMap(rawData.Inventory.BearingComponents);
|
||||
Dictionary<long, BaseCompItemData> baseMap = BuildComponentMap(rawData.Inventory.BaseComponents);
|
||||
Dictionary<long, TowerItemData> towerMap = BuildTowerMap(rawData.Inventory.Towers);
|
||||
HashSet<long> selectedSellItemIds = BuildSelectedSellItemSet(rawData.SelectedSellItemIds);
|
||||
bool isSellState = rawData.State == RepoFormState.Sell;
|
||||
|
||||
List<RepoItemContext> componentItems = new List<RepoItemContext>();
|
||||
List<TowerRepoItemContext> towerItems = new List<TowerRepoItemContext>();
|
||||
List<RepoItemContext> selectedSellComponentItems = new List<RepoItemContext>();
|
||||
List<TowerRepoItemContext> selectedSellTowerItems = new List<TowerRepoItemContext>();
|
||||
|
||||
if (rawData.Inventory.Towers != null)
|
||||
{
|
||||
foreach (var tower in rawData.Inventory.Towers)
|
||||
foreach (TowerItemData tower in rawData.Inventory.Towers)
|
||||
{
|
||||
if (tower == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TowerRepoItemContext towerContext = BuildTowerRepoItemContext(tower, muzzleMap, bearingMap, baseMap,
|
||||
RepoItemClickActionType.OpenDetail, true);
|
||||
bool isParticipantTower = rawData.Inventory.ParticipantTowerInstanceIds != null &&
|
||||
rawData.Inventory.ParticipantTowerInstanceIds.Contains(tower.InstanceId);
|
||||
bool isSellSelected = isSellState && selectedSellItemIds.Contains(tower.InstanceId);
|
||||
|
||||
TowerRepoItemContext towerContext = BuildTowerRepoItemContext(
|
||||
tower,
|
||||
muzzleMap,
|
||||
bearingMap,
|
||||
baseMap,
|
||||
RepoItemClickActionType.OpenDetail,
|
||||
!isSellState,
|
||||
isSellState,
|
||||
!isParticipantTower,
|
||||
isSellSelected,
|
||||
false);
|
||||
AddTowerItemContext(towerItems, towerContext);
|
||||
if (isSellSelected)
|
||||
{
|
||||
selectedSellTowerItems.Add(towerContext);
|
||||
}
|
||||
|
||||
AddItemDescSeed(
|
||||
tower.InstanceId,
|
||||
tower.Name,
|
||||
|
|
@ -50,23 +73,24 @@ namespace GeometryTD.UI
|
|||
|
||||
if (rawData.Inventory.MuzzleComponents != null)
|
||||
{
|
||||
foreach (var item in rawData.Inventory.MuzzleComponents)
|
||||
foreach (MuzzleCompItemData item in rawData.Inventory.MuzzleComponents)
|
||||
{
|
||||
if (item == null || item.IsAssembledIntoTower)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RepoItemContext componentContext = new RepoItemContext
|
||||
{
|
||||
InstanceId = item.InstanceId,
|
||||
CanDrag = true,
|
||||
EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item),
|
||||
ClickActionType = RepoItemClickActionType.OpenDetail,
|
||||
ComponentSlotType = TowerCompSlotType.Muzzle,
|
||||
IconAreaContext = BuildIconAreaContext(item)
|
||||
};
|
||||
RepoItemContext componentContext = BuildSellableComponentContext(
|
||||
item,
|
||||
TowerCompSlotType.Muzzle,
|
||||
isSellState,
|
||||
selectedSellItemIds.Contains(item.InstanceId));
|
||||
AddComponentItemContext(componentItems, componentContext);
|
||||
if (componentContext.IsSellSelected)
|
||||
{
|
||||
selectedSellComponentItems.Add(componentContext);
|
||||
}
|
||||
|
||||
AddItemDescSeed(
|
||||
item.InstanceId,
|
||||
item.Name,
|
||||
|
|
@ -86,16 +110,17 @@ namespace GeometryTD.UI
|
|||
continue;
|
||||
}
|
||||
|
||||
RepoItemContext componentContext = new RepoItemContext
|
||||
{
|
||||
InstanceId = item.InstanceId,
|
||||
CanDrag = true,
|
||||
EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item),
|
||||
ClickActionType = RepoItemClickActionType.OpenDetail,
|
||||
ComponentSlotType = TowerCompSlotType.Bearing,
|
||||
IconAreaContext = BuildIconAreaContext(item)
|
||||
};
|
||||
RepoItemContext componentContext = BuildSellableComponentContext(
|
||||
item,
|
||||
TowerCompSlotType.Bearing,
|
||||
isSellState,
|
||||
selectedSellItemIds.Contains(item.InstanceId));
|
||||
AddComponentItemContext(componentItems, componentContext);
|
||||
if (componentContext.IsSellSelected)
|
||||
{
|
||||
selectedSellComponentItems.Add(componentContext);
|
||||
}
|
||||
|
||||
AddItemDescSeed(
|
||||
item.InstanceId,
|
||||
item.Name,
|
||||
|
|
@ -115,16 +140,17 @@ namespace GeometryTD.UI
|
|||
continue;
|
||||
}
|
||||
|
||||
RepoItemContext componentContext = new RepoItemContext
|
||||
{
|
||||
InstanceId = item.InstanceId,
|
||||
CanDrag = true,
|
||||
EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item),
|
||||
ClickActionType = RepoItemClickActionType.OpenDetail,
|
||||
ComponentSlotType = TowerCompSlotType.Base,
|
||||
IconAreaContext = BuildIconAreaContext(item)
|
||||
};
|
||||
RepoItemContext componentContext = BuildSellableComponentContext(
|
||||
item,
|
||||
TowerCompSlotType.Base,
|
||||
isSellState,
|
||||
selectedSellItemIds.Contains(item.InstanceId));
|
||||
AddComponentItemContext(componentItems, componentContext);
|
||||
if (componentContext.IsSellSelected)
|
||||
{
|
||||
selectedSellComponentItems.Add(componentContext);
|
||||
}
|
||||
|
||||
AddItemDescSeed(
|
||||
item.InstanceId,
|
||||
item.Name,
|
||||
|
|
@ -141,7 +167,19 @@ namespace GeometryTD.UI
|
|||
return new RepoFormContext
|
||||
{
|
||||
GoldText = $"金币: {rawData.Inventory.Gold}",
|
||||
State = rawData.State,
|
||||
ShowSellModeButton = rawData.State == RepoFormState.Assemble,
|
||||
SellModeButtonText = "出售模式",
|
||||
ShowCombineArea = rawData.State == RepoFormState.Assemble,
|
||||
ShowSellArea = rawData.State == RepoFormState.Sell,
|
||||
CombineAreaContext = new CombineAreaContext(),
|
||||
SellAreaContext = new SellAreaContext
|
||||
{
|
||||
TotalPriceText = $"总价值: {rawData.SelectedSellTotalPrice}",
|
||||
CanConfirmSell = rawData.State == RepoFormState.Sell && rawData.SelectedSellItemCount > 0,
|
||||
ComponentItems = selectedSellComponentItems.ToArray(),
|
||||
TowerItems = selectedSellTowerItems.ToArray()
|
||||
},
|
||||
CompAreaContext = new CompAreaContext
|
||||
{
|
||||
ComponentItems = componentItems.ToArray(),
|
||||
|
|
@ -151,6 +189,26 @@ namespace GeometryTD.UI
|
|||
};
|
||||
}
|
||||
|
||||
private static RepoItemContext BuildSellableComponentContext(
|
||||
TowerCompItemData item,
|
||||
TowerCompSlotType slotType,
|
||||
bool isSellState,
|
||||
bool isSellSelected)
|
||||
{
|
||||
return new RepoItemContext
|
||||
{
|
||||
InstanceId = item.InstanceId,
|
||||
CanDrag = !isSellState,
|
||||
EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item),
|
||||
ClickActionType = RepoItemClickActionType.OpenDetail,
|
||||
ComponentSlotType = slotType,
|
||||
IsSellMode = isSellState,
|
||||
IsSellable = true,
|
||||
IsSellSelected = isSellState && isSellSelected,
|
||||
IconAreaContext = BuildIconAreaContext(item)
|
||||
};
|
||||
}
|
||||
|
||||
private void AddComponentItemContext(List<RepoItemContext> items, RepoItemContext itemContext)
|
||||
{
|
||||
if (itemContext == null)
|
||||
|
|
@ -218,7 +276,7 @@ namespace GeometryTD.UI
|
|||
return map;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
foreach (TComp item in items)
|
||||
{
|
||||
if (item == null || item.InstanceId <= 0)
|
||||
{
|
||||
|
|
@ -231,6 +289,26 @@ namespace GeometryTD.UI
|
|||
return map;
|
||||
}
|
||||
|
||||
private static HashSet<long> BuildSelectedSellItemSet(IReadOnlyList<long> selectedSellItemIds)
|
||||
{
|
||||
HashSet<long> selectedIds = new HashSet<long>();
|
||||
if (selectedSellItemIds == null)
|
||||
{
|
||||
return selectedIds;
|
||||
}
|
||||
|
||||
for (int i = 0; i < selectedSellItemIds.Count; i++)
|
||||
{
|
||||
long itemId = selectedSellItemIds[i];
|
||||
if (itemId > 0)
|
||||
{
|
||||
selectedIds.Add(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
return selectedIds;
|
||||
}
|
||||
|
||||
private static Dictionary<long, TowerItemData> BuildTowerMap(IReadOnlyList<TowerItemData> towers)
|
||||
{
|
||||
Dictionary<long, TowerItemData> map = new Dictionary<long, TowerItemData>();
|
||||
|
|
@ -239,7 +317,7 @@ namespace GeometryTD.UI
|
|||
return map;
|
||||
}
|
||||
|
||||
foreach (var tower in towers)
|
||||
foreach (TowerItemData tower in towers)
|
||||
{
|
||||
if (tower == null || tower.InstanceId <= 0)
|
||||
{
|
||||
|
|
@ -263,7 +341,7 @@ namespace GeometryTD.UI
|
|||
List<TowerRepoItemContext> participantItems = new List<TowerRepoItemContext>();
|
||||
if (inventory?.ParticipantTowerInstanceIds != null && towerMap != null)
|
||||
{
|
||||
foreach (var towerId in inventory.ParticipantTowerInstanceIds)
|
||||
foreach (long towerId in inventory.ParticipantTowerInstanceIds)
|
||||
{
|
||||
if (towerId <= 0)
|
||||
{
|
||||
|
|
@ -280,8 +358,17 @@ namespace GeometryTD.UI
|
|||
continue;
|
||||
}
|
||||
|
||||
TowerRepoItemContext towerContext = BuildTowerRepoItemContext(tower, muzzleMap, bearingMap, baseMap,
|
||||
RepoItemClickActionType.RemoveParticipant, false);
|
||||
TowerRepoItemContext towerContext = BuildTowerRepoItemContext(
|
||||
tower,
|
||||
muzzleMap,
|
||||
bearingMap,
|
||||
baseMap,
|
||||
RepoItemClickActionType.RemoveParticipant,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false);
|
||||
if (towerContext != null)
|
||||
{
|
||||
participantItems.Add(towerContext);
|
||||
|
|
@ -304,6 +391,11 @@ namespace GeometryTD.UI
|
|||
return;
|
||||
}
|
||||
|
||||
if (Context != null && Context.State == RepoFormState.Sell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (long towerId in _compAreaTowerIds)
|
||||
{
|
||||
Form.SetRepoItemSelected(towerId, _participantTowerIds.Contains(towerId));
|
||||
|
|
@ -334,7 +426,11 @@ namespace GeometryTD.UI
|
|||
IReadOnlyDictionary<long, BearingCompItemData> bearingMap,
|
||||
IReadOnlyDictionary<long, BaseCompItemData> baseMap,
|
||||
RepoItemClickActionType clickActionType,
|
||||
bool canDrag)
|
||||
bool canDrag,
|
||||
bool isSellMode,
|
||||
bool isSellable,
|
||||
bool isSellSelected,
|
||||
bool highlightSelected)
|
||||
{
|
||||
if (tower == null)
|
||||
{
|
||||
|
|
@ -348,6 +444,9 @@ namespace GeometryTD.UI
|
|||
EnduranceRate01 = ItemDescUtility.ResolveTowerEnduranceRate(tower, muzzleMap, bearingMap, baseMap),
|
||||
ClickActionType = clickActionType,
|
||||
ComponentSlotType = TowerCompSlotType.None,
|
||||
IsSellMode = isSellMode,
|
||||
IsSellable = isSellable,
|
||||
IsSellSelected = isSellSelected || highlightSelected,
|
||||
IconAreaContext = new TowerIconAreaContext
|
||||
{
|
||||
Rarity = tower.Rarity,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using GeometryTD.CustomEvent;
|
|||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using GameFramework.Event;
|
||||
using GeometryTD.CustomComponent;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
|
|
@ -43,6 +44,9 @@ namespace GeometryTD.UI
|
|||
GameEntry.Event.Subscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested);
|
||||
GameEntry.Event.Subscribe(RepoParticipantAssignRequestedEventArgs.EventId, OnRepoParticipantAssignRequested);
|
||||
GameEntry.Event.Subscribe(RepoFormReturnEventArgs.EventId, OnRepoFormReturn);
|
||||
GameEntry.Event.Subscribe(RepoSellModeToggleRequestedEventArgs.EventId, OnRepoSellModeToggleRequested);
|
||||
GameEntry.Event.Subscribe(RepoSellCancelRequestedEventArgs.EventId, OnRepoSellCancelRequested);
|
||||
GameEntry.Event.Subscribe(RepoSellConfirmRequestedEventArgs.EventId, OnRepoSellConfirmRequested);
|
||||
}
|
||||
|
||||
protected override void UnsubscribeCustomEvents()
|
||||
|
|
@ -53,6 +57,9 @@ namespace GeometryTD.UI
|
|||
GameEntry.Event.Unsubscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested);
|
||||
GameEntry.Event.Unsubscribe(RepoParticipantAssignRequestedEventArgs.EventId, OnRepoParticipantAssignRequested);
|
||||
GameEntry.Event.Unsubscribe(RepoFormReturnEventArgs.EventId, OnRepoFormReturn);
|
||||
GameEntry.Event.Unsubscribe(RepoSellModeToggleRequestedEventArgs.EventId, OnRepoSellModeToggleRequested);
|
||||
GameEntry.Event.Unsubscribe(RepoSellCancelRequestedEventArgs.EventId, OnRepoSellCancelRequested);
|
||||
GameEntry.Event.Unsubscribe(RepoSellConfirmRequestedEventArgs.EventId, OnRepoSellConfirmRequested);
|
||||
}
|
||||
|
||||
public override int? OpenUI(object userData = null)
|
||||
|
|
@ -125,6 +132,24 @@ namespace GeometryTD.UI
|
|||
return;
|
||||
}
|
||||
|
||||
if (_useCase != null && _useCase.State == RepoFormState.Sell)
|
||||
{
|
||||
if (_useCase.TryToggleSellSelection(
|
||||
args.ItemId,
|
||||
out RepoFormRawData sellModeRawData,
|
||||
out PlayerInventorySaleFailureReason failureReason))
|
||||
{
|
||||
SetContext(BuildContext(sellModeRawData));
|
||||
RefreshCurrentUI();
|
||||
}
|
||||
else if (failureReason == PlayerInventorySaleFailureReason.ParticipantTower)
|
||||
{
|
||||
OpenSellBlockedDialog("参战防御塔不能直接出售,请先移出参战区。");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (clickActionType == RepoItemClickActionType.RemoveParticipant)
|
||||
{
|
||||
if (_useCase == null || Form == null)
|
||||
|
|
@ -165,6 +190,11 @@ namespace GeometryTD.UI
|
|||
return;
|
||||
}
|
||||
|
||||
if (Context != null && Context.State == RepoFormState.Sell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e is RepoItemDragEndedEventArgs args))
|
||||
{
|
||||
return;
|
||||
|
|
@ -188,6 +218,11 @@ namespace GeometryTD.UI
|
|||
return;
|
||||
}
|
||||
|
||||
if (Context != null && Context.State == RepoFormState.Sell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e is CombineSlotClickedEventArgs args))
|
||||
{
|
||||
return;
|
||||
|
|
@ -257,6 +292,11 @@ namespace GeometryTD.UI
|
|||
return;
|
||||
}
|
||||
|
||||
if (Context != null && Context.State == RepoFormState.Sell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e is RepoParticipantAssignRequestedEventArgs args))
|
||||
{
|
||||
return;
|
||||
|
|
@ -288,6 +328,65 @@ namespace GeometryTD.UI
|
|||
RefreshParticipantAreaOnly();
|
||||
}
|
||||
|
||||
private void OnRepoSellModeToggleRequested(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!IsEventFromCurrentForm(sender) || !(e is RepoSellModeToggleRequestedEventArgs) || _useCase == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_useCase.State != RepoFormState.Assemble)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RepoFormRawData rawData = _useCase.EnterSellState();
|
||||
SetContext(BuildContext(rawData));
|
||||
RefreshCurrentUI();
|
||||
}
|
||||
|
||||
private void OnRepoSellCancelRequested(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!IsEventFromCurrentForm(sender) || !(e is RepoSellCancelRequestedEventArgs) || _useCase == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_useCase.State != RepoFormState.Sell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetContext(BuildContext(_useCase.ExitSellState()));
|
||||
RefreshCurrentUI();
|
||||
}
|
||||
|
||||
private void OnRepoSellConfirmRequested(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!IsEventFromCurrentForm(sender) || !(e is RepoSellConfirmRequestedEventArgs) || _useCase == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_useCase.TryConfirmSellSelection(out RepoFormRawData rawData, out PlayerInventorySaleResult result))
|
||||
{
|
||||
if (result != null && result.FailureReason == PlayerInventorySaleFailureReason.ParticipantTower)
|
||||
{
|
||||
OpenSellBlockedDialog("参战防御塔不能直接出售,请先移出参战区。");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SetContext(BuildContext(rawData));
|
||||
RefreshCurrentUI();
|
||||
|
||||
if (GameEntry.UI.GetUIForm(UIFormType.ShopForm) != null)
|
||||
{
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.ShopForm);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsEventFromCurrentForm(object sender)
|
||||
{
|
||||
if (Form == null)
|
||||
|
|
@ -309,6 +408,18 @@ namespace GeometryTD.UI
|
|||
return false;
|
||||
}
|
||||
|
||||
private static void OpenSellBlockedDialog(string message)
|
||||
{
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData
|
||||
{
|
||||
Mode = 1,
|
||||
Title = "无法出售",
|
||||
Message = message,
|
||||
PauseGame = false,
|
||||
ConfirmText = "知道了"
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,9 @@ namespace GeometryTD.UI
|
|||
public class RepoFormRawData
|
||||
{
|
||||
public BackpackInventoryData Inventory;
|
||||
public RepoFormState State;
|
||||
public long[] SelectedSellItemIds;
|
||||
public int SelectedSellItemCount;
|
||||
public int SelectedSellTotalPrice;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using GeometryTD.CustomUtility;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomComponent;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
|
|
@ -7,6 +9,8 @@ namespace GeometryTD.UI
|
|||
{
|
||||
private const int MaxParticipantCount = 4;
|
||||
private BackpackInventoryData _fallbackInventory;
|
||||
private readonly HashSet<long> _selectedSellItemIds = new HashSet<long>();
|
||||
private RepoFormState _state = RepoFormState.Assemble;
|
||||
|
||||
public RepoFormRawData CreateInitialModel()
|
||||
{
|
||||
|
|
@ -15,12 +19,23 @@ namespace GeometryTD.UI
|
|||
: GetOrCreateFallbackInventory();
|
||||
return new RepoFormRawData
|
||||
{
|
||||
Inventory = sample
|
||||
Inventory = sample,
|
||||
State = _state,
|
||||
SelectedSellItemIds = BuildSelectedSellItemArray(),
|
||||
SelectedSellItemCount = _selectedSellItemIds.Count,
|
||||
SelectedSellTotalPrice = ResolveSelectedSellTotalPrice()
|
||||
};
|
||||
}
|
||||
|
||||
public RepoFormState State => _state;
|
||||
|
||||
public bool TryAssembleTower(long muzzleItemId, long bearingItemId, long baseItemId)
|
||||
{
|
||||
if (_state == RepoFormState.Sell)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GameEntry.PlayerInventory == null)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -35,6 +50,15 @@ namespace GeometryTD.UI
|
|||
|
||||
public ParticipantTowerAssignResult TryAddParticipantTower(long towerItemId)
|
||||
{
|
||||
if (_state == RepoFormState.Sell)
|
||||
{
|
||||
return new ParticipantTowerAssignResult
|
||||
{
|
||||
TowerInstanceId = towerItemId,
|
||||
FailureReason = ParticipantTowerAssignFailureReason.ParticipantAreaFull
|
||||
};
|
||||
}
|
||||
|
||||
if (GameEntry.PlayerInventory == null)
|
||||
{
|
||||
BackpackInventoryData fallbackInventory = GetOrCreateFallbackInventory();
|
||||
|
|
@ -44,11 +68,16 @@ namespace GeometryTD.UI
|
|||
MaxParticipantCount);
|
||||
}
|
||||
|
||||
return GameEntry.PlayerInventory.TryAddParticipantTower(towerItemId, 4);
|
||||
return GameEntry.PlayerInventory.TryAddParticipantTower(towerItemId, MaxParticipantCount);
|
||||
}
|
||||
|
||||
public bool TryRemoveParticipantTower(long towerItemId)
|
||||
{
|
||||
if (_state == RepoFormState.Sell)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GameEntry.PlayerInventory == null)
|
||||
{
|
||||
BackpackInventoryData fallbackInventory = GetOrCreateFallbackInventory();
|
||||
|
|
@ -61,12 +90,120 @@ namespace GeometryTD.UI
|
|||
return GameEntry.PlayerInventory.TryRemoveParticipantTower(towerItemId);
|
||||
}
|
||||
|
||||
public RepoFormRawData EnterSellState()
|
||||
{
|
||||
_state = RepoFormState.Sell;
|
||||
_selectedSellItemIds.Clear();
|
||||
return CreateInitialModel();
|
||||
}
|
||||
|
||||
public RepoFormRawData ExitSellState()
|
||||
{
|
||||
_state = RepoFormState.Assemble;
|
||||
_selectedSellItemIds.Clear();
|
||||
return CreateInitialModel();
|
||||
}
|
||||
|
||||
public bool TryToggleSellSelection(
|
||||
long itemId,
|
||||
out RepoFormRawData rawData,
|
||||
out PlayerInventorySaleFailureReason failureReason)
|
||||
{
|
||||
rawData = null;
|
||||
failureReason = PlayerInventorySaleFailureReason.None;
|
||||
if (_state != RepoFormState.Sell || itemId <= 0 || GameEntry.PlayerInventory == null)
|
||||
{
|
||||
failureReason = PlayerInventorySaleFailureReason.InvalidSelection;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_selectedSellItemIds.Contains(itemId))
|
||||
{
|
||||
_selectedSellItemIds.Remove(itemId);
|
||||
rawData = CreateInitialModel();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!GameEntry.PlayerInventory.TryGetSaleCandidate(itemId, out PlayerInventorySaleCandidate candidate) ||
|
||||
candidate == null)
|
||||
{
|
||||
failureReason = PlayerInventorySaleFailureReason.ItemNotFound;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!candidate.IsSellable)
|
||||
{
|
||||
failureReason = candidate.FailureReason;
|
||||
return false;
|
||||
}
|
||||
|
||||
_selectedSellItemIds.Add(itemId);
|
||||
rawData = CreateInitialModel();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryConfirmSellSelection(out RepoFormRawData rawData, out PlayerInventorySaleResult result)
|
||||
{
|
||||
rawData = null;
|
||||
result = null;
|
||||
if (_state != RepoFormState.Sell || _selectedSellItemIds.Count <= 0 || GameEntry.PlayerInventory == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!GameEntry.PlayerInventory.TrySellItems(_selectedSellItemIds, out PlayerInventorySaleResult saleResult) ||
|
||||
saleResult == null ||
|
||||
!saleResult.IsSuccess)
|
||||
{
|
||||
result = saleResult;
|
||||
rawData = CreateInitialModel();
|
||||
return false;
|
||||
}
|
||||
|
||||
_selectedSellItemIds.Clear();
|
||||
result = saleResult;
|
||||
rawData = CreateInitialModel();
|
||||
return true;
|
||||
}
|
||||
|
||||
private BackpackInventoryData GetOrCreateFallbackInventory()
|
||||
{
|
||||
_fallbackInventory ??= InventorySeedUtility.CreateSampleInventory();
|
||||
InventoryParticipantUtility.NormalizeParticipantState(_fallbackInventory, MaxParticipantCount);
|
||||
return _fallbackInventory;
|
||||
}
|
||||
|
||||
private long[] BuildSelectedSellItemArray()
|
||||
{
|
||||
if (_selectedSellItemIds.Count <= 0)
|
||||
{
|
||||
return System.Array.Empty<long>();
|
||||
}
|
||||
|
||||
long[] itemIds = new long[_selectedSellItemIds.Count];
|
||||
_selectedSellItemIds.CopyTo(itemIds);
|
||||
return itemIds;
|
||||
}
|
||||
|
||||
private int ResolveSelectedSellTotalPrice()
|
||||
{
|
||||
if (_state != RepoFormState.Sell || _selectedSellItemIds.Count <= 0 || GameEntry.PlayerInventory == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
foreach (long itemId in _selectedSellItemIds)
|
||||
{
|
||||
if (GameEntry.PlayerInventory.TryGetSaleCandidate(itemId, out PlayerInventorySaleCandidate candidate) &&
|
||||
candidate != null &&
|
||||
candidate.IsSellable)
|
||||
{
|
||||
total += candidate.Price;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ namespace GeometryTD.UI
|
|||
public class CombineArea : MonoBehaviour, IDropHandler
|
||||
{
|
||||
[SerializeField] private CombineSlotItem[] _slots;
|
||||
private bool _isInteractionEnabled = true;
|
||||
|
||||
public void OnInit(CombineAreaContext context)
|
||||
{
|
||||
|
|
@ -43,6 +44,11 @@ namespace GeometryTD.UI
|
|||
|
||||
public void OnCombineClick()
|
||||
{
|
||||
if (!_isInteractionEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGetBoundItemId(TowerCompSlotType.Muzzle, out long muzzleItemId) ||
|
||||
!TryGetBoundItemId(TowerCompSlotType.Bearing, out long bearingItemId) ||
|
||||
!TryGetBoundItemId(TowerCompSlotType.Base, out long baseItemId))
|
||||
|
|
@ -55,6 +61,11 @@ namespace GeometryTD.UI
|
|||
|
||||
public bool TryAssignItem(RepoItemContext itemContext)
|
||||
{
|
||||
if (!_isInteractionEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (itemContext == null || itemContext.ComponentSlotType == TowerCompSlotType.None)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -91,6 +102,11 @@ namespace GeometryTD.UI
|
|||
|
||||
public void OnDrop(PointerEventData eventData)
|
||||
{
|
||||
if (!_isInteractionEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventData == null)
|
||||
{
|
||||
return;
|
||||
|
|
@ -113,6 +129,11 @@ namespace GeometryTD.UI
|
|||
dragItem.SetDropResult(assigned);
|
||||
}
|
||||
|
||||
public void SetInteractionEnabled(bool enabled)
|
||||
{
|
||||
_isInteractionEnabled = enabled;
|
||||
}
|
||||
|
||||
private CombineSlotItem FindSlot(TowerCompSlotType slotType)
|
||||
{
|
||||
if (_slots == null)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ namespace GeometryTD.UI
|
|||
[SerializeField] private Transform _content;
|
||||
[SerializeField] private TowerRepoItem _towerItemTemplate;
|
||||
[SerializeField] private int _instancePoolCapacity = 8;
|
||||
private bool _isInteractionEnabled = true;
|
||||
|
||||
private readonly List<TowerRepoItem> _activeItems = new List<TowerRepoItem>();
|
||||
private readonly HashSet<long> _boundItemIds = new HashSet<long>();
|
||||
|
|
@ -81,6 +82,11 @@ namespace GeometryTD.UI
|
|||
|
||||
public bool CanAssign(IRepoDragItemView dragItem)
|
||||
{
|
||||
if (!_isInteractionEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dragItem == null || dragItem.InstanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -106,6 +112,11 @@ namespace GeometryTD.UI
|
|||
|
||||
public void OnDrop(PointerEventData eventData)
|
||||
{
|
||||
if (!_isInteractionEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventData == null)
|
||||
{
|
||||
return;
|
||||
|
|
@ -134,6 +145,11 @@ namespace GeometryTD.UI
|
|||
GameEntry.Event.Fire(this, RepoParticipantAssignRequestedEventArgs.Create(dragItem.InstanceId));
|
||||
}
|
||||
|
||||
public void SetInteractionEnabled(bool enabled)
|
||||
{
|
||||
_isInteractionEnabled = enabled;
|
||||
}
|
||||
|
||||
private void EnsurePool()
|
||||
{
|
||||
if (_itemPool != null)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using GeometryTD.CustomEvent;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
|
|
@ -8,12 +9,12 @@ namespace GeometryTD.UI
|
|||
public class RepoForm : UGuiForm
|
||||
{
|
||||
[SerializeField] private CombineArea _combineArea;
|
||||
|
||||
[SerializeField] private CompArea _compArea;
|
||||
|
||||
[SerializeField] private SellArea _sellArea;
|
||||
[SerializeField] private ParticipantArea _participantArea;
|
||||
|
||||
[SerializeField] private TMP_Text _goldText;
|
||||
[SerializeField] private CommonButton _sellModeButton;
|
||||
[SerializeField] private TMP_Text _sellModeButtonText;
|
||||
|
||||
public void RefreshUI(RepoFormContext context)
|
||||
{
|
||||
|
|
@ -23,10 +24,25 @@ namespace GeometryTD.UI
|
|||
}
|
||||
|
||||
RefreshGoldText(context.GoldText);
|
||||
RefreshStateUI(context);
|
||||
|
||||
_combineArea?.OnInit(context.CombineAreaContext);
|
||||
_compArea?.OnInit(context.CompAreaContext);
|
||||
_sellArea?.OnInit(context.SellAreaContext);
|
||||
_participantArea?.OnInit(context.ParticipantAreaContext);
|
||||
|
||||
if (_combineArea != null)
|
||||
{
|
||||
_combineArea.gameObject.SetActive(context.ShowCombineArea);
|
||||
_combineArea.SetInteractionEnabled(context.State == RepoFormState.Assemble);
|
||||
}
|
||||
|
||||
if (_sellArea != null)
|
||||
{
|
||||
_sellArea.gameObject.SetActive(context.ShowSellArea);
|
||||
}
|
||||
|
||||
_participantArea?.SetInteractionEnabled(context.State == RepoFormState.Assemble);
|
||||
}
|
||||
|
||||
public void RefreshGoldText(string goldText)
|
||||
|
|
@ -89,6 +105,7 @@ namespace GeometryTD.UI
|
|||
|
||||
_combineArea?.OnReset();
|
||||
_compArea?.OnReset();
|
||||
_sellArea?.OnReset();
|
||||
_participantArea?.OnReset();
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
|
@ -97,5 +114,23 @@ namespace GeometryTD.UI
|
|||
{
|
||||
GameEntry.Event.Fire(this, RepoFormReturnEventArgs.Create());
|
||||
}
|
||||
|
||||
public void OnSellModeButtonClick()
|
||||
{
|
||||
GameEntry.Event.Fire(this, RepoSellModeToggleRequestedEventArgs.Create());
|
||||
}
|
||||
|
||||
private void RefreshStateUI(RepoFormContext context)
|
||||
{
|
||||
if (_sellModeButton != null)
|
||||
{
|
||||
_sellModeButton.gameObject.SetActive(context != null && context.ShowSellModeButton);
|
||||
}
|
||||
|
||||
if (_sellModeButtonText != null)
|
||||
{
|
||||
_sellModeButtonText.text = context?.SellModeButtonText ?? "出售模式";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace GeometryTD.UI
|
|||
[SerializeField] private IconArea _iconArea;
|
||||
|
||||
private static readonly Color SelectedColor = new Color32(255, 216, 102, 255);
|
||||
private static readonly Color DisabledSellColor = new Color32(96, 96, 96, 255);
|
||||
private static readonly Color EmptyEnduranceColor = new Color(0.8f, 0f, 0f, 1f);
|
||||
private static readonly Color FullEnduranceColor = new Color(0f, 0.8f, 0f, 1f);
|
||||
private static readonly Vector2 DefaultDragGhostSize = new Vector2(64f, 64f);
|
||||
|
|
@ -72,7 +73,7 @@ namespace GeometryTD.UI
|
|||
_context = context;
|
||||
_iconArea.OnInit(context.IconAreaContext);
|
||||
|
||||
SetSelected(false);
|
||||
SetSelected(context.IsSellSelected);
|
||||
ResetDragState();
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +105,12 @@ namespace GeometryTD.UI
|
|||
return;
|
||||
}
|
||||
|
||||
if (_context != null && _context.IsSellMode && !_context.IsSellable)
|
||||
{
|
||||
_bgImage.color = DisabledSellColor;
|
||||
return;
|
||||
}
|
||||
|
||||
float enduranceRate = _context != null ? Mathf.Clamp01(_context.EnduranceRate01) : 1f;
|
||||
_bgImage.color = Color.Lerp(EmptyEnduranceColor, FullEnduranceColor, enduranceRate);
|
||||
}
|
||||
|
|
@ -212,6 +219,11 @@ namespace GeometryTD.UI
|
|||
return false;
|
||||
}
|
||||
|
||||
if (_context.IsSellMode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_isSelected && _context.ComponentSlotType != TowerCompSlotType.None)
|
||||
{
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
{
|
||||
public class SellArea : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Transform _content;
|
||||
[SerializeField] private RepoItem _itemTemplate;
|
||||
[SerializeField] private TowerRepoItem _towerItemTemplate;
|
||||
[SerializeField] private TMP_Text _totalPrice;
|
||||
|
||||
private readonly List<RepoItem> _activeComponentItems = new List<RepoItem>();
|
||||
private readonly List<TowerRepoItem> _activeTowerItems = new List<TowerRepoItem>();
|
||||
private SellAreaContext _context;
|
||||
|
||||
public void OnInit(SellAreaContext context)
|
||||
{
|
||||
OnReset();
|
||||
_context = context;
|
||||
|
||||
if (_totalPrice != null)
|
||||
{
|
||||
_totalPrice.text = context?.TotalPriceText ?? string.Empty;
|
||||
}
|
||||
|
||||
if (context == null || _content == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.ComponentItems != null)
|
||||
{
|
||||
foreach (RepoItemContext itemContext in context.ComponentItems)
|
||||
{
|
||||
if (itemContext == null || _itemTemplate == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RepoItem item = Instantiate(_itemTemplate, _content);
|
||||
item.gameObject.SetActive(true);
|
||||
item.OnInit(itemContext);
|
||||
_activeComponentItems.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.TowerItems != null)
|
||||
{
|
||||
foreach (TowerRepoItemContext itemContext in context.TowerItems)
|
||||
{
|
||||
if (itemContext == null || _towerItemTemplate == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TowerRepoItem item = Instantiate(_towerItemTemplate, _content);
|
||||
item.gameObject.SetActive(true);
|
||||
item.OnInit(itemContext);
|
||||
_activeTowerItems.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
for (int i = _activeComponentItems.Count - 1; i >= 0; i--)
|
||||
{
|
||||
RepoItem item = _activeComponentItems[i];
|
||||
if (item != null)
|
||||
{
|
||||
Destroy(item.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = _activeTowerItems.Count - 1; i >= 0; i--)
|
||||
{
|
||||
TowerRepoItem item = _activeTowerItems[i];
|
||||
if (item != null)
|
||||
{
|
||||
Destroy(item.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
_activeComponentItems.Clear();
|
||||
_activeTowerItems.Clear();
|
||||
_context = null;
|
||||
|
||||
if (_totalPrice != null)
|
||||
{
|
||||
_totalPrice.text = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnCancelButtonClick()
|
||||
{
|
||||
GameEntry.Event.Fire(this, RepoSellCancelRequestedEventArgs.Create());
|
||||
}
|
||||
|
||||
public void OnConfirmButtonClick()
|
||||
{
|
||||
if (_context == null || !_context.CanConfirmSell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Fire(this, RepoSellConfirmRequestedEventArgs.Create());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 548fcad4dc8202f41b67f48aab7bed13
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -61,37 +61,16 @@ namespace GeometryTD.UI
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!GameEntry.PlayerInventory.TryConsumeGold(goodsItem.Price))
|
||||
if (!GameEntry.PlayerInventory.TryPurchaseComponent(goodsItem.SourceItem, goodsItem.Price))
|
||||
{
|
||||
Log.Warning("ShopFormUseCase.TryPurchase() failed. Not enough gold for goods item {0}.", goodsIndex);
|
||||
Log.Warning("ShopFormUseCase.TryPurchase() failed. Purchase command was rejected for goods item {0}.", goodsIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData inventoryDelta = WrapSingleItem(goodsItem.SourceItem);
|
||||
GameEntry.PlayerInventory.MergeInventory(inventoryDelta);
|
||||
goodsItem.IsPurchased = true;
|
||||
updatedRawData = CreateInitialModel();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BackpackInventoryData WrapSingleItem(TowerCompItemData item)
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData();
|
||||
switch (item)
|
||||
{
|
||||
case MuzzleCompItemData muzzleComp:
|
||||
inventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp));
|
||||
break;
|
||||
case BearingCompItemData bearingComp:
|
||||
inventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
|
||||
break;
|
||||
case BaseCompItemData baseComp:
|
||||
inventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
|
||||
break;
|
||||
}
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomUtility
|
||||
{
|
||||
public static class ShopPriceRuleService
|
||||
{
|
||||
public static int ResolveRandomBuyPrice(IReadOnlyList<DRShopPrice> shopPriceRows, RarityType rarity, Random random)
|
||||
{
|
||||
if (!TryFindPriceRow(shopPriceRows, rarity, out DRShopPrice row) || row == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int min = Mathf.Max(0, row.MinPrice);
|
||||
int max = Mathf.Max(min, row.MaxPrice);
|
||||
return random != null ? random.Next(min, max + 1) : min;
|
||||
}
|
||||
|
||||
public static int ResolveComponentSalePrice(TowerCompItemData component, IDataTable<DRShopPrice> shopPriceTable = null)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ResolveBasePrice(component.Rarity, shopPriceTable);
|
||||
}
|
||||
|
||||
public static bool TryResolveTowerSalePrice(
|
||||
TowerItemData tower,
|
||||
BackpackInventoryData inventory,
|
||||
out int price,
|
||||
IDataTable<DRShopPrice> shopPriceTable = null)
|
||||
{
|
||||
price = 0;
|
||||
if (tower == null || inventory == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetComponentById(inventory.MuzzleComponents, tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComp) ||
|
||||
!TryGetComponentById(inventory.BearingComponents, tower.BearingComponentInstanceId, out BearingCompItemData bearingComp) ||
|
||||
!TryGetComponentById(inventory.BaseComponents, tower.BaseComponentInstanceId, out BaseCompItemData baseComp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
price = ResolveComponentSalePrice(muzzleComp, shopPriceTable) +
|
||||
ResolveComponentSalePrice(bearingComp, shopPriceTable) +
|
||||
ResolveComponentSalePrice(baseComp, shopPriceTable);
|
||||
return price > 0;
|
||||
}
|
||||
|
||||
public static int ResolveBasePrice(RarityType rarity, IDataTable<DRShopPrice> shopPriceTable = null)
|
||||
{
|
||||
IDataTable<DRShopPrice> resolvedTable = shopPriceTable ?? GameEntry.DataTable.GetDataTable<DRShopPrice>();
|
||||
if (resolvedTable == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
DRShopPrice[] rows = resolvedTable.GetAllDataRows();
|
||||
if (!TryFindPriceRow(rows, rarity, out DRShopPrice row) || row == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int min = Mathf.Max(0, row.MinPrice);
|
||||
int max = Mathf.Max(min, row.MaxPrice);
|
||||
return Mathf.RoundToInt((min + max) * 0.5f);
|
||||
}
|
||||
|
||||
private static bool TryFindPriceRow(IReadOnlyList<DRShopPrice> rows, RarityType rarity, out DRShopPrice result)
|
||||
{
|
||||
result = null;
|
||||
if (rows == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
{
|
||||
DRShopPrice row = rows[i];
|
||||
if (row != null && row.Rarity == rarity)
|
||||
{
|
||||
result = row;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetComponentById<TComp>(IReadOnlyList<TComp> components, long instanceId, out TComp result)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
result = null;
|
||||
if (components == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
{
|
||||
TComp component = components[i];
|
||||
if (component != null && component.InstanceId == instanceId)
|
||||
{
|
||||
result = component;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b1bcf5c75bea4faaacdb4ffc5adfaf36
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,298 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomComponent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GeometryTD.Tests.EditMode
|
||||
{
|
||||
public sealed class PlayerInventoryTradeServiceTests
|
||||
{
|
||||
[Test]
|
||||
public void TrySellItems_Sells_Selected_Component_And_Tower_Atomically()
|
||||
{
|
||||
BackpackInventoryData inventory = CreateInventory();
|
||||
PlayerInventoryState state = new PlayerInventoryState();
|
||||
PlayerInventoryQueryModel queryModel = new PlayerInventoryQueryModel(state);
|
||||
PlayerInventoryCommandModel commandModel = new PlayerInventoryCommandModel(state);
|
||||
commandModel.Initialize(inventory, maxParticipantTowerCount: 4);
|
||||
|
||||
PlayerInventoryTradeService tradeService = new PlayerInventoryTradeService(
|
||||
queryModel,
|
||||
commandModel,
|
||||
CreatePriceTable(
|
||||
CreateShopPriceRow(1, RarityType.Blue, 30, 34),
|
||||
CreateShopPriceRow(2, RarityType.Purple, 60, 64)));
|
||||
|
||||
bool success = tradeService.TrySellItems(new long[] { 101, 201 }, out PlayerInventorySaleResult result);
|
||||
|
||||
Assert.That(success, Is.True);
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.IsSuccess, Is.True);
|
||||
Assert.That(result.SoldComponentCount, Is.EqualTo(1));
|
||||
Assert.That(result.SoldTowerCount, Is.EqualTo(1));
|
||||
Assert.That(queryModel.Inventory.Gold, Is.EqualTo(138));
|
||||
Assert.That(queryModel.Inventory.MuzzleComponents.Exists(item => item.InstanceId == 101), Is.False);
|
||||
Assert.That(queryModel.Inventory.Towers.Exists(item => item.InstanceId == 201), Is.False);
|
||||
Assert.That(queryModel.Inventory.MuzzleComponents.Exists(item => item.InstanceId == 202), Is.False);
|
||||
Assert.That(queryModel.Inventory.BearingComponents.Exists(item => item.InstanceId == 203), Is.False);
|
||||
Assert.That(queryModel.Inventory.BaseComponents.Exists(item => item.InstanceId == 204), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryGetSaleCandidate_Rejects_Participant_Tower()
|
||||
{
|
||||
BackpackInventoryData inventory = CreateInventory();
|
||||
inventory.ParticipantTowerInstanceIds.Add(201);
|
||||
|
||||
PlayerInventoryState state = new PlayerInventoryState();
|
||||
PlayerInventoryQueryModel queryModel = new PlayerInventoryQueryModel(state);
|
||||
PlayerInventoryCommandModel commandModel = new PlayerInventoryCommandModel(state);
|
||||
commandModel.Initialize(inventory, maxParticipantTowerCount: 4);
|
||||
|
||||
PlayerInventoryTradeService tradeService = new PlayerInventoryTradeService(
|
||||
queryModel,
|
||||
commandModel,
|
||||
CreatePriceTable(CreateShopPriceRow(1, RarityType.Blue, 30, 34)));
|
||||
|
||||
bool resolved = tradeService.TryGetSaleCandidate(201, out PlayerInventorySaleCandidate candidate);
|
||||
|
||||
Assert.That(resolved, Is.True);
|
||||
Assert.That(candidate, Is.Not.Null);
|
||||
Assert.That(candidate.IsSellable, Is.False);
|
||||
Assert.That(candidate.FailureReason, Is.EqualTo(PlayerInventorySaleFailureReason.ParticipantTower));
|
||||
}
|
||||
|
||||
private static BackpackInventoryData CreateInventory()
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData
|
||||
{
|
||||
Gold = 10
|
||||
};
|
||||
|
||||
inventory.MuzzleComponents.Add(new MuzzleCompItemData
|
||||
{
|
||||
InstanceId = 101,
|
||||
ConfigId = 1,
|
||||
Name = "Loose Muzzle",
|
||||
Rarity = RarityType.Blue,
|
||||
Endurance = 100f,
|
||||
IsAssembledIntoTower = false
|
||||
});
|
||||
inventory.MuzzleComponents.Add(new MuzzleCompItemData
|
||||
{
|
||||
InstanceId = 202,
|
||||
ConfigId = 2,
|
||||
Name = "Tower Muzzle",
|
||||
Rarity = RarityType.Blue,
|
||||
Endurance = 100f,
|
||||
IsAssembledIntoTower = true
|
||||
});
|
||||
inventory.BearingComponents.Add(new BearingCompItemData
|
||||
{
|
||||
InstanceId = 203,
|
||||
ConfigId = 3,
|
||||
Name = "Tower Bearing",
|
||||
Rarity = RarityType.Blue,
|
||||
Endurance = 100f,
|
||||
IsAssembledIntoTower = true
|
||||
});
|
||||
inventory.BaseComponents.Add(new BaseCompItemData
|
||||
{
|
||||
InstanceId = 204,
|
||||
ConfigId = 4,
|
||||
Name = "Tower Base",
|
||||
Rarity = RarityType.Blue,
|
||||
Endurance = 100f,
|
||||
IsAssembledIntoTower = true
|
||||
});
|
||||
|
||||
inventory.Towers.Add(new TowerItemData
|
||||
{
|
||||
InstanceId = 201,
|
||||
Name = "Tower #201",
|
||||
Rarity = RarityType.Blue,
|
||||
MuzzleComponentInstanceId = 202,
|
||||
BearingComponentInstanceId = 203,
|
||||
BaseComponentInstanceId = 204
|
||||
});
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private static IDataTable<DRShopPrice> CreatePriceTable(params DRShopPrice[] rows)
|
||||
{
|
||||
return new FakeDataTable<DRShopPrice>(rows);
|
||||
}
|
||||
|
||||
private static DRShopPrice CreateShopPriceRow(int id, RarityType rarity, int minPrice, int maxPrice)
|
||||
{
|
||||
DRShopPrice row = new DRShopPrice();
|
||||
Assert.That(row.ParseDataRow($"\t{id}\t\t{rarity}\t{minPrice}\t{maxPrice}", null), Is.True);
|
||||
return row;
|
||||
}
|
||||
|
||||
private sealed class FakeDataTable<TRow> : IDataTable<TRow> where TRow : class, IDataRow
|
||||
{
|
||||
private readonly Dictionary<int, TRow> _rowsById = new();
|
||||
|
||||
public FakeDataTable(params TRow[] rows)
|
||||
{
|
||||
if (rows == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < rows.Length; i++)
|
||||
{
|
||||
TRow row = rows[i];
|
||||
if (row != null)
|
||||
{
|
||||
_rowsById[row.Id] = row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Name => typeof(TRow).Name;
|
||||
public string FullName => typeof(TRow).FullName;
|
||||
public Type Type => typeof(TRow);
|
||||
public int Count => _rowsById.Count;
|
||||
public TRow this[int id] => GetDataRow(id);
|
||||
public TRow MinIdDataRow => null;
|
||||
public TRow MaxIdDataRow => null;
|
||||
|
||||
public bool HasDataRow(int id) => _rowsById.ContainsKey(id);
|
||||
public bool HasDataRow(Predicate<TRow> condition) => GetDataRow(condition) != null;
|
||||
public TRow GetDataRow(int id) => _rowsById.TryGetValue(id, out TRow row) ? row : null;
|
||||
|
||||
public TRow GetDataRow(Predicate<TRow> condition)
|
||||
{
|
||||
foreach (TRow row in _rowsById.Values)
|
||||
{
|
||||
if (row != null && condition != null && condition(row))
|
||||
{
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public TRow[] GetDataRows(Predicate<TRow> condition)
|
||||
{
|
||||
List<TRow> results = new();
|
||||
GetDataRows(condition, results);
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
public void GetDataRows(Predicate<TRow> condition, List<TRow> results)
|
||||
{
|
||||
results?.Clear();
|
||||
if (condition == null || results == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (TRow row in _rowsById.Values)
|
||||
{
|
||||
if (row != null && condition(row))
|
||||
{
|
||||
results.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TRow[] GetDataRows(Comparison<TRow> comparison)
|
||||
{
|
||||
List<TRow> results = new();
|
||||
GetDataRows(comparison, results);
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
public void GetDataRows(Comparison<TRow> comparison, List<TRow> results)
|
||||
{
|
||||
results?.Clear();
|
||||
if (results == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
results.AddRange(_rowsById.Values);
|
||||
if (comparison != null)
|
||||
{
|
||||
results.Sort(comparison);
|
||||
}
|
||||
}
|
||||
|
||||
public TRow[] GetDataRows(Predicate<TRow> condition, Comparison<TRow> comparison)
|
||||
{
|
||||
List<TRow> results = new();
|
||||
GetDataRows(condition, comparison, results);
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
public void GetDataRows(Predicate<TRow> condition, Comparison<TRow> comparison, List<TRow> results)
|
||||
{
|
||||
GetDataRows(condition, results);
|
||||
if (results != null && comparison != null)
|
||||
{
|
||||
results.Sort(comparison);
|
||||
}
|
||||
}
|
||||
|
||||
public TRow[] GetAllDataRows()
|
||||
{
|
||||
List<TRow> results = new();
|
||||
GetAllDataRows(results);
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
public void GetAllDataRows(List<TRow> results)
|
||||
{
|
||||
results?.Clear();
|
||||
if (results == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (int id in GetOrderedIds())
|
||||
{
|
||||
results.Add(_rowsById[id]);
|
||||
}
|
||||
}
|
||||
|
||||
public bool AddDataRow(string dataRowString, object userData) => throw new NotSupportedException();
|
||||
public bool AddDataRow(byte[] dataRowBytes, int startIndex, int length, object userData) => throw new NotSupportedException();
|
||||
public bool RemoveDataRow(int id) => _rowsById.Remove(id);
|
||||
|
||||
public void RemoveAllDataRows()
|
||||
{
|
||||
_rowsById.Clear();
|
||||
}
|
||||
|
||||
public IEnumerator<TRow> GetEnumerator()
|
||||
{
|
||||
foreach (int id in GetOrderedIds())
|
||||
{
|
||||
yield return _rowsById[id];
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
private int[] GetOrderedIds()
|
||||
{
|
||||
int[] ids = new int[_rowsById.Count];
|
||||
_rowsById.Keys.CopyTo(ids, 0);
|
||||
Array.Sort(ids);
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: eae86223cf354e758fad4be62efd678d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -90,20 +90,24 @@
|
|||
- 三个示例事件在局内都能完整触发、结算、回写状态。
|
||||
- 不会出现组件被扣掉但奖励未发、或耐久扣成负值这种半成功状态。
|
||||
|
||||
### P1-03 商店节点:购买 / 出售组件
|
||||
### P1-03 商店节点购买 + 背包出售组件 / 防御塔
|
||||
|
||||
- 核心目标:把当前“仅购买组件”的商店补成完整交易节点。
|
||||
- 核心目标:保留商店节点的购买职责,把出售入口迁到背包 `RepoForm`,形成完整交易闭环。
|
||||
- 交付物:
|
||||
- `Assets/GameMain/Scripts/UI/Shop/`
|
||||
- `Assets/GameMain/Scripts/UI/Game/`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs`
|
||||
- 必要时新增 `Assets/GameMain/Scripts/Utility/` 下的交易规则工具
|
||||
- `Assets/GameMain/Scripts/CustomComponent/PlayerInventory/`
|
||||
- 具体拆分:
|
||||
- Shop UI 增加玩家库存展示与出售入口,至少支持出售三类组件。
|
||||
- 交易结算统一走一个买卖接口,不要在 UI 层分别修改金币和库存。
|
||||
- Shop UI 保持“4 个商品 + 购买 + 打开背包”主链,不再承担库存展示与出售交互。
|
||||
- Repo UI 增加明确的出售模式入口;进入后可批量选择未组装组件与非参战防御塔,并显示当前选择总价值。
|
||||
- 参战防御塔不能直接出售,必须先从参战区移出;出售整塔时,需要连同其绑定的 3 个组件一起从库存移除。
|
||||
- 交易结算统一走库存域的显式买/卖命令,不要在 UI 层分别修改金币和库存。
|
||||
- 商品购买后状态、库存变化、金币变化要同时刷新,不允许 UI 还显示可买但实际已买。
|
||||
- 明确商店节点退出条件与节点完成时机,避免出现开店即完成或出售后不回流的问题。
|
||||
- 验收补充:
|
||||
- 买卖后库存与金币实时正确更新。
|
||||
- 商店购买、背包出售后库存与金币实时正确更新。
|
||||
- 从商店里打开背包出售后,返回商店时金币显示与商品状态正确刷新。
|
||||
- 关闭商店后重新打开同一节点时,商品状态与本节点交易结果一致。
|
||||
|
||||
### P1-04 商店定价规则:买价、半价回收、卖塔 +10%、耐久折价
|
||||
|
|
@ -114,7 +118,7 @@
|
|||
- `Assets/GameMain/Scripts/Entity/` 或库存相关域对象
|
||||
- 视需要调整 `Assets/GameMain/DataTables/ShopPrice.txt`
|
||||
- 具体拆分:
|
||||
- 抽出统一价格解析入口:基础买价、出售价、塔出售加成、耐久折价都走同一公式服务。
|
||||
- 抽出统一价格解析入口:商店买价、背包组件出售价、背包防御塔出售价、塔出售加成、耐久折价都走同一公式服务。
|
||||
- 明确“组件价格”和“整塔价格”关系,不能在 UI 里临时把三组件价格相加后再乘一个魔法数。
|
||||
- 明确耐久折价规则:按当前耐久比例折价,还是按损坏阈值分段折价;先写死规则文档,再落代码。
|
||||
- 如果 `ShopPrice.txt` 只够表达品质区间,新增字段或新表承载倍率,不要继续在代码里硬编码。
|
||||
|
|
|
|||
Loading…
Reference in New Issue