补全部分 EventComponent 设计
This commit is contained in:
parent
c76fd85a2c
commit
dc2aa59d58
|
|
@ -1,6 +1,6 @@
|
|||
# Id 列1 Title Description Options
|
||||
# int string string string
|
||||
# 事件编号 策划备注 事件题目 事件描述 事件选项
|
||||
1 赌马 一名商人邀请你下注。赢了就能赚一笔。 [{"optionText":"下注 100(金)- 稳健:70% 赢 150","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":150}}],"probability":0.7},{"optionText":"下注 100(金)- 激进:30% 赢 250","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":250}}],"probability":0.3}]
|
||||
2 工匠的熔炉 工匠以金币交换防御塔组件。 [{"optionText":"交出 3 个白色组件,获得 50 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":3,"Rarity":"White"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":3,"Rarity":"White"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":50}}]},{"optionText":"交出 2 个绿色组件,获得 70 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":2,"Rarity":"Green"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":2,"Rarity":"Green"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":70}}]},{"optionText":"交出 1 个蓝色组件,获得 80 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":1,"Rarity":"Blue"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":1,"Rarity":"Blue"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":80}}]},{"optionText":"拒绝","rewardEffects":[]}]
|
||||
3 代价与回报 某种黑暗力量向你索取代价。 [{"optionText":"展示你的防御塔","requirements":[{"type":"TowerCountAtLeast","param":{"Count":2}}],"rewardEffects":[{"type":"AddGold","param":{"Count":50}}]},{"optionText":"离开","rewardEffects":[]}]
|
||||
# Id 列1 Title Description Option1 Option2 Option3 Option4
|
||||
# int string string string string string string
|
||||
# 事件编号 策划备注 事件题目 事件描述 选项 1 选项 2 选项 3 选项 4
|
||||
1 赌马 一名商人邀请你下注。赢了就能赚一笔。 {"optionText":"下注 100(金)- 稳健:70% 赢 150","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":150}}],"probability":0.7} {"optionText":"下注 100(金)- 激进:30% 赢 250","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":250}}],"probability":0.3}
|
||||
2 工匠的熔炉 工匠愿意熔炼你的旧组件,换给你一件更有希望的新货。 {"optionText":"交出 2 个白色组件,获得 1 个白色或绿色组件","requirements":[{"type":"CompCountAtLeast","param":{"Count":2,"Rarity":"White"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":2,"Rarity":"White"}}],"rewardEffects":[{"type":"AddRandomComps","param":{"Count":1,"MinRarity":"White","MaxRarity":"Green"}}]} {"optionText":"交出 2 个绿色组件,获得 1 个绿色或蓝色组件","requirements":[{"type":"CompCountAtLeast","param":{"Count":2,"Rarity":"Green"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":2,"Rarity":"Green"}}],"rewardEffects":[{"type":"AddRandomComps","param":{"Count":1,"MinRarity":"Green","MaxRarity":"Blue"}}]} {"optionText":"交出 2 个蓝色组件,获得 1 个蓝色或紫色组件","requirements":[{"type":"CompCountAtLeast","param":{"Count":2,"Rarity":"Blue"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":2,"Rarity":"Blue"}}],"rewardEffects":[{"type":"AddRandomComps","param":{"Count":1,"MinRarity":"Blue","MaxRarity":"Purple"}}]} {"optionText":"拒绝","requirements":[],"rewardEffects":[]}
|
||||
3 代价与回报 某种黑暗力量承诺给你金币,但它索取的代价会落在你的一座防御塔上。 {"optionText":"让黑暗力量抽取一座防御塔 20 点耐久,获得 50 金币","requirements":[{"type":"TowerCountAtLeast","param":{"Count":1}}],"costEffects":[{"type":"DamageRandomTowersEndurance","param":{"Count":1,"Amount":20}}],"rewardEffects":[{"type":"AddGold","param":{"Count":50}}]} {"optionText":"离开","requirements":[],"rewardEffects":[]}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace GeometryTD.CustomComponent
|
|||
public class EventNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
private RunNodeExecutionContext _activeContext;
|
||||
private EventItem _activeEvent;
|
||||
|
||||
private readonly List<EventItem> _eventItems = new List<EventItem>();
|
||||
|
||||
|
|
@ -64,9 +65,6 @@ namespace GeometryTD.CustomComponent
|
|||
return;
|
||||
}
|
||||
|
||||
int index = Random.Range(0, _eventItems.Count);
|
||||
EventItem randomEvent = _eventItems[index];
|
||||
|
||||
if (_eventFormUseCase == null)
|
||||
{
|
||||
Log.Warning("EventNodeComponent StartEvent failed. Event form is not initialized.");
|
||||
|
|
@ -74,7 +72,14 @@ namespace GeometryTD.CustomComponent
|
|||
}
|
||||
|
||||
_activeContext = context != null ? context.Clone() : null;
|
||||
_eventFormUseCase.SetCurrentEvent(randomEvent);
|
||||
_activeEvent = SelectActiveEvent(_activeContext);
|
||||
if (_activeEvent == null)
|
||||
{
|
||||
Log.Warning("EventNodeComponent StartEvent failed. No active event could be resolved.");
|
||||
return;
|
||||
}
|
||||
|
||||
_eventFormUseCase.BindEvent(_activeEvent, _activeContext);
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.EventForm);
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
|
|
@ -107,6 +112,8 @@ namespace GeometryTD.CustomComponent
|
|||
private void ClearActiveNodeContext()
|
||||
{
|
||||
_activeContext = null;
|
||||
_activeEvent = null;
|
||||
_eventFormUseCase?.Clear();
|
||||
}
|
||||
|
||||
private static EventOption[] ParseOptions(string optionsRaw)
|
||||
|
|
@ -200,5 +207,34 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
return effects.ToArray();
|
||||
}
|
||||
|
||||
private EventItem SelectActiveEvent(RunNodeExecutionContext context)
|
||||
{
|
||||
if (_eventItems.Count <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
int randomIndex = Random.Range(0, _eventItems.Count);
|
||||
return _eventItems[randomIndex];
|
||||
}
|
||||
|
||||
System.Random random = new System.Random(BuildSelectionSeed(context));
|
||||
return _eventItems[random.Next(0, _eventItems.Count)];
|
||||
}
|
||||
|
||||
private static int BuildSelectionSeed(RunNodeExecutionContext context)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int seed = 17;
|
||||
seed = seed * 31 + context.RunSeed;
|
||||
seed = seed * 31 + context.SequenceIndex;
|
||||
seed = seed * 31 + context.NodeId;
|
||||
return seed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,18 +102,78 @@ namespace GeometryTD.CustomComponent
|
|||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildEventRewardComponents(
|
||||
int count,
|
||||
RarityType rarity,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex)
|
||||
{
|
||||
return BuildEventRewardComponents(
|
||||
count,
|
||||
rarity,
|
||||
rarity,
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
eventId,
|
||||
optionIndex,
|
||||
effectIndex);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildEventRewardComponents(
|
||||
int count,
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex)
|
||||
{
|
||||
EnsureComponentTables();
|
||||
|
||||
int resolvedCount = Mathf.Max(0, count);
|
||||
if (resolvedCount <= 0)
|
||||
{
|
||||
return System.Array.Empty<TowerCompItemData>();
|
||||
}
|
||||
|
||||
RarityType normalizedMinRarity = InventoryRarityRuleService.NormalizeComponentRarity(minRarity);
|
||||
RarityType normalizedMaxRarity = InventoryRarityRuleService.NormalizeComponentRarity(maxRarity);
|
||||
if (normalizedMinRarity > normalizedMaxRarity)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
$"Event reward rarity range is invalid: {normalizedMinRarity} > {normalizedMaxRarity}.");
|
||||
}
|
||||
|
||||
List<TowerCompItemData> result = new List<TowerCompItemData>(resolvedCount);
|
||||
for (int i = 0; i < resolvedCount; i++)
|
||||
{
|
||||
InventoryGenerationRandomContext randomContext = CreateEventRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
eventId,
|
||||
optionIndex,
|
||||
effectIndex,
|
||||
i);
|
||||
Random random = randomContext.CreateRandom();
|
||||
result.Add(BuildRandomEventComponentItem(normalizedMinRarity, normalizedMaxRarity, randomContext, random));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void EnsureShopTables()
|
||||
{
|
||||
_shopPriceTable ??= GameEntry.DataTable.GetDataTable<DRShopPrice>();
|
||||
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
||||
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||
EnsureComponentTables();
|
||||
|
||||
if (_shopPriceTable == null || _muzzleCompTable == null || _bearingCompTable == null ||
|
||||
_baseCompTable == null)
|
||||
if (_shopPriceTable == null)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires ShopPrice, MuzzleComp, BearingComp, and BaseComp data tables.");
|
||||
"InventoryGenerationComponent requires ShopPrice data table.");
|
||||
}
|
||||
|
||||
if (_shopPriceRows.Count > 0)
|
||||
|
|
@ -155,14 +215,12 @@ namespace GeometryTD.CustomComponent
|
|||
private void EnsureDropTables()
|
||||
{
|
||||
_dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
|
||||
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
||||
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||
EnsureComponentTables();
|
||||
|
||||
if (_dropPoolTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
|
||||
if (_dropPoolTable == null)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires OutGameDropPool, MuzzleComp, BearingComp, and BaseComp data tables.");
|
||||
"InventoryGenerationComponent requires OutGameDropPool data table.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,5 +282,118 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
return droppedItem;
|
||||
}
|
||||
|
||||
private void EnsureComponentTables()
|
||||
{
|
||||
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
||||
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||
|
||||
if (_muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires MuzzleComp, BearingComp, and BaseComp data tables.");
|
||||
}
|
||||
}
|
||||
|
||||
private TowerCompItemData BuildRandomEventComponentItem(
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
RarityType rarity = ResolveEventRewardRarity(minRarity, maxRarity, random);
|
||||
int slotRoll = random.Next(0, 3);
|
||||
return slotRoll switch
|
||||
{
|
||||
0 => BuildRandomEventMuzzleItem(rarity, randomContext, random),
|
||||
1 => BuildRandomEventBearingItem(rarity, randomContext, random),
|
||||
_ => BuildRandomEventBaseItem(rarity, randomContext, random)
|
||||
};
|
||||
}
|
||||
|
||||
private static RarityType ResolveEventRewardRarity(
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
Random random)
|
||||
{
|
||||
if (minRarity >= maxRarity)
|
||||
{
|
||||
return minRarity;
|
||||
}
|
||||
|
||||
int rarityValue = random.Next((int)minRarity, (int)maxRarity + 1);
|
||||
return (RarityType)rarityValue;
|
||||
}
|
||||
|
||||
private MuzzleCompItemData BuildRandomEventMuzzleItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
|
||||
DRMuzzleComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateMuzzle(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BearingCompItemData BuildRandomEventBearingItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
|
||||
DRBearingComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateBearing(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BaseCompItemData BuildRandomEventBaseItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
|
||||
DRBaseComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateBase(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private static InventoryGenerationRandomContext CreateEventRandomContext(
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex,
|
||||
int itemIndex)
|
||||
{
|
||||
return new InventoryGenerationRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
InventoryTagSourceType.Event,
|
||||
BuildEventLocalOrdinal(eventId, optionIndex, effectIndex, itemIndex));
|
||||
}
|
||||
|
||||
private static int BuildEventLocalOrdinal(int eventId, int optionIndex, int effectIndex, int itemIndex)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int value = 17;
|
||||
value = value * 31 + eventId;
|
||||
value = value * 31 + optionIndex;
|
||||
value = value * 31 + effectIndex;
|
||||
value = value * 31 + itemIndex;
|
||||
return value & int.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ namespace GeometryTD.Definition
|
|||
{
|
||||
InventoryTagSourceType.Shop => InventoryTagRandomContext.CreateShop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
InventoryTagSourceType.Event => InventoryTagRandomContext.CreateEvent(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
_ => InventoryTagRandomContext.CreateDrop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,14 +35,27 @@ namespace GeometryTD.CustomComponent
|
|||
}
|
||||
|
||||
public int ReduceTowerEndurance(IReadOnlyList<long> towerInstanceIds, float enduranceLoss)
|
||||
{
|
||||
return InventoryTowerEnduranceUtility.ReduceTowerEndurance(
|
||||
_queryModel.Inventory,
|
||||
towerInstanceIds,
|
||||
enduranceLoss);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InventoryTowerEnduranceUtility
|
||||
{
|
||||
public static int ReduceTowerEndurance(
|
||||
BackpackInventoryData inventory,
|
||||
IReadOnlyList<long> towerInstanceIds,
|
||||
float enduranceLoss)
|
||||
{
|
||||
float resolvedLoss = Mathf.Max(0f, enduranceLoss);
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (resolvedLoss <= 0f ||
|
||||
if (inventory?.Towers == null ||
|
||||
inventory.Towers.Count <= 0 ||
|
||||
resolvedLoss <= 0f ||
|
||||
towerInstanceIds == null ||
|
||||
towerInstanceIds.Count <= 0 ||
|
||||
inventory.Towers == null ||
|
||||
inventory.Towers.Count <= 0)
|
||||
towerInstanceIds.Count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using UnityGameFramework.Runtime;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
|
|
@ -30,6 +31,14 @@ namespace GeometryTD.DataTable
|
|||
/// <remarks>原始字符串(如 JSON 文本),不在此处做解析。</remarks>
|
||||
public string OptionsRaw { get; private set; }
|
||||
|
||||
public string Option1Raw { get; private set; }
|
||||
|
||||
public string Option2Raw { get; private set; }
|
||||
|
||||
public string Option3Raw { get; private set; }
|
||||
|
||||
public string Option4Raw { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
|
|
@ -44,9 +53,30 @@ namespace GeometryTD.DataTable
|
|||
index++;
|
||||
Title = columnStrings[index++];
|
||||
Description = columnStrings[index++];
|
||||
OptionsRaw = columnStrings[index++];
|
||||
Option1Raw = columnStrings[index++];
|
||||
Option2Raw = columnStrings[index++];
|
||||
Option3Raw = columnStrings[index++];
|
||||
Option4Raw = columnStrings[index++];
|
||||
OptionsRaw = BuildOptionsRaw(Option1Raw, Option2Raw, Option3Raw, Option4Raw);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildOptionsRaw(params string[] optionColumns)
|
||||
{
|
||||
JArray array = new JArray();
|
||||
for (int i = 0; i < optionColumns.Length; i++)
|
||||
{
|
||||
string optionRaw = optionColumns[i];
|
||||
if (string.IsNullOrWhiteSpace(optionRaw))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
array.Add(JObject.Parse(optionRaw));
|
||||
}
|
||||
|
||||
return array.ToString(Newtonsoft.Json.Formatting.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,5 +6,6 @@ namespace GeometryTD.Definition
|
|||
Shop = 2,
|
||||
Drop = 3,
|
||||
Reward = 4,
|
||||
Event = 5,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,31 @@ namespace GeometryTD.Definition
|
|||
{
|
||||
public int Count;
|
||||
public RarityType Rarity;
|
||||
public RarityType MinRarity;
|
||||
public RarityType MaxRarity;
|
||||
|
||||
public AddRandomCompsParam(int count, RarityType rarity)
|
||||
{
|
||||
Count = count;
|
||||
Rarity = rarity;
|
||||
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
|
||||
Rarity = normalizedRarity;
|
||||
MinRarity = normalizedRarity;
|
||||
MaxRarity = normalizedRarity;
|
||||
}
|
||||
|
||||
public AddRandomCompsParam(int count, RarityType minRarity, RarityType maxRarity)
|
||||
{
|
||||
Count = count;
|
||||
MinRarity = InventoryRarityRuleService.NormalizeComponentRarity(minRarity);
|
||||
MaxRarity = InventoryRarityRuleService.NormalizeComponentRarity(maxRarity);
|
||||
if (MinRarity > MaxRarity)
|
||||
{
|
||||
throw new System.ArgumentOutOfRangeException(
|
||||
nameof(minRarity),
|
||||
$"AddRandomComps rarity range is invalid: {MinRarity} > {MaxRarity}.");
|
||||
}
|
||||
|
||||
Rarity = MinRarity == MaxRarity ? MinRarity : RarityType.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,490 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomComponent;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.Definition
|
||||
{
|
||||
public sealed class EventOptionExecutor
|
||||
{
|
||||
public EventOptionAvailability EvaluateOption(EventOption option, BackpackInventoryData inventory)
|
||||
{
|
||||
if (option == null)
|
||||
{
|
||||
return EventOptionAvailability.Blocked("选项配置无效");
|
||||
}
|
||||
|
||||
EventRequirementBase[] requirements = option.Requirements ?? Array.Empty<EventRequirementBase>();
|
||||
for (int i = 0; i < requirements.Length; i++)
|
||||
{
|
||||
EventRequirementBase requirement = requirements[i];
|
||||
if (requirement == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsRequirementSatisfied(requirement, inventory))
|
||||
{
|
||||
return EventOptionAvailability.Blocked(BuildBlockedReason(requirement));
|
||||
}
|
||||
}
|
||||
|
||||
return EventOptionAvailability.Selectable();
|
||||
}
|
||||
|
||||
public EventOptionExecutionResult Execute(
|
||||
EventItem eventItem,
|
||||
int optionIndex,
|
||||
RunNodeExecutionContext context,
|
||||
BackpackInventoryData workingInventory)
|
||||
{
|
||||
if (eventItem == null || workingInventory == null)
|
||||
{
|
||||
return EventOptionExecutionResult.Rejected("事件数据无效");
|
||||
}
|
||||
|
||||
if (optionIndex < 0 || optionIndex >= eventItem.Options.Length)
|
||||
{
|
||||
return EventOptionExecutionResult.Rejected("事件选项索引无效");
|
||||
}
|
||||
|
||||
EventOption option = eventItem.Options[optionIndex];
|
||||
EventOptionAvailability availability = EvaluateOption(option, workingInventory);
|
||||
if (!availability.IsSelectable)
|
||||
{
|
||||
return EventOptionExecutionResult.Rejected(availability.BlockedReason);
|
||||
}
|
||||
|
||||
ApplyEffects(option.CostEffects, eventItem, optionIndex, context, workingInventory, isRewardPhase: false);
|
||||
|
||||
bool isProbabilitySuccess = RollProbability(eventItem.Id, optionIndex, context, option.Probability);
|
||||
if (isProbabilitySuccess)
|
||||
{
|
||||
ApplyEffects(option.RewardEffects, eventItem, optionIndex, context, workingInventory, isRewardPhase: true);
|
||||
}
|
||||
|
||||
return EventOptionExecutionResult.Accepted(isProbabilitySuccess);
|
||||
}
|
||||
|
||||
private void ApplyEffects(
|
||||
EventEffectBase[] effects,
|
||||
EventItem eventItem,
|
||||
int optionIndex,
|
||||
RunNodeExecutionContext context,
|
||||
BackpackInventoryData workingInventory,
|
||||
bool isRewardPhase)
|
||||
{
|
||||
if (effects == null || effects.Length <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < effects.Length; i++)
|
||||
{
|
||||
EventEffectBase effect = effects[i];
|
||||
if (effect == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (effect.EffectType)
|
||||
{
|
||||
case EventEffectType.AddGold:
|
||||
ApplyAddGoldEffect((AddGoldParam)effect.Param, workingInventory);
|
||||
break;
|
||||
case EventEffectType.RemoveRandomComps:
|
||||
ApplyRemoveRandomComponentsEffect(
|
||||
(RemoveRandomCompsParam)effect.Param,
|
||||
eventItem.Id,
|
||||
optionIndex,
|
||||
context,
|
||||
workingInventory,
|
||||
i);
|
||||
break;
|
||||
case EventEffectType.AddRandomComps:
|
||||
ApplyAddRandomComponentsEffect(
|
||||
(AddRandomCompsParam)effect.Param,
|
||||
eventItem.Id,
|
||||
optionIndex,
|
||||
context,
|
||||
workingInventory,
|
||||
i);
|
||||
break;
|
||||
case EventEffectType.DamageRandomTowersEndurance:
|
||||
ApplyDamageRandomTowerEnduranceEffect(
|
||||
(DamageRandomTowerEnduranceParam)effect.Param,
|
||||
eventItem.Id,
|
||||
optionIndex,
|
||||
context,
|
||||
workingInventory,
|
||||
i);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported event effect at runtime: {effect.EffectType} (rewardPhase={isRewardPhase}).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyAddGoldEffect(AddGoldParam param, BackpackInventoryData workingInventory)
|
||||
{
|
||||
int nextGold = workingInventory.Gold + (param?.Count ?? 0);
|
||||
if (nextGold < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Event gold effect would reduce gold below zero.");
|
||||
}
|
||||
|
||||
workingInventory.Gold = nextGold;
|
||||
}
|
||||
|
||||
private static void ApplyRemoveRandomComponentsEffect(
|
||||
RemoveRandomCompsParam param,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
RunNodeExecutionContext context,
|
||||
BackpackInventoryData workingInventory,
|
||||
int effectIndex)
|
||||
{
|
||||
int removeCount = Mathf.Max(0, param?.Count ?? 0);
|
||||
if (removeCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<TowerCompItemData> candidates = CollectLooseComponents(workingInventory, param.Rarity);
|
||||
if (candidates.Count < removeCount)
|
||||
{
|
||||
throw new InvalidOperationException("Event component removal effect does not have enough candidates.");
|
||||
}
|
||||
|
||||
System.Random random = CreateRandom(context, eventId, optionIndex, effectIndex, 101);
|
||||
Shuffle(candidates, random);
|
||||
for (int i = 0; i < removeCount; i++)
|
||||
{
|
||||
RemoveComponentByInstanceId(workingInventory, candidates[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyAddRandomComponentsEffect(
|
||||
AddRandomCompsParam param,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
RunNodeExecutionContext context,
|
||||
BackpackInventoryData workingInventory,
|
||||
int effectIndex)
|
||||
{
|
||||
int addCount = Mathf.Max(0, param?.Count ?? 0);
|
||||
if (addCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameEntry.InventoryGeneration == null)
|
||||
{
|
||||
throw new InvalidOperationException("Event component generation requires InventoryGenerationComponent.");
|
||||
}
|
||||
|
||||
IReadOnlyList<TowerCompItemData> generatedComponents = GameEntry.InventoryGeneration.BuildEventRewardComponents(
|
||||
addCount,
|
||||
param.MinRarity,
|
||||
param.MaxRarity,
|
||||
context?.RunSeed ?? 0,
|
||||
context?.SequenceIndex ?? -1,
|
||||
eventId,
|
||||
optionIndex,
|
||||
effectIndex);
|
||||
for (int i = 0; i < generatedComponents.Count; i++)
|
||||
{
|
||||
AddComponentToInventory(workingInventory, generatedComponents[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyDamageRandomTowerEnduranceEffect(
|
||||
DamageRandomTowerEnduranceParam param,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
RunNodeExecutionContext context,
|
||||
BackpackInventoryData workingInventory,
|
||||
int effectIndex)
|
||||
{
|
||||
int towerCount = Mathf.Max(0, param?.Count ?? 0);
|
||||
float enduranceLoss = Mathf.Max(0, param?.Amount ?? 0);
|
||||
if (towerCount <= 0 || enduranceLoss <= 0f || workingInventory.Towers == null || workingInventory.Towers.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<long> candidateTowerIds = new List<long>(workingInventory.Towers.Count);
|
||||
for (int i = 0; i < workingInventory.Towers.Count; i++)
|
||||
{
|
||||
TowerItemData tower = workingInventory.Towers[i];
|
||||
if (tower != null && tower.InstanceId > 0)
|
||||
{
|
||||
candidateTowerIds.Add(tower.InstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidateTowerIds.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
System.Random random = CreateRandom(context, eventId, optionIndex, effectIndex, 211);
|
||||
Shuffle(candidateTowerIds, random);
|
||||
int resolvedCount = Mathf.Min(towerCount, candidateTowerIds.Count);
|
||||
List<long> selectedTowerIds = candidateTowerIds.GetRange(0, resolvedCount);
|
||||
InventoryTowerEnduranceUtility.ReduceTowerEndurance(workingInventory, selectedTowerIds, enduranceLoss);
|
||||
}
|
||||
|
||||
private static bool IsRequirementSatisfied(EventRequirementBase requirement, BackpackInventoryData inventory)
|
||||
{
|
||||
switch (requirement.RequirementType)
|
||||
{
|
||||
case EventRequirementType.GoldAtLeast:
|
||||
return (inventory?.Gold ?? 0) >= ((GoldAtLeastParam)requirement.Param).Gold;
|
||||
case EventRequirementType.CompCountAtLeast:
|
||||
CompCountAtLeastParam compParam = (CompCountAtLeastParam)requirement.Param;
|
||||
return CollectLooseComponents(inventory, compParam.Rarity).Count >= compParam.Count;
|
||||
case EventRequirementType.TowerCountAtLeast:
|
||||
return (inventory?.Towers?.Count ?? 0) >= ((TowerCountAtLeastParam)requirement.Param).Count;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported event requirement at runtime: {requirement.RequirementType}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildBlockedReason(EventRequirementBase requirement)
|
||||
{
|
||||
switch (requirement.RequirementType)
|
||||
{
|
||||
case EventRequirementType.GoldAtLeast:
|
||||
return $"需要至少 {((GoldAtLeastParam)requirement.Param).Gold} 金币";
|
||||
case EventRequirementType.CompCountAtLeast:
|
||||
CompCountAtLeastParam compParam = (CompCountAtLeastParam)requirement.Param;
|
||||
return $"需要至少 {compParam.Count} 个未装配的{GetRarityText(compParam.Rarity)}组件";
|
||||
case EventRequirementType.TowerCountAtLeast:
|
||||
return $"需要至少 {((TowerCountAtLeastParam)requirement.Param).Count} 座防御塔";
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported event requirement at runtime: {requirement.RequirementType}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RollProbability(int eventId, int optionIndex, RunNodeExecutionContext context, float probability)
|
||||
{
|
||||
float clampedProbability = Mathf.Clamp01(probability);
|
||||
if (clampedProbability >= 1f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (clampedProbability <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
System.Random random = CreateRandom(context, eventId, optionIndex, 0, 17);
|
||||
return random.NextDouble() <= clampedProbability;
|
||||
}
|
||||
|
||||
private static System.Random CreateRandom(
|
||||
RunNodeExecutionContext context,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex,
|
||||
int salt)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int seed = 17;
|
||||
seed = seed * 31 + (context?.RunSeed ?? 0);
|
||||
seed = seed * 31 + (context?.SequenceIndex ?? -1);
|
||||
seed = seed * 31 + eventId;
|
||||
seed = seed * 31 + optionIndex;
|
||||
seed = seed * 31 + effectIndex;
|
||||
seed = seed * 31 + salt;
|
||||
return new System.Random(seed);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<TowerCompItemData> CollectLooseComponents(
|
||||
BackpackInventoryData inventory,
|
||||
RarityType rarity)
|
||||
{
|
||||
List<TowerCompItemData> result = new List<TowerCompItemData>();
|
||||
if (inventory == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
|
||||
CollectFromList(result, inventory.MuzzleComponents, normalizedRarity);
|
||||
CollectFromList(result, inventory.BearingComponents, normalizedRarity);
|
||||
CollectFromList(result, inventory.BaseComponents, normalizedRarity);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void CollectFromList<TComp>(
|
||||
List<TowerCompItemData> destination,
|
||||
List<TComp> source,
|
||||
RarityType rarity)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
TComp component = source[i];
|
||||
if (component == null || component.IsAssembledIntoTower)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (InventoryRarityRuleService.NormalizeComponentRarity(component.Rarity) != rarity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
destination.Add(component);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveComponentByInstanceId(BackpackInventoryData inventory, TowerCompItemData component)
|
||||
{
|
||||
switch (component)
|
||||
{
|
||||
case MuzzleCompItemData muzzleComp:
|
||||
RemoveByInstanceId(inventory.MuzzleComponents, muzzleComp.InstanceId);
|
||||
break;
|
||||
case BearingCompItemData bearingComp:
|
||||
RemoveByInstanceId(inventory.BearingComponents, bearingComp.InstanceId);
|
||||
break;
|
||||
case BaseCompItemData baseComp:
|
||||
RemoveByInstanceId(inventory.BaseComponents, baseComp.InstanceId);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unsupported component type for event removal: {component?.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddComponentToInventory(BackpackInventoryData inventory, TowerCompItemData component)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (component)
|
||||
{
|
||||
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;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unsupported component type for event addition: {component.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveByInstanceId<TComp>(List<TComp> components, long instanceId)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
if (components == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
{
|
||||
TComp component = components[i];
|
||||
if (component != null && component.InstanceId == instanceId)
|
||||
{
|
||||
components.RemoveAt(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Failed to remove component instance #{instanceId} from inventory.");
|
||||
}
|
||||
|
||||
private static void Shuffle<T>(IList<T> values, System.Random random)
|
||||
{
|
||||
for (int i = values.Count - 1; i > 0; i--)
|
||||
{
|
||||
int swapIndex = random.Next(0, i + 1);
|
||||
(values[i], values[swapIndex]) = (values[swapIndex], values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRarityText(RarityType rarity)
|
||||
{
|
||||
return InventoryRarityRuleService.NormalizeComponentRarity(rarity) switch
|
||||
{
|
||||
RarityType.White => "白色",
|
||||
RarityType.Green => "绿色",
|
||||
RarityType.Blue => "蓝色",
|
||||
RarityType.Purple => "紫色",
|
||||
RarityType.Red => "红色",
|
||||
_ => "未知"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EventOptionAvailability
|
||||
{
|
||||
private EventOptionAvailability(bool isSelectable, string blockedReason)
|
||||
{
|
||||
IsSelectable = isSelectable;
|
||||
BlockedReason = blockedReason ?? string.Empty;
|
||||
}
|
||||
|
||||
public bool IsSelectable { get; }
|
||||
|
||||
public string BlockedReason { get; }
|
||||
|
||||
public static EventOptionAvailability Selectable()
|
||||
{
|
||||
return new EventOptionAvailability(true, string.Empty);
|
||||
}
|
||||
|
||||
public static EventOptionAvailability Blocked(string blockedReason)
|
||||
{
|
||||
return new EventOptionAvailability(false, blockedReason);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EventOptionExecutionResult
|
||||
{
|
||||
private EventOptionExecutionResult(bool isAccepted, bool isProbabilitySuccess, string failureReason)
|
||||
{
|
||||
IsAccepted = isAccepted;
|
||||
IsProbabilitySuccess = isProbabilitySuccess;
|
||||
FailureReason = failureReason ?? string.Empty;
|
||||
}
|
||||
|
||||
public bool IsAccepted { get; }
|
||||
|
||||
public bool IsProbabilitySuccess { get; }
|
||||
|
||||
public string FailureReason { get; }
|
||||
|
||||
public static EventOptionExecutionResult Accepted(bool isProbabilitySuccess)
|
||||
{
|
||||
return new EventOptionExecutionResult(true, isProbabilitySuccess, string.Empty);
|
||||
}
|
||||
|
||||
public static EventOptionExecutionResult Rejected(string failureReason)
|
||||
{
|
||||
return new EventOptionExecutionResult(false, false, failureReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8e4d7125aa7b4cb9aa62cb0bf9301d5b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -53,6 +53,15 @@ namespace GeometryTD.Definition
|
|||
configId);
|
||||
}
|
||||
|
||||
public static InventoryTagRandomContext CreateEvent(int runSeed, int nodeSequenceIndex, int eventOrdinal, int configId)
|
||||
{
|
||||
return new InventoryTagRandomContext(
|
||||
runSeed,
|
||||
InventoryTagSourceType.Event,
|
||||
ComposeLocalItemInstanceId(nodeSequenceIndex, eventOrdinal),
|
||||
configId);
|
||||
}
|
||||
|
||||
public static long ComposeLocalItemInstanceId(int nodeSequenceIndex, int localOrdinal)
|
||||
{
|
||||
long normalizedSequence = Math.Max(0, nodeSequenceIndex) + 1L;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,21 @@ namespace GeometryTD.Factory
|
|||
case EventEffectType.AddRandomComps:
|
||||
{
|
||||
int count = GetInt(param, "Count");
|
||||
bool hasMinRarity = TryGetString(param, "MinRarity", out string minRarityRaw);
|
||||
bool hasMaxRarity = TryGetString(param, "MaxRarity", out string maxRarityRaw);
|
||||
if (hasMinRarity || hasMaxRarity)
|
||||
{
|
||||
if (!hasMinRarity || !hasMaxRarity)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"AddRandomComps requires both MinRarity and MaxRarity when using ranged rarity config.");
|
||||
}
|
||||
|
||||
RarityType minRarity = EnumUtility<RarityType>.Get(minRarityRaw);
|
||||
RarityType maxRarity = EnumUtility<RarityType>.Get(maxRarityRaw);
|
||||
return new AddRandomCompsEffect(new AddRandomCompsParam(count, minRarity, maxRarity), probability);
|
||||
}
|
||||
|
||||
RarityType rarity = EnumUtility<RarityType>.Get(GetString(param, "Rarity"));
|
||||
return new AddRandomCompsEffect(new AddRandomCompsParam(count, rarity), probability);
|
||||
}
|
||||
|
|
@ -73,5 +88,17 @@ namespace GeometryTD.Factory
|
|||
|
||||
return token.Value<string>();
|
||||
}
|
||||
|
||||
private static bool TryGetString(JObject param, string key, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (param == null || !param.TryGetValue(key, out JToken token) || token.Type == JTokenType.Null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = token.Value<string>();
|
||||
return !string.IsNullOrWhiteSpace(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,5 +4,7 @@ namespace GeometryTD.UI
|
|||
{
|
||||
public int OptionIndex;
|
||||
public string OptionText;
|
||||
public bool IsSelectable;
|
||||
public string BlockedReason;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace GeometryTD.UI
|
|||
{
|
||||
public class RepoFormContext : UIContext
|
||||
{
|
||||
public string GoldText;
|
||||
public CombineAreaContext CombineAreaContext;
|
||||
public CompAreaContext CompAreaContext;
|
||||
public ParticipantAreaContext ParticipantAreaContext;
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ namespace GeometryTD.UI
|
|||
|
||||
return new RepoFormContext
|
||||
{
|
||||
GoldText = $"金币: {rawData.Inventory.Gold}",
|
||||
CombineAreaContext = new CombineAreaContext(),
|
||||
CompAreaContext = new CompAreaContext
|
||||
{
|
||||
|
|
@ -322,6 +323,7 @@ namespace GeometryTD.UI
|
|||
return;
|
||||
}
|
||||
|
||||
Form.RefreshGoldText(latestContext.GoldText);
|
||||
Form.RefreshParticipantArea(latestContext.ParticipantAreaContext);
|
||||
ApplyParticipantSelection();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ namespace GeometryTD.UI
|
|||
|
||||
private static EventFormContext BuildContext(EventFormRawData rawData)
|
||||
{
|
||||
if (rawData?.EventItem == null)
|
||||
if (rawData == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
@ -82,24 +82,30 @@ namespace GeometryTD.UI
|
|||
for (int i = 0; i < options.Length; i++)
|
||||
{
|
||||
string optionText = string.Empty;
|
||||
if (rawData.EventItem.Options != null && i < rawData.EventItem.Options.Length &&
|
||||
rawData.EventItem.Options[i] != null)
|
||||
bool isSelectable = false;
|
||||
string blockedReason = string.Empty;
|
||||
if (rawData.OptionItems != null && i < rawData.OptionItems.Length &&
|
||||
rawData.OptionItems[i] != null)
|
||||
{
|
||||
optionText = rawData.EventItem.Options[i].OptionText;
|
||||
optionText = rawData.OptionItems[i].OptionText;
|
||||
isSelectable = rawData.OptionItems[i].IsSelectable;
|
||||
blockedReason = rawData.OptionItems[i].BlockedReason;
|
||||
}
|
||||
|
||||
options[i] = new EventOptionItemContext
|
||||
{
|
||||
OptionIndex = i,
|
||||
OptionText = optionText
|
||||
OptionText = optionText,
|
||||
IsSelectable = isSelectable,
|
||||
BlockedReason = blockedReason
|
||||
};
|
||||
}
|
||||
|
||||
return new EventFormContext
|
||||
{
|
||||
EventId = rawData.EventItem.Id,
|
||||
Title = rawData.EventItem.Title,
|
||||
Description = rawData.EventItem.Description,
|
||||
EventId = rawData.EventId,
|
||||
Title = rawData.Title,
|
||||
Description = rawData.Description,
|
||||
OptionItems = options
|
||||
};
|
||||
}
|
||||
|
|
@ -117,7 +123,7 @@ namespace GeometryTD.UI
|
|||
return;
|
||||
}
|
||||
|
||||
m_UseCase.SelectOption(args.SelectedItemId);
|
||||
m_UseCase.TrySelectOption(args.SelectedItemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -245,8 +245,8 @@ namespace GeometryTD.UI
|
|||
RepoFormContext latestContext = BuildContext(latestRawData);
|
||||
if (latestContext != null)
|
||||
{
|
||||
Form.RefreshUI(latestContext);
|
||||
ApplyParticipantSelection();
|
||||
SetContext(latestContext);
|
||||
RefreshCurrentUI();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
{
|
||||
public class EventFormRawData
|
||||
{
|
||||
public EventItem EventItem;
|
||||
public int EventId;
|
||||
public string Title;
|
||||
public string Description;
|
||||
public EventOptionRawData[] OptionItems;
|
||||
}
|
||||
|
||||
public sealed class EventOptionRawData
|
||||
{
|
||||
public int OptionIndex;
|
||||
public string OptionText;
|
||||
public bool IsSelectable;
|
||||
public string BlockedReason;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using GeometryTD.CustomComponent;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
|
|
@ -8,10 +7,21 @@ namespace GeometryTD.UI
|
|||
public class EventFormUseCase : IUIUseCase
|
||||
{
|
||||
private EventItem _currentEvent;
|
||||
private RunNodeExecutionContext _currentContext;
|
||||
private readonly EventOptionExecutor _executor = new EventOptionExecutor();
|
||||
|
||||
public void SetCurrentEvent(EventItem eventItem)
|
||||
public void BindEvent(
|
||||
EventItem eventItem,
|
||||
RunNodeExecutionContext context)
|
||||
{
|
||||
_currentEvent = eventItem;
|
||||
_currentContext = context != null ? context.Clone() : null;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_currentEvent = null;
|
||||
_currentContext = null;
|
||||
}
|
||||
|
||||
public EventFormRawData CreateInitialModel()
|
||||
|
|
@ -23,26 +33,74 @@ namespace GeometryTD.UI
|
|||
|
||||
return new EventFormRawData
|
||||
{
|
||||
EventItem = _currentEvent
|
||||
EventId = _currentEvent.Id,
|
||||
Title = _currentEvent.Title,
|
||||
Description = _currentEvent.Description,
|
||||
OptionItems = BuildOptionItems()
|
||||
};
|
||||
}
|
||||
|
||||
public EventOption SelectOption(int optionIndex)
|
||||
public bool TrySelectOption(int optionIndex)
|
||||
{
|
||||
if (_currentEvent == null || _currentEvent.Options == null)
|
||||
{
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (optionIndex < 0 || optionIndex >= _currentEvent.Options.Length)
|
||||
{
|
||||
Log.Warning("EventFormUseCase.SelectOption() option index is invalid: {0}", optionIndex);
|
||||
return null;
|
||||
Log.Warning("EventFormUseCase.TrySelectOption() option index is invalid: {0}", optionIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData workingInventory = GameEntry.PlayerInventory != null
|
||||
? GameEntry.PlayerInventory.GetInventorySnapshot()
|
||||
: new BackpackInventoryData();
|
||||
EventOptionExecutionResult executionResult = _executor.Execute(
|
||||
_currentEvent,
|
||||
optionIndex,
|
||||
_currentContext,
|
||||
workingInventory);
|
||||
if (!executionResult.IsAccepted)
|
||||
{
|
||||
Log.Warning(
|
||||
"EventFormUseCase.TrySelectOption() rejected. EventId={0}, OptionIndex={1}, Reason={2}",
|
||||
_currentEvent.Id,
|
||||
optionIndex,
|
||||
executionResult.FailureReason);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GameEntry.PlayerInventory != null)
|
||||
{
|
||||
GameEntry.PlayerInventory.ReplaceInventorySnapshot(workingInventory);
|
||||
}
|
||||
|
||||
// TODO: 执行 Requirements 校验、CostEffects/RewardEffects 结算与后续节点推进。
|
||||
GameEntry.EventNode.EndEvent();
|
||||
return _currentEvent.Options[optionIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
private EventOptionRawData[] BuildOptionItems()
|
||||
{
|
||||
EventOption[] options = _currentEvent.Options ?? System.Array.Empty<EventOption>();
|
||||
EventOptionRawData[] result = new EventOptionRawData[options.Length];
|
||||
BackpackInventoryData inventory = GameEntry.PlayerInventory != null
|
||||
? GameEntry.PlayerInventory.GetInventorySnapshot()
|
||||
: new BackpackInventoryData();
|
||||
for (int i = 0; i < options.Length; i++)
|
||||
{
|
||||
EventOption option = options[i];
|
||||
EventOptionAvailability availability = _executor.EvaluateOption(option, inventory);
|
||||
result[i] = new EventOptionRawData
|
||||
{
|
||||
OptionIndex = i,
|
||||
OptionText = option?.OptionText ?? string.Empty,
|
||||
IsSelectable = availability.IsSelectable,
|
||||
BlockedReason = availability.BlockedReason
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,17 @@ namespace GeometryTD.UI
|
|||
{
|
||||
[SerializeField] private TMP_Text _optionText;
|
||||
|
||||
private static readonly Color SelectableTextColor = Color.white;
|
||||
private static readonly Color BlockedTextColor = new Color(0.75f, 0.75f, 0.75f, 1f);
|
||||
|
||||
private int _optionIndex;
|
||||
private bool _isSelectable;
|
||||
private CommonButton _commonButton;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_commonButton = GetComponent<CommonButton>();
|
||||
}
|
||||
|
||||
public void OnInit(EventOptionItemContext context)
|
||||
{
|
||||
|
|
@ -20,9 +30,16 @@ namespace GeometryTD.UI
|
|||
}
|
||||
|
||||
_optionIndex = context.OptionIndex;
|
||||
_isSelectable = context.IsSelectable;
|
||||
if (_optionText != null)
|
||||
{
|
||||
_optionText.text = context.OptionText ?? string.Empty;
|
||||
_optionText.text = BuildDisplayText(context);
|
||||
_optionText.color = _isSelectable ? SelectableTextColor : BlockedTextColor;
|
||||
}
|
||||
|
||||
if (_commonButton != null)
|
||||
{
|
||||
_commonButton.Interactive = _isSelectable;
|
||||
}
|
||||
|
||||
gameObject.SetActive(!string.IsNullOrEmpty(context.OptionText));
|
||||
|
|
@ -31,20 +48,38 @@ namespace GeometryTD.UI
|
|||
public void OnReset()
|
||||
{
|
||||
_optionIndex = -1;
|
||||
_isSelectable = false;
|
||||
if (_optionText != null)
|
||||
{
|
||||
_optionText.text = string.Empty;
|
||||
_optionText.color = SelectableTextColor;
|
||||
}
|
||||
|
||||
if (_commonButton != null)
|
||||
{
|
||||
_commonButton.Interactive = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnClick()
|
||||
{
|
||||
if (_optionIndex < 0)
|
||||
if (_optionIndex < 0 || !_isSelectable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Fire(this, EventOptionItemSelectedEventArgs.Create(_optionIndex));
|
||||
}
|
||||
|
||||
private static string BuildDisplayText(EventOptionItemContext context)
|
||||
{
|
||||
string optionText = context.OptionText ?? string.Empty;
|
||||
if (context.IsSelectable || string.IsNullOrEmpty(context.BlockedReason))
|
||||
{
|
||||
return optionText;
|
||||
}
|
||||
|
||||
return $"{optionText}\n<size=75%>{context.BlockedReason}</size>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using GeometryTD.CustomEvent;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
|
|
@ -12,6 +13,8 @@ namespace GeometryTD.UI
|
|||
|
||||
[SerializeField] private ParticipantArea _participantArea;
|
||||
|
||||
[SerializeField] private TMP_Text _goldText;
|
||||
|
||||
public void RefreshUI(RepoFormContext context)
|
||||
{
|
||||
if (context == null)
|
||||
|
|
@ -19,11 +22,21 @@ namespace GeometryTD.UI
|
|||
return;
|
||||
}
|
||||
|
||||
RefreshGoldText(context.GoldText);
|
||||
|
||||
_combineArea?.OnInit(context.CombineAreaContext);
|
||||
_compArea?.OnInit(context.CompAreaContext);
|
||||
_participantArea?.OnInit(context.ParticipantAreaContext);
|
||||
}
|
||||
|
||||
public void RefreshGoldText(string goldText)
|
||||
{
|
||||
if (_goldText != null)
|
||||
{
|
||||
_goldText.text = goldText ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryAssignItemToCombineArea(RepoItemContext itemContext)
|
||||
{
|
||||
if (_combineArea == null)
|
||||
|
|
@ -69,6 +82,11 @@ namespace GeometryTD.UI
|
|||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
if (_goldText != null)
|
||||
{
|
||||
_goldText.text = string.Empty;
|
||||
}
|
||||
|
||||
_combineArea?.OnReset();
|
||||
_compArea?.OnReset();
|
||||
_participantArea?.OnReset();
|
||||
|
|
|
|||
|
|
@ -130,6 +130,140 @@ MonoBehaviour:
|
|||
m_Spacing: {x: 20, y: 20}
|
||||
m_Constraint: 1
|
||||
m_ConstraintCount: 6
|
||||
--- !u!1 &3121582388666951180
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 8127279669446431800}
|
||||
- component: {fileID: 3226573329967497035}
|
||||
- component: {fileID: 661567130228843717}
|
||||
m_Layer: 5
|
||||
m_Name: GoldText
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &8127279669446431800
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3121582388666951180}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 956737910225412855}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &3226573329967497035
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3121582388666951180}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &661567130228843717
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3121582388666951180}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: 1234555
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
|
||||
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 71.6
|
||||
m_fontSizeBase: 36
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 1
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 4
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_enableWordWrapping: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 1
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &3151563931008387230
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -380,6 +514,7 @@ MonoBehaviour:
|
|||
_combineArea: {fileID: 2687805356437946940}
|
||||
_compArea: {fileID: 370202498391855143}
|
||||
_participantArea: {fileID: 6217930899804600847}
|
||||
_goldText: {fileID: 661567130228843717}
|
||||
--- !u!1 &4000410608128749255
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -971,6 +1106,7 @@ RectTransform:
|
|||
- {fileID: 437137211665287909}
|
||||
- {fileID: 4661661401541872014}
|
||||
- {fileID: 6194629833245286025}
|
||||
- {fileID: 956737910225412855}
|
||||
m_Father: {fileID: 3043356307630469178}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
|
|
@ -1091,6 +1227,42 @@ MonoBehaviour:
|
|||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &8409825260122624528
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 956737910225412855}
|
||||
m_Layer: 5
|
||||
m_Name: GoldArea
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &956737910225412855
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8409825260122624528}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 8127279669446431800}
|
||||
m_Father: {fileID: 948419798348630685}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 1, y: 1}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: -1000, y: -100}
|
||||
m_SizeDelta: {x: 400, y: 80}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1001 &19160693396753940
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using GeometryTD.Definition;
|
||||
using GeometryTD.Factory;
|
||||
using GeometryTD.DataTable;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
|
||||
|
|
@ -75,6 +76,33 @@ namespace GeometryTD.Tests.EditMode
|
|||
Assert.That(removeRandomCompEffect.Probability, Is.EqualTo(0.25f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EffectFactory_Creates_AddRandomComponents_With_Ranged_Rarity()
|
||||
{
|
||||
EventEffectBase effect = EventEffectFactory.Create(
|
||||
"AddRandomComps",
|
||||
JObject.Parse(@"{ ""Count"": 1, ""MinRarity"": ""Green"", ""MaxRarity"": ""Blue"" }"),
|
||||
0.6f);
|
||||
|
||||
Assert.That(effect, Is.TypeOf<AddRandomCompsEffect>());
|
||||
AddRandomCompsParam param = (AddRandomCompsParam)effect.Param;
|
||||
Assert.That(param.Count, Is.EqualTo(1));
|
||||
Assert.That(param.MinRarity, Is.EqualTo(RarityType.Green));
|
||||
Assert.That(param.MaxRarity, Is.EqualTo(RarityType.Blue));
|
||||
Assert.That(param.Rarity, Is.EqualTo(RarityType.None));
|
||||
Assert.That(effect.Probability, Is.EqualTo(0.6f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EffectFactory_Throws_When_Ranged_Rarity_Config_Is_Incomplete()
|
||||
{
|
||||
Assert.That(
|
||||
() => EventEffectFactory.Create(
|
||||
"AddRandomComps",
|
||||
JObject.Parse(@"{ ""Count"": 1, ""MinRarity"": ""Green"" }")),
|
||||
Throws.TypeOf<System.InvalidOperationException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EffectFactory_Creates_DamageRandomTowerEnduranceEffect_With_Expected_Params()
|
||||
{
|
||||
|
|
@ -98,5 +126,20 @@ namespace GeometryTD.Tests.EditMode
|
|||
Assert.That(EventEffectFactory.Create(string.Empty, JObject.Parse("{}")), Is.Null);
|
||||
Assert.That(EventEffectFactory.Create("UnknownEffect", JObject.Parse("{}")), Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DREvent_ParseDataRow_Builds_OptionsRaw_From_Four_Columns()
|
||||
{
|
||||
DREvent row = new DREvent();
|
||||
bool parsed = row.ParseDataRow(
|
||||
"\t1\t\t测试事件\t测试描述\t{\"optionText\":\"选项一\",\"rewardEffects\":[]}\t{\"optionText\":\"选项二\",\"rewardEffects\":[]}\t\t",
|
||||
null);
|
||||
|
||||
Assert.That(parsed, Is.True);
|
||||
JArray options = JArray.Parse(row.OptionsRaw);
|
||||
Assert.That(options.Count, Is.EqualTo(2));
|
||||
Assert.That(options[0].Value<string>("optionText"), Is.EqualTo("选项一"));
|
||||
Assert.That(options[1].Value<string>("optionText"), Is.EqualTo("选项二"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,803 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomComponent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace GeometryTD.Tests.EditMode
|
||||
{
|
||||
public sealed class EventOptionExecutorTests
|
||||
{
|
||||
private GameObject _inventoryGenerationObject;
|
||||
private InventoryGenerationComponent _originalInventoryGeneration;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
TagDefinitionRegistry.ResetToDefaults();
|
||||
TagGenerationRuleRegistry.ResetToDefaults();
|
||||
RarityTagBudgetRuleRegistry.ResetToDefaults();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
TagDefinitionRegistry.ResetToDefaults();
|
||||
TagGenerationRuleRegistry.ResetToDefaults();
|
||||
RarityTagBudgetRuleRegistry.ResetToDefaults();
|
||||
|
||||
SetStaticInventoryGeneration(_originalInventoryGeneration);
|
||||
_originalInventoryGeneration = null;
|
||||
|
||||
if (_inventoryGenerationObject != null)
|
||||
{
|
||||
Object.DestroyImmediate(_inventoryGenerationObject);
|
||||
_inventoryGenerationObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EvaluateOption_Blocks_When_Gold_Requirement_Not_Met()
|
||||
{
|
||||
EventOptionExecutor executor = new EventOptionExecutor();
|
||||
EventOption option = new EventOption(
|
||||
"下注 100",
|
||||
new EventRequirementBase[]
|
||||
{
|
||||
new GoldAtLeastRequirement(new GoldAtLeastParam(100))
|
||||
},
|
||||
Array.Empty<EventEffectBase>(),
|
||||
Array.Empty<EventEffectBase>());
|
||||
|
||||
EventOptionAvailability availability = executor.EvaluateOption(
|
||||
option,
|
||||
new BackpackInventoryData { Gold = 60 });
|
||||
|
||||
Assert.That(availability.IsSelectable, Is.False);
|
||||
Assert.That(availability.BlockedReason, Is.EqualTo("需要至少 100 金币"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_Applies_Cost_Before_Probability_And_Skips_Reward_On_Failure()
|
||||
{
|
||||
EventOptionExecutor executor = new EventOptionExecutor();
|
||||
EventItem eventItem = new EventItem(
|
||||
1,
|
||||
"赌马",
|
||||
"测试事件",
|
||||
new[]
|
||||
{
|
||||
new EventOption(
|
||||
"下注",
|
||||
new EventRequirementBase[]
|
||||
{
|
||||
new GoldAtLeastRequirement(new GoldAtLeastParam(100))
|
||||
},
|
||||
new EventEffectBase[]
|
||||
{
|
||||
new AddGoldEffect(new AddGoldParam(-100))
|
||||
},
|
||||
new EventEffectBase[]
|
||||
{
|
||||
new AddGoldEffect(new AddGoldParam(250))
|
||||
},
|
||||
probability: 0f)
|
||||
});
|
||||
BackpackInventoryData inventory = new BackpackInventoryData { Gold = 120 };
|
||||
|
||||
EventOptionExecutionResult result = executor.Execute(
|
||||
eventItem,
|
||||
0,
|
||||
CreateContext(),
|
||||
inventory);
|
||||
|
||||
Assert.That(result.IsAccepted, Is.True);
|
||||
Assert.That(result.IsProbabilitySuccess, Is.False);
|
||||
Assert.That(inventory.Gold, Is.EqualTo(20));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_Removes_Only_Loose_Components_Of_Requested_Rarity()
|
||||
{
|
||||
EventOptionExecutor executor = new EventOptionExecutor();
|
||||
EventItem eventItem = new EventItem(
|
||||
2,
|
||||
"工匠",
|
||||
"测试事件",
|
||||
new[]
|
||||
{
|
||||
new EventOption(
|
||||
"交出 2 个白色组件",
|
||||
new EventRequirementBase[]
|
||||
{
|
||||
new CompCountAtLeastRequirement(new CompCountAtLeastParam(2, RarityType.White))
|
||||
},
|
||||
new EventEffectBase[]
|
||||
{
|
||||
new RemoveRandomCompsEffect(new RemoveRandomCompsParam(2, RarityType.White))
|
||||
},
|
||||
Array.Empty<EventEffectBase>())
|
||||
});
|
||||
BackpackInventoryData inventory = new BackpackInventoryData
|
||||
{
|
||||
MuzzleComponents = new List<MuzzleCompItemData>
|
||||
{
|
||||
new MuzzleCompItemData
|
||||
{
|
||||
InstanceId = 10001,
|
||||
Name = "已装配白枪口",
|
||||
Rarity = RarityType.White,
|
||||
IsAssembledIntoTower = true
|
||||
},
|
||||
new MuzzleCompItemData
|
||||
{
|
||||
InstanceId = 10002,
|
||||
Name = "未装配白枪口",
|
||||
Rarity = RarityType.White,
|
||||
IsAssembledIntoTower = false
|
||||
}
|
||||
},
|
||||
BearingComponents = new List<BearingCompItemData>
|
||||
{
|
||||
new BearingCompItemData
|
||||
{
|
||||
InstanceId = 20001,
|
||||
Name = "未装配白轴承",
|
||||
Rarity = RarityType.White,
|
||||
IsAssembledIntoTower = false
|
||||
},
|
||||
new BearingCompItemData
|
||||
{
|
||||
InstanceId = 20002,
|
||||
Name = "绿色轴承",
|
||||
Rarity = RarityType.Green,
|
||||
IsAssembledIntoTower = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
EventOptionExecutionResult result = executor.Execute(eventItem, 0, CreateContext(), inventory);
|
||||
|
||||
Assert.That(result.IsAccepted, Is.True);
|
||||
Assert.That(inventory.MuzzleComponents.Count, Is.EqualTo(1));
|
||||
Assert.That(inventory.MuzzleComponents[0].InstanceId, Is.EqualTo(10001));
|
||||
Assert.That(inventory.BearingComponents.Count, Is.EqualTo(1));
|
||||
Assert.That(inventory.BearingComponents[0].InstanceId, Is.EqualTo(20002));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_AddRandomComponents_Is_Stable_For_Same_Context()
|
||||
{
|
||||
BindInventoryGeneration();
|
||||
EventOptionExecutor executor = new EventOptionExecutor();
|
||||
EventItem eventItem = new EventItem(
|
||||
3,
|
||||
"奖励组件",
|
||||
"测试事件",
|
||||
new[]
|
||||
{
|
||||
new EventOption(
|
||||
"获得两个蓝色组件",
|
||||
Array.Empty<EventRequirementBase>(),
|
||||
Array.Empty<EventEffectBase>(),
|
||||
new EventEffectBase[]
|
||||
{
|
||||
new AddRandomCompsEffect(new AddRandomCompsParam(2, RarityType.Blue))
|
||||
})
|
||||
});
|
||||
RunNodeExecutionContext context = CreateContext();
|
||||
|
||||
BackpackInventoryData firstInventory = new BackpackInventoryData();
|
||||
BackpackInventoryData secondInventory = new BackpackInventoryData();
|
||||
|
||||
EventOptionExecutionResult firstResult = executor.Execute(eventItem, 0, context, firstInventory);
|
||||
EventOptionExecutionResult secondResult = executor.Execute(eventItem, 0, context, secondInventory);
|
||||
|
||||
Assert.That(firstResult.IsAccepted, Is.True);
|
||||
Assert.That(secondResult.IsAccepted, Is.True);
|
||||
Assert.That(BuildComponentSignature(secondInventory), Is.EqualTo(BuildComponentSignature(firstInventory)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_ComponentExchange_Removes_Two_And_Returns_One_In_Configured_Range()
|
||||
{
|
||||
BindInventoryGeneration();
|
||||
EventOptionExecutor executor = new EventOptionExecutor();
|
||||
EventItem eventItem = new EventItem(
|
||||
5,
|
||||
"组件交换",
|
||||
"测试事件",
|
||||
new[]
|
||||
{
|
||||
new EventOption(
|
||||
"交出 2 个绿色组件,获得 1 个绿色或蓝色组件",
|
||||
new EventRequirementBase[]
|
||||
{
|
||||
new CompCountAtLeastRequirement(new CompCountAtLeastParam(2, RarityType.Green))
|
||||
},
|
||||
new EventEffectBase[]
|
||||
{
|
||||
new RemoveRandomCompsEffect(new RemoveRandomCompsParam(2, RarityType.Green))
|
||||
},
|
||||
new EventEffectBase[]
|
||||
{
|
||||
new AddRandomCompsEffect(new AddRandomCompsParam(1, RarityType.Green, RarityType.Blue))
|
||||
})
|
||||
});
|
||||
BackpackInventoryData inventory = new BackpackInventoryData
|
||||
{
|
||||
MuzzleComponents = new List<MuzzleCompItemData>
|
||||
{
|
||||
new MuzzleCompItemData
|
||||
{
|
||||
InstanceId = 10001,
|
||||
Name = "绿枪口 A",
|
||||
Rarity = RarityType.Green,
|
||||
IsAssembledIntoTower = false
|
||||
}
|
||||
},
|
||||
BearingComponents = new List<BearingCompItemData>
|
||||
{
|
||||
new BearingCompItemData
|
||||
{
|
||||
InstanceId = 20001,
|
||||
Name = "绿轴承 B",
|
||||
Rarity = RarityType.Green,
|
||||
IsAssembledIntoTower = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
EventOptionExecutionResult result = executor.Execute(eventItem, 0, CreateContext(), inventory);
|
||||
|
||||
Assert.That(result.IsAccepted, Is.True);
|
||||
Assert.That(CountLooseComponentsOfRarity(inventory, RarityType.Green), Is.LessThanOrEqualTo(1));
|
||||
Assert.That(CountAllComponents(inventory), Is.EqualTo(1));
|
||||
|
||||
TowerCompItemData rewardItem = GetOnlyComponent(inventory);
|
||||
Assert.That(rewardItem.Rarity == RarityType.Green || rewardItem.Rarity == RarityType.Blue, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_EnduranceForGold_Applies_Cost_Before_Reward()
|
||||
{
|
||||
EventOptionExecutor executor = new EventOptionExecutor();
|
||||
EventItem eventItem = new EventItem(
|
||||
6,
|
||||
"耐久换金币",
|
||||
"测试事件",
|
||||
new[]
|
||||
{
|
||||
new EventOption(
|
||||
"扣耐久换金币",
|
||||
new EventRequirementBase[]
|
||||
{
|
||||
new TowerCountAtLeastRequirement(new TowerCountAtLeastParam(1))
|
||||
},
|
||||
new EventEffectBase[]
|
||||
{
|
||||
new DamageRandomTowerEnduranceEffect(new DamageRandomTowerEnduranceParam(1, 20))
|
||||
},
|
||||
new EventEffectBase[]
|
||||
{
|
||||
new AddGoldEffect(new AddGoldParam(50))
|
||||
})
|
||||
});
|
||||
BackpackInventoryData inventory = CreateParticipantInventory(endurance: 30f);
|
||||
inventory.Gold = 10;
|
||||
|
||||
EventOptionExecutionResult result = executor.Execute(eventItem, 0, CreateContext(), inventory);
|
||||
|
||||
Assert.That(result.IsAccepted, Is.True);
|
||||
Assert.That(inventory.Gold, Is.EqualTo(60));
|
||||
Assert.That(inventory.MuzzleComponents[0].Endurance, Is.EqualTo(10f));
|
||||
Assert.That(inventory.BearingComponents[0].Endurance, Is.EqualTo(10f));
|
||||
Assert.That(inventory.BaseComponents[0].Endurance, Is.EqualTo(10f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_Damaged_Participant_Tower_Can_Be_Cleaned_Up_By_Procedure_Service()
|
||||
{
|
||||
EventOptionExecutor executor = new EventOptionExecutor();
|
||||
EventItem eventItem = new EventItem(
|
||||
4,
|
||||
"耐久惩罚",
|
||||
"测试事件",
|
||||
new[]
|
||||
{
|
||||
new EventOption(
|
||||
"损坏防御塔",
|
||||
Array.Empty<EventRequirementBase>(),
|
||||
Array.Empty<EventEffectBase>(),
|
||||
new EventEffectBase[]
|
||||
{
|
||||
new DamageRandomTowerEnduranceEffect(new DamageRandomTowerEnduranceParam(1, 20))
|
||||
})
|
||||
});
|
||||
BackpackInventoryData inventory = CreateParticipantInventory(endurance: 10f);
|
||||
|
||||
EventOptionExecutionResult result = executor.Execute(eventItem, 0, CreateContext(), inventory);
|
||||
ProcedureMainParticipantTowerCleanupResult cleanupResult =
|
||||
ProcedureMainParticipantTowerCleanupService.RemoveBrokenParticipantTowers(inventory, 4);
|
||||
|
||||
Assert.That(result.IsAccepted, Is.True);
|
||||
Assert.That(cleanupResult.HasAnyRemovedTower, Is.True);
|
||||
Assert.That(inventory.ParticipantTowerInstanceIds, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EventTable_Uses_Only_Runtime_Supported_Types()
|
||||
{
|
||||
string filePath = ResolveRepoFilePath("Assets/GameMain/DataTables/Event.txt");
|
||||
Assert.That(File.Exists(filePath), Is.True, filePath);
|
||||
|
||||
HashSet<string> supportedRequirementTypes = new HashSet<string>
|
||||
{
|
||||
"GoldAtLeast",
|
||||
"CompCountAtLeast",
|
||||
"TowerCountAtLeast"
|
||||
};
|
||||
HashSet<string> supportedEffectTypes = new HashSet<string>
|
||||
{
|
||||
"AddGold",
|
||||
"RemoveRandomComps",
|
||||
"AddRandomComps",
|
||||
"DamageRandomTowersEndurance"
|
||||
};
|
||||
|
||||
foreach (string line in File.ReadLines(filePath))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DREvent row = new DREvent();
|
||||
Assert.That(row.ParseDataRow(line, null), Is.True, line);
|
||||
JArray options = JArray.Parse(row.OptionsRaw);
|
||||
for (int i = 0; i < options.Count; i++)
|
||||
{
|
||||
JObject optionObject = options[i] as JObject;
|
||||
Assert.That(optionObject, Is.Not.Null, $"EventId={row.Id}, OptionIndex={i}");
|
||||
AssertSupportedTypes(
|
||||
row.Id,
|
||||
i,
|
||||
optionObject["requirements"] as JArray,
|
||||
supportedRequirementTypes,
|
||||
isRequirement: true);
|
||||
AssertSupportedTypes(
|
||||
row.Id,
|
||||
i,
|
||||
optionObject["costEffects"] as JArray,
|
||||
supportedEffectTypes,
|
||||
isRequirement: false);
|
||||
AssertSupportedTypes(
|
||||
row.Id,
|
||||
i,
|
||||
optionObject["rewardEffects"] as JArray,
|
||||
supportedEffectTypes,
|
||||
isRequirement: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EventNodeComponent_Selects_Same_Event_For_Same_Context()
|
||||
{
|
||||
GameObject gameObject = new GameObject("EventNodeComponentTests");
|
||||
try
|
||||
{
|
||||
EventNodeComponent component = gameObject.AddComponent<EventNodeComponent>();
|
||||
List<EventItem> eventItems = GetPrivateField<List<EventItem>>(component, "_eventItems");
|
||||
eventItems.Add(new EventItem(11, "事件一", string.Empty, Array.Empty<EventOption>()));
|
||||
eventItems.Add(new EventItem(12, "事件二", string.Empty, Array.Empty<EventOption>()));
|
||||
eventItems.Add(new EventItem(13, "事件三", string.Empty, Array.Empty<EventOption>()));
|
||||
|
||||
MethodInfo method = typeof(EventNodeComponent).GetMethod(
|
||||
"SelectActiveEvent",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(method, Is.Not.Null);
|
||||
|
||||
RunNodeExecutionContext context = CreateContext();
|
||||
EventItem first = (EventItem)method.Invoke(component, new object[] { context });
|
||||
EventItem second = (EventItem)method.Invoke(component, new object[] { context });
|
||||
|
||||
Assert.That(second, Is.Not.Null);
|
||||
Assert.That(second.Id, Is.EqualTo(first.Id));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void BindInventoryGeneration()
|
||||
{
|
||||
_originalInventoryGeneration = GameEntry.InventoryGeneration;
|
||||
_inventoryGenerationObject = new GameObject("EventInventoryGenerationTests");
|
||||
InventoryGenerationComponent component = _inventoryGenerationObject.AddComponent<InventoryGenerationComponent>();
|
||||
SetPrivateField(component, "_muzzleCompTable", new FakeDataTable<DRMuzzleComp>(
|
||||
CreateMuzzleRow(1, "火焰枪口", "[Fire,Ice,Crit]"),
|
||||
CreateMuzzleRow(2, "暴击枪口", "[Crit,Shatter,Execution]")));
|
||||
SetPrivateField(component, "_bearingCompTable", new FakeDataTable<DRBearingComp>(
|
||||
CreateBearingRow(1, "寒冰轴承", "[Ice,Shatter]"),
|
||||
CreateBearingRow(2, "穿透轴承", "[Pierce,Crit]")));
|
||||
SetPrivateField(component, "_baseCompTable", new FakeDataTable<DRBaseComp>(
|
||||
CreateBaseRow(1, "迅捷底座", "[Fire,Crit]"),
|
||||
CreateBaseRow(2, "处决底座", "[Execution,Ice]")));
|
||||
SetStaticInventoryGeneration(component);
|
||||
}
|
||||
|
||||
private static RunNodeExecutionContext CreateContext()
|
||||
{
|
||||
return new RunNodeExecutionContext
|
||||
{
|
||||
RunId = "testrun",
|
||||
RunSeed = 12345,
|
||||
NodeId = 101,
|
||||
NodeType = RunNodeType.Event,
|
||||
SequenceIndex = 3
|
||||
};
|
||||
}
|
||||
|
||||
private static BackpackInventoryData CreateParticipantInventory(float endurance)
|
||||
{
|
||||
return new BackpackInventoryData
|
||||
{
|
||||
MuzzleComponents = new List<MuzzleCompItemData>
|
||||
{
|
||||
new MuzzleCompItemData
|
||||
{
|
||||
InstanceId = 10001,
|
||||
Name = "枪口",
|
||||
Rarity = RarityType.Blue,
|
||||
Endurance = endurance,
|
||||
IsAssembledIntoTower = true
|
||||
}
|
||||
},
|
||||
BearingComponents = new List<BearingCompItemData>
|
||||
{
|
||||
new BearingCompItemData
|
||||
{
|
||||
InstanceId = 20001,
|
||||
Name = "轴承",
|
||||
Rarity = RarityType.Blue,
|
||||
Endurance = endurance,
|
||||
IsAssembledIntoTower = true
|
||||
}
|
||||
},
|
||||
BaseComponents = new List<BaseCompItemData>
|
||||
{
|
||||
new BaseCompItemData
|
||||
{
|
||||
InstanceId = 30001,
|
||||
Name = "底座",
|
||||
Rarity = RarityType.Blue,
|
||||
Endurance = endurance,
|
||||
IsAssembledIntoTower = true
|
||||
}
|
||||
},
|
||||
Towers = new List<TowerItemData>
|
||||
{
|
||||
new TowerItemData
|
||||
{
|
||||
InstanceId = 90001,
|
||||
Name = "测试防御塔",
|
||||
MuzzleComponentInstanceId = 10001,
|
||||
BearingComponentInstanceId = 20001,
|
||||
BaseComponentInstanceId = 30001
|
||||
}
|
||||
},
|
||||
ParticipantTowerInstanceIds = new List<long> { 90001 }
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildComponentSignature(BackpackInventoryData inventory)
|
||||
{
|
||||
IEnumerable<TowerCompItemData> items =
|
||||
inventory.MuzzleComponents.Cast<TowerCompItemData>()
|
||||
.Concat(inventory.BearingComponents)
|
||||
.Concat(inventory.BaseComponents)
|
||||
.OrderBy(item => item.InstanceId);
|
||||
return string.Join(
|
||||
"|",
|
||||
items.Select(item =>
|
||||
{
|
||||
string tags = item.Tags == null || item.Tags.Length <= 0
|
||||
? string.Empty
|
||||
: string.Join(",", item.Tags.Select(tag => tag.ToString()));
|
||||
return $"{item.InstanceId}:{item.SlotType}:{item.ConfigId}:{item.Name}:{item.Rarity}:{tags}";
|
||||
}));
|
||||
}
|
||||
|
||||
private static int CountLooseComponentsOfRarity(BackpackInventoryData inventory, RarityType rarity)
|
||||
{
|
||||
return GetAllComponents(inventory)
|
||||
.Count(component => !component.IsAssembledIntoTower && component.Rarity == rarity);
|
||||
}
|
||||
|
||||
private static int CountAllComponents(BackpackInventoryData inventory)
|
||||
{
|
||||
return GetAllComponents(inventory).Count();
|
||||
}
|
||||
|
||||
private static TowerCompItemData GetOnlyComponent(BackpackInventoryData inventory)
|
||||
{
|
||||
return GetAllComponents(inventory).Single();
|
||||
}
|
||||
|
||||
private static IEnumerable<TowerCompItemData> GetAllComponents(BackpackInventoryData inventory)
|
||||
{
|
||||
return inventory.MuzzleComponents.Cast<TowerCompItemData>()
|
||||
.Concat(inventory.BearingComponents)
|
||||
.Concat(inventory.BaseComponents);
|
||||
}
|
||||
|
||||
private static void AssertSupportedTypes(
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
JArray entries,
|
||||
HashSet<string> supportedTypes,
|
||||
bool isRequirement)
|
||||
{
|
||||
if (entries == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
JObject entry = entries[i] as JObject;
|
||||
Assert.That(entry, Is.Not.Null, $"EventId={eventId}, OptionIndex={optionIndex}, EntryIndex={i}");
|
||||
string rawType = entry.Value<string>("type");
|
||||
Assert.That(
|
||||
supportedTypes.Contains(rawType),
|
||||
Is.True,
|
||||
$"{(isRequirement ? "Requirement" : "Effect")} type '{rawType}' is not supported at runtime. EventId={eventId}, OptionIndex={optionIndex}, EntryIndex={i}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetStaticInventoryGeneration(InventoryGenerationComponent component)
|
||||
{
|
||||
FieldInfo backingField = typeof(GameEntry).GetField(
|
||||
"<InventoryGeneration>k__BackingField",
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
Assert.That(backingField, Is.Not.Null);
|
||||
backingField.SetValue(null, component);
|
||||
}
|
||||
|
||||
private static TField GetPrivateField<TField>(object instance, string fieldName)
|
||||
{
|
||||
FieldInfo field = instance.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(field, Is.Not.Null, fieldName);
|
||||
return (TField)field.GetValue(instance);
|
||||
}
|
||||
|
||||
private static void SetPrivateField(object instance, string fieldName, object value)
|
||||
{
|
||||
FieldInfo field = instance.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(field, Is.Not.Null, fieldName);
|
||||
field.SetValue(instance, value);
|
||||
}
|
||||
|
||||
private static string ResolveRepoFilePath(string relativePath)
|
||||
{
|
||||
DirectoryInfo directory = new DirectoryInfo(Directory.GetCurrentDirectory());
|
||||
while (directory != null)
|
||||
{
|
||||
string candidate = Path.Combine(directory.FullName, relativePath);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
return Path.Combine(Directory.GetCurrentDirectory(), relativePath);
|
||||
}
|
||||
|
||||
private static DRMuzzleComp CreateMuzzleRow(int id, string name, string possibleTags)
|
||||
{
|
||||
DRMuzzleComp row = new DRMuzzleComp();
|
||||
Assert.That(
|
||||
row.ParseDataRow($"\t{id}\t\t{name}\t[10,20,30,40,50]\t3\t0.15\tNormalBullet\t\t{possibleTags}", null),
|
||||
Is.True);
|
||||
return row;
|
||||
}
|
||||
|
||||
private static DRBearingComp CreateBearingRow(int id, string name, string possibleTags)
|
||||
{
|
||||
DRBearingComp row = new DRBearingComp();
|
||||
Assert.That(
|
||||
row.ParseDataRow($"\t{id}\t\t{name}\t[1,2,3,4,5]\t0.5\t[10,20,30,40,50]\t1\t\t{possibleTags}", null),
|
||||
Is.True);
|
||||
return row;
|
||||
}
|
||||
|
||||
private static DRBaseComp CreateBaseRow(int id, string name, string possibleTags)
|
||||
{
|
||||
DRBaseComp row = new DRBaseComp();
|
||||
Assert.That(
|
||||
row.ParseDataRow($"\t{id}\t\t{name}\t[2,4,6,8,10]\t-0.25\tFire\t\t{possibleTags}", 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 => _rowsById.Count <= 0 ? null : GetDataRow(GetOrderedIds()[0]);
|
||||
public TRow MaxIdDataRow => _rowsById.Count <= 0 ? null : GetDataRow(GetOrderedIds()[^1]);
|
||||
|
||||
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)
|
||||
{
|
||||
if (condition == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (TRow row in _rowsById.Values)
|
||||
{
|
||||
if (row != 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 = _rowsById.Keys.ToArray();
|
||||
Array.Sort(ids);
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b3563fbb433e44fa9c91a61c401e511b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
数据表/Event.xlsx
BIN
数据表/Event.xlsx
Binary file not shown.
Loading…
Reference in New Issue