为 RepoForm 添加出售功能,ShopNode 只承载玩家购买组件的逻辑

This commit is contained in:
SepComet 2026-03-16 17:52:23 +08:00
parent dc2aa59d58
commit 2e54acbc85
33 changed files with 3198 additions and 91 deletions

View File

@ -123,18 +123,7 @@ namespace GeometryTD.CustomComponent
private int ResolveRandomPrice(RarityType rarity, Random random) private int ResolveRandomPrice(RarityType rarity, Random random)
{ {
for (int i = 0; i < _shopPriceRows.Count; i++) return ShopPriceRuleService.ResolveRandomBuyPrice(_shopPriceRows, rarity, random);
{
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;
} }
private static IconAreaContext BuildIconAreaContext(TowerCompItemData item) private static IconAreaContext BuildIconAreaContext(TowerCompItemData item)

View File

@ -14,6 +14,7 @@ namespace GeometryTD.CustomComponent
private PlayerInventoryCommandModel _commandModel; private PlayerInventoryCommandModel _commandModel;
private PlayerInventoryTowerRosterService _towerRosterService; private PlayerInventoryTowerRosterService _towerRosterService;
private PlayerInventoryTowerAssemblyService _towerAssemblyService; private PlayerInventoryTowerAssemblyService _towerAssemblyService;
private PlayerInventoryTradeService _tradeService;
public int Gold public int Gold
{ {
@ -100,6 +101,12 @@ namespace GeometryTD.CustomComponent
return _commandModel.TryConsumeGold(costGold); return _commandModel.TryConsumeGold(costGold);
} }
public bool TryPurchaseComponent(TowerCompItemData item, int price)
{
EnsureInitialized();
return _tradeService.TryPurchaseComponent(item, price);
}
public void AddGold(int gainGold) public void AddGold(int gainGold)
{ {
EnsureInitialized(); EnsureInitialized();
@ -138,6 +145,18 @@ namespace GeometryTD.CustomComponent
return _towerRosterService.ReduceTowerEndurance(towerInstanceIds, enduranceLoss); 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() private void EnsureInitialized()
{ {
if (_queryModel.IsInitialized) if (_queryModel.IsInitialized)
@ -155,6 +174,7 @@ namespace GeometryTD.CustomComponent
_commandModel ??= new PlayerInventoryCommandModel(_state); _commandModel ??= new PlayerInventoryCommandModel(_state);
_towerRosterService ??= new PlayerInventoryTowerRosterService(_queryModel, MaxParticipantTowerCount); _towerRosterService ??= new PlayerInventoryTowerRosterService(_queryModel, MaxParticipantTowerCount);
_towerAssemblyService ??= new PlayerInventoryTowerAssemblyService(_queryModel, _commandModel); _towerAssemblyService ??= new PlayerInventoryTowerAssemblyService(_queryModel, _commandModel);
_tradeService ??= new PlayerInventoryTradeService(_queryModel, _commandModel);
} }
} }
} }

View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 298cf9214556453cb5e7ec7c0605c350
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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()
{
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2cc34a22ea6347619b065c5874e0b715
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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()
{
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 598b87b9cb8e4783bc73db5e255d31f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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()
{
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44692273ce6a417a875dea9675663d82
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,11 +1,15 @@
using GeometryTD.UI;
namespace GeometryTD.UI namespace GeometryTD.UI
{ {
public class RepoFormContext : UIContext public class RepoFormContext : UIContext
{ {
public string GoldText; public string GoldText;
public RepoFormState State;
public bool ShowSellModeButton;
public string SellModeButtonText;
public bool ShowCombineArea;
public bool ShowSellArea;
public CombineAreaContext CombineAreaContext; public CombineAreaContext CombineAreaContext;
public SellAreaContext SellAreaContext;
public CompAreaContext CompAreaContext; public CompAreaContext CompAreaContext;
public ParticipantAreaContext ParticipantAreaContext; public ParticipantAreaContext ParticipantAreaContext;
} }

View File

@ -0,0 +1,8 @@
namespace GeometryTD.UI
{
public enum RepoFormState
{
Assemble = 0,
Sell = 1
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9110fc764f3044bda40df4a8c02f320b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -11,6 +11,9 @@ namespace GeometryTD.UI
public float EnduranceRate01; public float EnduranceRate01;
public RepoItemClickActionType ClickActionType; public RepoItemClickActionType ClickActionType;
public TowerCompSlotType ComponentSlotType; public TowerCompSlotType ComponentSlotType;
public bool IsSellMode;
public bool IsSellable = true;
public bool IsSellSelected;
public IconAreaContext IconAreaContext; public IconAreaContext IconAreaContext;
} }
} }

View File

@ -0,0 +1,10 @@
namespace GeometryTD.UI
{
public class SellAreaContext
{
public string TotalPriceText;
public bool CanConfirmSell;
public RepoItemContext[] ComponentItems;
public TowerRepoItemContext[] TowerItems;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f153d1a509df4bb69c6d231df5b60db9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -23,21 +23,44 @@ namespace GeometryTD.UI
Dictionary<long, BearingCompItemData> bearingMap = BuildComponentMap(rawData.Inventory.BearingComponents); Dictionary<long, BearingCompItemData> bearingMap = BuildComponentMap(rawData.Inventory.BearingComponents);
Dictionary<long, BaseCompItemData> baseMap = BuildComponentMap(rawData.Inventory.BaseComponents); Dictionary<long, BaseCompItemData> baseMap = BuildComponentMap(rawData.Inventory.BaseComponents);
Dictionary<long, TowerItemData> towerMap = BuildTowerMap(rawData.Inventory.Towers); 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<RepoItemContext> componentItems = new List<RepoItemContext>();
List<TowerRepoItemContext> towerItems = new List<TowerRepoItemContext>(); List<TowerRepoItemContext> towerItems = new List<TowerRepoItemContext>();
List<RepoItemContext> selectedSellComponentItems = new List<RepoItemContext>();
List<TowerRepoItemContext> selectedSellTowerItems = new List<TowerRepoItemContext>();
if (rawData.Inventory.Towers != null) if (rawData.Inventory.Towers != null)
{ {
foreach (var tower in rawData.Inventory.Towers) foreach (TowerItemData tower in rawData.Inventory.Towers)
{ {
if (tower == null) if (tower == null)
{ {
continue; continue;
} }
TowerRepoItemContext towerContext = BuildTowerRepoItemContext(tower, muzzleMap, bearingMap, baseMap, bool isParticipantTower = rawData.Inventory.ParticipantTowerInstanceIds != null &&
RepoItemClickActionType.OpenDetail, true); 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); AddTowerItemContext(towerItems, towerContext);
if (isSellSelected)
{
selectedSellTowerItems.Add(towerContext);
}
AddItemDescSeed( AddItemDescSeed(
tower.InstanceId, tower.InstanceId,
tower.Name, tower.Name,
@ -50,23 +73,24 @@ namespace GeometryTD.UI
if (rawData.Inventory.MuzzleComponents != null) if (rawData.Inventory.MuzzleComponents != null)
{ {
foreach (var item in rawData.Inventory.MuzzleComponents) foreach (MuzzleCompItemData item in rawData.Inventory.MuzzleComponents)
{ {
if (item == null || item.IsAssembledIntoTower) if (item == null || item.IsAssembledIntoTower)
{ {
continue; continue;
} }
RepoItemContext componentContext = new RepoItemContext RepoItemContext componentContext = BuildSellableComponentContext(
{ item,
InstanceId = item.InstanceId, TowerCompSlotType.Muzzle,
CanDrag = true, isSellState,
EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item), selectedSellItemIds.Contains(item.InstanceId));
ClickActionType = RepoItemClickActionType.OpenDetail,
ComponentSlotType = TowerCompSlotType.Muzzle,
IconAreaContext = BuildIconAreaContext(item)
};
AddComponentItemContext(componentItems, componentContext); AddComponentItemContext(componentItems, componentContext);
if (componentContext.IsSellSelected)
{
selectedSellComponentItems.Add(componentContext);
}
AddItemDescSeed( AddItemDescSeed(
item.InstanceId, item.InstanceId,
item.Name, item.Name,
@ -86,16 +110,17 @@ namespace GeometryTD.UI
continue; continue;
} }
RepoItemContext componentContext = new RepoItemContext RepoItemContext componentContext = BuildSellableComponentContext(
{ item,
InstanceId = item.InstanceId, TowerCompSlotType.Bearing,
CanDrag = true, isSellState,
EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item), selectedSellItemIds.Contains(item.InstanceId));
ClickActionType = RepoItemClickActionType.OpenDetail,
ComponentSlotType = TowerCompSlotType.Bearing,
IconAreaContext = BuildIconAreaContext(item)
};
AddComponentItemContext(componentItems, componentContext); AddComponentItemContext(componentItems, componentContext);
if (componentContext.IsSellSelected)
{
selectedSellComponentItems.Add(componentContext);
}
AddItemDescSeed( AddItemDescSeed(
item.InstanceId, item.InstanceId,
item.Name, item.Name,
@ -115,16 +140,17 @@ namespace GeometryTD.UI
continue; continue;
} }
RepoItemContext componentContext = new RepoItemContext RepoItemContext componentContext = BuildSellableComponentContext(
{ item,
InstanceId = item.InstanceId, TowerCompSlotType.Base,
CanDrag = true, isSellState,
EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item), selectedSellItemIds.Contains(item.InstanceId));
ClickActionType = RepoItemClickActionType.OpenDetail,
ComponentSlotType = TowerCompSlotType.Base,
IconAreaContext = BuildIconAreaContext(item)
};
AddComponentItemContext(componentItems, componentContext); AddComponentItemContext(componentItems, componentContext);
if (componentContext.IsSellSelected)
{
selectedSellComponentItems.Add(componentContext);
}
AddItemDescSeed( AddItemDescSeed(
item.InstanceId, item.InstanceId,
item.Name, item.Name,
@ -141,7 +167,19 @@ namespace GeometryTD.UI
return new RepoFormContext return new RepoFormContext
{ {
GoldText = $"金币: {rawData.Inventory.Gold}", 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(), 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 CompAreaContext = new CompAreaContext
{ {
ComponentItems = componentItems.ToArray(), 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) private void AddComponentItemContext(List<RepoItemContext> items, RepoItemContext itemContext)
{ {
if (itemContext == null) if (itemContext == null)
@ -218,7 +276,7 @@ namespace GeometryTD.UI
return map; return map;
} }
foreach (var item in items) foreach (TComp item in items)
{ {
if (item == null || item.InstanceId <= 0) if (item == null || item.InstanceId <= 0)
{ {
@ -231,6 +289,26 @@ namespace GeometryTD.UI
return map; 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) private static Dictionary<long, TowerItemData> BuildTowerMap(IReadOnlyList<TowerItemData> towers)
{ {
Dictionary<long, TowerItemData> map = new Dictionary<long, TowerItemData>(); Dictionary<long, TowerItemData> map = new Dictionary<long, TowerItemData>();
@ -239,7 +317,7 @@ namespace GeometryTD.UI
return map; return map;
} }
foreach (var tower in towers) foreach (TowerItemData tower in towers)
{ {
if (tower == null || tower.InstanceId <= 0) if (tower == null || tower.InstanceId <= 0)
{ {
@ -263,7 +341,7 @@ namespace GeometryTD.UI
List<TowerRepoItemContext> participantItems = new List<TowerRepoItemContext>(); List<TowerRepoItemContext> participantItems = new List<TowerRepoItemContext>();
if (inventory?.ParticipantTowerInstanceIds != null && towerMap != null) if (inventory?.ParticipantTowerInstanceIds != null && towerMap != null)
{ {
foreach (var towerId in inventory.ParticipantTowerInstanceIds) foreach (long towerId in inventory.ParticipantTowerInstanceIds)
{ {
if (towerId <= 0) if (towerId <= 0)
{ {
@ -280,8 +358,17 @@ namespace GeometryTD.UI
continue; continue;
} }
TowerRepoItemContext towerContext = BuildTowerRepoItemContext(tower, muzzleMap, bearingMap, baseMap, TowerRepoItemContext towerContext = BuildTowerRepoItemContext(
RepoItemClickActionType.RemoveParticipant, false); tower,
muzzleMap,
bearingMap,
baseMap,
RepoItemClickActionType.RemoveParticipant,
false,
false,
false,
false,
false);
if (towerContext != null) if (towerContext != null)
{ {
participantItems.Add(towerContext); participantItems.Add(towerContext);
@ -304,6 +391,11 @@ namespace GeometryTD.UI
return; return;
} }
if (Context != null && Context.State == RepoFormState.Sell)
{
return;
}
foreach (long towerId in _compAreaTowerIds) foreach (long towerId in _compAreaTowerIds)
{ {
Form.SetRepoItemSelected(towerId, _participantTowerIds.Contains(towerId)); Form.SetRepoItemSelected(towerId, _participantTowerIds.Contains(towerId));
@ -334,7 +426,11 @@ namespace GeometryTD.UI
IReadOnlyDictionary<long, BearingCompItemData> bearingMap, IReadOnlyDictionary<long, BearingCompItemData> bearingMap,
IReadOnlyDictionary<long, BaseCompItemData> baseMap, IReadOnlyDictionary<long, BaseCompItemData> baseMap,
RepoItemClickActionType clickActionType, RepoItemClickActionType clickActionType,
bool canDrag) bool canDrag,
bool isSellMode,
bool isSellable,
bool isSellSelected,
bool highlightSelected)
{ {
if (tower == null) if (tower == null)
{ {
@ -348,6 +444,9 @@ namespace GeometryTD.UI
EnduranceRate01 = ItemDescUtility.ResolveTowerEnduranceRate(tower, muzzleMap, bearingMap, baseMap), EnduranceRate01 = ItemDescUtility.ResolveTowerEnduranceRate(tower, muzzleMap, bearingMap, baseMap),
ClickActionType = clickActionType, ClickActionType = clickActionType,
ComponentSlotType = TowerCompSlotType.None, ComponentSlotType = TowerCompSlotType.None,
IsSellMode = isSellMode,
IsSellable = isSellable,
IsSellSelected = isSellSelected || highlightSelected,
IconAreaContext = new TowerIconAreaContext IconAreaContext = new TowerIconAreaContext
{ {
Rarity = tower.Rarity, Rarity = tower.Rarity,

View File

@ -3,6 +3,7 @@ using GeometryTD.CustomEvent;
using GeometryTD.CustomUtility; using GeometryTD.CustomUtility;
using GeometryTD.Definition; using GeometryTD.Definition;
using GameFramework.Event; using GameFramework.Event;
using GeometryTD.CustomComponent;
using UnityEngine; using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
@ -43,6 +44,9 @@ namespace GeometryTD.UI
GameEntry.Event.Subscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested); GameEntry.Event.Subscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested);
GameEntry.Event.Subscribe(RepoParticipantAssignRequestedEventArgs.EventId, OnRepoParticipantAssignRequested); GameEntry.Event.Subscribe(RepoParticipantAssignRequestedEventArgs.EventId, OnRepoParticipantAssignRequested);
GameEntry.Event.Subscribe(RepoFormReturnEventArgs.EventId, OnRepoFormReturn); 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() protected override void UnsubscribeCustomEvents()
@ -53,6 +57,9 @@ namespace GeometryTD.UI
GameEntry.Event.Unsubscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested); GameEntry.Event.Unsubscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested);
GameEntry.Event.Unsubscribe(RepoParticipantAssignRequestedEventArgs.EventId, OnRepoParticipantAssignRequested); GameEntry.Event.Unsubscribe(RepoParticipantAssignRequestedEventArgs.EventId, OnRepoParticipantAssignRequested);
GameEntry.Event.Unsubscribe(RepoFormReturnEventArgs.EventId, OnRepoFormReturn); 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) public override int? OpenUI(object userData = null)
@ -125,6 +132,24 @@ namespace GeometryTD.UI
return; 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 (clickActionType == RepoItemClickActionType.RemoveParticipant)
{ {
if (_useCase == null || Form == null) if (_useCase == null || Form == null)
@ -165,6 +190,11 @@ namespace GeometryTD.UI
return; return;
} }
if (Context != null && Context.State == RepoFormState.Sell)
{
return;
}
if (!(e is RepoItemDragEndedEventArgs args)) if (!(e is RepoItemDragEndedEventArgs args))
{ {
return; return;
@ -188,6 +218,11 @@ namespace GeometryTD.UI
return; return;
} }
if (Context != null && Context.State == RepoFormState.Sell)
{
return;
}
if (!(e is CombineSlotClickedEventArgs args)) if (!(e is CombineSlotClickedEventArgs args))
{ {
return; return;
@ -257,6 +292,11 @@ namespace GeometryTD.UI
return; return;
} }
if (Context != null && Context.State == RepoFormState.Sell)
{
return;
}
if (!(e is RepoParticipantAssignRequestedEventArgs args)) if (!(e is RepoParticipantAssignRequestedEventArgs args))
{ {
return; return;
@ -288,6 +328,65 @@ namespace GeometryTD.UI
RefreshParticipantAreaOnly(); 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) private bool IsEventFromCurrentForm(object sender)
{ {
if (Form == null) if (Form == null)
@ -309,6 +408,18 @@ namespace GeometryTD.UI
return false; return false;
} }
private static void OpenSellBlockedDialog(string message)
{
GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData
{
Mode = 1,
Title = "无法出售",
Message = message,
PauseGame = false,
ConfirmText = "知道了"
});
}
#endregion #endregion
} }
} }

View File

@ -5,5 +5,9 @@ namespace GeometryTD.UI
public class RepoFormRawData public class RepoFormRawData
{ {
public BackpackInventoryData Inventory; public BackpackInventoryData Inventory;
public RepoFormState State;
public long[] SelectedSellItemIds;
public int SelectedSellItemCount;
public int SelectedSellTotalPrice;
} }
} }

View File

@ -1,4 +1,6 @@
using GeometryTD.CustomUtility; using System.Collections.Generic;
using GeometryTD.CustomComponent;
using GeometryTD.CustomUtility;
using GeometryTD.Definition; using GeometryTD.Definition;
namespace GeometryTD.UI namespace GeometryTD.UI
@ -7,6 +9,8 @@ namespace GeometryTD.UI
{ {
private const int MaxParticipantCount = 4; private const int MaxParticipantCount = 4;
private BackpackInventoryData _fallbackInventory; private BackpackInventoryData _fallbackInventory;
private readonly HashSet<long> _selectedSellItemIds = new HashSet<long>();
private RepoFormState _state = RepoFormState.Assemble;
public RepoFormRawData CreateInitialModel() public RepoFormRawData CreateInitialModel()
{ {
@ -15,12 +19,23 @@ namespace GeometryTD.UI
: GetOrCreateFallbackInventory(); : GetOrCreateFallbackInventory();
return new RepoFormRawData 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) public bool TryAssembleTower(long muzzleItemId, long bearingItemId, long baseItemId)
{ {
if (_state == RepoFormState.Sell)
{
return false;
}
if (GameEntry.PlayerInventory == null) if (GameEntry.PlayerInventory == null)
{ {
return false; return false;
@ -35,6 +50,15 @@ namespace GeometryTD.UI
public ParticipantTowerAssignResult TryAddParticipantTower(long towerItemId) public ParticipantTowerAssignResult TryAddParticipantTower(long towerItemId)
{ {
if (_state == RepoFormState.Sell)
{
return new ParticipantTowerAssignResult
{
TowerInstanceId = towerItemId,
FailureReason = ParticipantTowerAssignFailureReason.ParticipantAreaFull
};
}
if (GameEntry.PlayerInventory == null) if (GameEntry.PlayerInventory == null)
{ {
BackpackInventoryData fallbackInventory = GetOrCreateFallbackInventory(); BackpackInventoryData fallbackInventory = GetOrCreateFallbackInventory();
@ -44,11 +68,16 @@ namespace GeometryTD.UI
MaxParticipantCount); MaxParticipantCount);
} }
return GameEntry.PlayerInventory.TryAddParticipantTower(towerItemId, 4); return GameEntry.PlayerInventory.TryAddParticipantTower(towerItemId, MaxParticipantCount);
} }
public bool TryRemoveParticipantTower(long towerItemId) public bool TryRemoveParticipantTower(long towerItemId)
{ {
if (_state == RepoFormState.Sell)
{
return false;
}
if (GameEntry.PlayerInventory == null) if (GameEntry.PlayerInventory == null)
{ {
BackpackInventoryData fallbackInventory = GetOrCreateFallbackInventory(); BackpackInventoryData fallbackInventory = GetOrCreateFallbackInventory();
@ -61,12 +90,120 @@ namespace GeometryTD.UI
return GameEntry.PlayerInventory.TryRemoveParticipantTower(towerItemId); 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() private BackpackInventoryData GetOrCreateFallbackInventory()
{ {
_fallbackInventory ??= InventorySeedUtility.CreateSampleInventory(); _fallbackInventory ??= InventorySeedUtility.CreateSampleInventory();
InventoryParticipantUtility.NormalizeParticipantState(_fallbackInventory, MaxParticipantCount); InventoryParticipantUtility.NormalizeParticipantState(_fallbackInventory, MaxParticipantCount);
return _fallbackInventory; 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;
}
} }
} }

View File

@ -8,6 +8,7 @@ namespace GeometryTD.UI
public class CombineArea : MonoBehaviour, IDropHandler public class CombineArea : MonoBehaviour, IDropHandler
{ {
[SerializeField] private CombineSlotItem[] _slots; [SerializeField] private CombineSlotItem[] _slots;
private bool _isInteractionEnabled = true;
public void OnInit(CombineAreaContext context) public void OnInit(CombineAreaContext context)
{ {
@ -43,6 +44,11 @@ namespace GeometryTD.UI
public void OnCombineClick() public void OnCombineClick()
{ {
if (!_isInteractionEnabled)
{
return;
}
if (!TryGetBoundItemId(TowerCompSlotType.Muzzle, out long muzzleItemId) || if (!TryGetBoundItemId(TowerCompSlotType.Muzzle, out long muzzleItemId) ||
!TryGetBoundItemId(TowerCompSlotType.Bearing, out long bearingItemId) || !TryGetBoundItemId(TowerCompSlotType.Bearing, out long bearingItemId) ||
!TryGetBoundItemId(TowerCompSlotType.Base, out long baseItemId)) !TryGetBoundItemId(TowerCompSlotType.Base, out long baseItemId))
@ -55,6 +61,11 @@ namespace GeometryTD.UI
public bool TryAssignItem(RepoItemContext itemContext) public bool TryAssignItem(RepoItemContext itemContext)
{ {
if (!_isInteractionEnabled)
{
return false;
}
if (itemContext == null || itemContext.ComponentSlotType == TowerCompSlotType.None) if (itemContext == null || itemContext.ComponentSlotType == TowerCompSlotType.None)
{ {
return false; return false;
@ -91,6 +102,11 @@ namespace GeometryTD.UI
public void OnDrop(PointerEventData eventData) public void OnDrop(PointerEventData eventData)
{ {
if (!_isInteractionEnabled)
{
return;
}
if (eventData == null) if (eventData == null)
{ {
return; return;
@ -113,6 +129,11 @@ namespace GeometryTD.UI
dragItem.SetDropResult(assigned); dragItem.SetDropResult(assigned);
} }
public void SetInteractionEnabled(bool enabled)
{
_isInteractionEnabled = enabled;
}
private CombineSlotItem FindSlot(TowerCompSlotType slotType) private CombineSlotItem FindSlot(TowerCompSlotType slotType)
{ {
if (_slots == null) if (_slots == null)

View File

@ -14,6 +14,7 @@ namespace GeometryTD.UI
[SerializeField] private Transform _content; [SerializeField] private Transform _content;
[SerializeField] private TowerRepoItem _towerItemTemplate; [SerializeField] private TowerRepoItem _towerItemTemplate;
[SerializeField] private int _instancePoolCapacity = 8; [SerializeField] private int _instancePoolCapacity = 8;
private bool _isInteractionEnabled = true;
private readonly List<TowerRepoItem> _activeItems = new List<TowerRepoItem>(); private readonly List<TowerRepoItem> _activeItems = new List<TowerRepoItem>();
private readonly HashSet<long> _boundItemIds = new HashSet<long>(); private readonly HashSet<long> _boundItemIds = new HashSet<long>();
@ -81,6 +82,11 @@ namespace GeometryTD.UI
public bool CanAssign(IRepoDragItemView dragItem) public bool CanAssign(IRepoDragItemView dragItem)
{ {
if (!_isInteractionEnabled)
{
return false;
}
if (dragItem == null || dragItem.InstanceId <= 0) if (dragItem == null || dragItem.InstanceId <= 0)
{ {
return false; return false;
@ -106,6 +112,11 @@ namespace GeometryTD.UI
public void OnDrop(PointerEventData eventData) public void OnDrop(PointerEventData eventData)
{ {
if (!_isInteractionEnabled)
{
return;
}
if (eventData == null) if (eventData == null)
{ {
return; return;
@ -134,6 +145,11 @@ namespace GeometryTD.UI
GameEntry.Event.Fire(this, RepoParticipantAssignRequestedEventArgs.Create(dragItem.InstanceId)); GameEntry.Event.Fire(this, RepoParticipantAssignRequestedEventArgs.Create(dragItem.InstanceId));
} }
public void SetInteractionEnabled(bool enabled)
{
_isInteractionEnabled = enabled;
}
private void EnsurePool() private void EnsurePool()
{ {
if (_itemPool != null) if (_itemPool != null)

View File

@ -1,6 +1,7 @@
using GeometryTD.CustomEvent; using GeometryTD.CustomEvent;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
using UnityEngine.UI;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.UI namespace GeometryTD.UI
@ -8,12 +9,12 @@ namespace GeometryTD.UI
public class RepoForm : UGuiForm public class RepoForm : UGuiForm
{ {
[SerializeField] private CombineArea _combineArea; [SerializeField] private CombineArea _combineArea;
[SerializeField] private CompArea _compArea; [SerializeField] private CompArea _compArea;
[SerializeField] private SellArea _sellArea;
[SerializeField] private ParticipantArea _participantArea; [SerializeField] private ParticipantArea _participantArea;
[SerializeField] private TMP_Text _goldText; [SerializeField] private TMP_Text _goldText;
[SerializeField] private CommonButton _sellModeButton;
[SerializeField] private TMP_Text _sellModeButtonText;
public void RefreshUI(RepoFormContext context) public void RefreshUI(RepoFormContext context)
{ {
@ -23,10 +24,25 @@ namespace GeometryTD.UI
} }
RefreshGoldText(context.GoldText); RefreshGoldText(context.GoldText);
RefreshStateUI(context);
_combineArea?.OnInit(context.CombineAreaContext); _combineArea?.OnInit(context.CombineAreaContext);
_compArea?.OnInit(context.CompAreaContext); _compArea?.OnInit(context.CompAreaContext);
_sellArea?.OnInit(context.SellAreaContext);
_participantArea?.OnInit(context.ParticipantAreaContext); _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) public void RefreshGoldText(string goldText)
@ -89,6 +105,7 @@ namespace GeometryTD.UI
_combineArea?.OnReset(); _combineArea?.OnReset();
_compArea?.OnReset(); _compArea?.OnReset();
_sellArea?.OnReset();
_participantArea?.OnReset(); _participantArea?.OnReset();
base.OnClose(isShutdown, userData); base.OnClose(isShutdown, userData);
} }
@ -97,5 +114,23 @@ namespace GeometryTD.UI
{ {
GameEntry.Event.Fire(this, RepoFormReturnEventArgs.Create()); 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 ?? "出售模式";
}
}
} }
} }

View File

@ -13,6 +13,7 @@ namespace GeometryTD.UI
[SerializeField] private IconArea _iconArea; [SerializeField] private IconArea _iconArea;
private static readonly Color SelectedColor = new Color32(255, 216, 102, 255); 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 EmptyEnduranceColor = new Color(0.8f, 0f, 0f, 1f);
private static readonly Color FullEnduranceColor = new Color(0f, 0.8f, 0f, 1f); private static readonly Color FullEnduranceColor = new Color(0f, 0.8f, 0f, 1f);
private static readonly Vector2 DefaultDragGhostSize = new Vector2(64f, 64f); private static readonly Vector2 DefaultDragGhostSize = new Vector2(64f, 64f);
@ -72,7 +73,7 @@ namespace GeometryTD.UI
_context = context; _context = context;
_iconArea.OnInit(context.IconAreaContext); _iconArea.OnInit(context.IconAreaContext);
SetSelected(false); SetSelected(context.IsSellSelected);
ResetDragState(); ResetDragState();
} }
@ -104,6 +105,12 @@ namespace GeometryTD.UI
return; return;
} }
if (_context != null && _context.IsSellMode && !_context.IsSellable)
{
_bgImage.color = DisabledSellColor;
return;
}
float enduranceRate = _context != null ? Mathf.Clamp01(_context.EnduranceRate01) : 1f; float enduranceRate = _context != null ? Mathf.Clamp01(_context.EnduranceRate01) : 1f;
_bgImage.color = Color.Lerp(EmptyEnduranceColor, FullEnduranceColor, enduranceRate); _bgImage.color = Color.Lerp(EmptyEnduranceColor, FullEnduranceColor, enduranceRate);
} }
@ -212,6 +219,11 @@ namespace GeometryTD.UI
return false; return false;
} }
if (_context.IsSellMode)
{
return false;
}
if (_isSelected && _context.ComponentSlotType != TowerCompSlotType.None) if (_isSelected && _context.ComponentSlotType != TowerCompSlotType.None)
{ {
return false; return false;

View File

@ -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());
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 548fcad4dc8202f41b67f48aab7bed13
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -61,37 +61,16 @@ namespace GeometryTD.UI
return false; 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; return false;
} }
BackpackInventoryData inventoryDelta = WrapSingleItem(goodsItem.SourceItem);
GameEntry.PlayerInventory.MergeInventory(inventoryDelta);
goodsItem.IsPurchased = true; goodsItem.IsPurchased = true;
updatedRawData = CreateInitialModel(); updatedRawData = CreateInitialModel();
return true; 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;
}
} }
} }

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eae86223cf354e758fad4be62efd678d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -90,20 +90,24 @@
- 三个示例事件在局内都能完整触发、结算、回写状态。 - 三个示例事件在局内都能完整触发、结算、回写状态。
- 不会出现组件被扣掉但奖励未发、或耐久扣成负值这种半成功状态。 - 不会出现组件被扣掉但奖励未发、或耐久扣成负值这种半成功状态。
### P1-03 商店节点:购买 / 出售组件 ### P1-03 商店节点购买 + 背包出售组件 / 防御塔
- 核心目标:把当前“仅购买组件”的商店补成完整交易节点 - 核心目标:保留商店节点的购买职责,把出售入口迁到背包 `RepoForm`,形成完整交易闭环
- 交付物: - 交付物:
- `Assets/GameMain/Scripts/UI/Shop/` - `Assets/GameMain/Scripts/UI/Shop/`
- `Assets/GameMain/Scripts/UI/Game/`
- `Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs` - `Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs`
- 必要时新增 `Assets/GameMain/Scripts/Utility/` 下的交易规则工具 - `Assets/GameMain/Scripts/CustomComponent/PlayerInventory/`
- 具体拆分: - 具体拆分:
- Shop UI 增加玩家库存展示与出售入口,至少支持出售三类组件。 - Shop UI 保持“4 个商品 + 购买 + 打开背包”主链,不再承担库存展示与出售交互。
- 交易结算统一走一个买卖接口,不要在 UI 层分别修改金币和库存。 - Repo UI 增加明确的出售模式入口;进入后可批量选择未组装组件与非参战防御塔,并显示当前选择总价值。
- 参战防御塔不能直接出售,必须先从参战区移出;出售整塔时,需要连同其绑定的 3 个组件一起从库存移除。
- 交易结算统一走库存域的显式买/卖命令,不要在 UI 层分别修改金币和库存。
- 商品购买后状态、库存变化、金币变化要同时刷新,不允许 UI 还显示可买但实际已买。 - 商品购买后状态、库存变化、金币变化要同时刷新,不允许 UI 还显示可买但实际已买。
- 明确商店节点退出条件与节点完成时机,避免出现开店即完成或出售后不回流的问题。 - 明确商店节点退出条件与节点完成时机,避免出现开店即完成或出售后不回流的问题。
- 验收补充: - 验收补充:
- 买卖后库存与金币实时正确更新。 - 商店购买、背包出售后库存与金币实时正确更新。
- 从商店里打开背包出售后,返回商店时金币显示与商品状态正确刷新。
- 关闭商店后重新打开同一节点时,商品状态与本节点交易结果一致。 - 关闭商店后重新打开同一节点时,商品状态与本节点交易结果一致。
### P1-04 商店定价规则:买价、半价回收、卖塔 +10%、耐久折价 ### P1-04 商店定价规则:买价、半价回收、卖塔 +10%、耐久折价
@ -114,7 +118,7 @@
- `Assets/GameMain/Scripts/Entity/` 或库存相关域对象 - `Assets/GameMain/Scripts/Entity/` 或库存相关域对象
- 视需要调整 `Assets/GameMain/DataTables/ShopPrice.txt` - 视需要调整 `Assets/GameMain/DataTables/ShopPrice.txt`
- 具体拆分: - 具体拆分:
- 抽出统一价格解析入口:基础买价、出售价、塔出售加成、耐久折价都走同一公式服务。 - 抽出统一价格解析入口:商店买价、背包组件出售价、背包防御塔出售价、塔出售加成、耐久折价都走同一公式服务。
- 明确“组件价格”和“整塔价格”关系,不能在 UI 里临时把三组件价格相加后再乘一个魔法数。 - 明确“组件价格”和“整塔价格”关系,不能在 UI 里临时把三组件价格相加后再乘一个魔法数。
- 明确耐久折价规则:按当前耐久比例折价,还是按损坏阈值分段折价;先写死规则文档,再落代码。 - 明确耐久折价规则:按当前耐久比例折价,还是按损坏阈值分段折价;先写死规则文档,再落代码。
- 如果 `ShopPrice.txt` 只够表达品质区间,新增字段或新表承载倍率,不要继续在代码里硬编码。 - 如果 `ShopPrice.txt` 只够表达品质区间,新增字段或新表承载倍率,不要继续在代码里硬编码。