This commit is contained in:
2026-04-30 19:33:16 +08:00
commit fb2252f688
556 changed files with 69721 additions and 0 deletions
@@ -0,0 +1,226 @@
using System.Collections.Generic;
namespace GeometryTD.Definition
{
public enum CombatParticipantTowerValidationFailureReason
{
None = 0,
TowerMissing = 1,
MissingMuzzleComponent = 2,
MissingBearingComponent = 3,
MissingBaseComponent = 4,
BrokenMuzzleComponent = 5,
BrokenBearingComponent = 6,
BrokenBaseComponent = 7
}
public sealed class CombatParticipantTowerValidationResult
{
public long TowerInstanceId { get; set; }
public bool IsValid => FailureReason == CombatParticipantTowerValidationFailureReason.None;
public CombatParticipantTowerValidationFailureReason FailureReason { get; set; }
public TowerItemData Tower { get; set; }
}
public sealed class CombatParticipantTowerValidationSummary
{
public IReadOnlyList<TowerItemData> ValidTowers { get; set; } = System.Array.Empty<TowerItemData>();
public IReadOnlyList<CombatParticipantTowerValidationResult> InvalidResults { get; set; } =
System.Array.Empty<CombatParticipantTowerValidationResult>();
public bool HasAnyValidParticipantTower => ValidTowers != null && ValidTowers.Count > 0;
}
public static class CombatParticipantTowerValidationService
{
public static CombatParticipantTowerValidationResult ValidateTower(
BackpackInventoryData inventory,
long towerInstanceId)
{
if (!TryGetTowerById(inventory, towerInstanceId, out TowerItemData tower))
{
return new CombatParticipantTowerValidationResult
{
TowerInstanceId = towerInstanceId,
FailureReason = CombatParticipantTowerValidationFailureReason.TowerMissing
};
}
return ValidateTower(inventory, tower);
}
public static CombatParticipantTowerValidationResult ValidateTower(
BackpackInventoryData inventory,
TowerItemData tower)
{
if (tower == null || tower.InstanceId <= 0)
{
return new CombatParticipantTowerValidationResult
{
TowerInstanceId = tower != null ? tower.InstanceId : 0,
Tower = tower,
FailureReason = CombatParticipantTowerValidationFailureReason.TowerMissing
};
}
if (!TryGetComponentById(inventory?.MuzzleComponents, tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComponent))
{
return new CombatParticipantTowerValidationResult
{
TowerInstanceId = tower.InstanceId,
Tower = tower,
FailureReason = CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent
};
}
if (muzzleComponent.Endurance <= 0f)
{
return new CombatParticipantTowerValidationResult
{
TowerInstanceId = tower.InstanceId,
Tower = tower,
FailureReason = CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent
};
}
if (!TryGetComponentById(inventory?.BearingComponents, tower.BearingComponentInstanceId, out BearingCompItemData bearingComponent))
{
return new CombatParticipantTowerValidationResult
{
TowerInstanceId = tower.InstanceId,
Tower = tower,
FailureReason = CombatParticipantTowerValidationFailureReason.MissingBearingComponent
};
}
if (bearingComponent.Endurance <= 0f)
{
return new CombatParticipantTowerValidationResult
{
TowerInstanceId = tower.InstanceId,
Tower = tower,
FailureReason = CombatParticipantTowerValidationFailureReason.BrokenBearingComponent
};
}
if (!TryGetComponentById(inventory?.BaseComponents, tower.BaseComponentInstanceId, out BaseCompItemData baseComponent))
{
return new CombatParticipantTowerValidationResult
{
TowerInstanceId = tower.InstanceId,
Tower = tower,
FailureReason = CombatParticipantTowerValidationFailureReason.MissingBaseComponent
};
}
if (baseComponent.Endurance <= 0f)
{
return new CombatParticipantTowerValidationResult
{
TowerInstanceId = tower.InstanceId,
Tower = tower,
FailureReason = CombatParticipantTowerValidationFailureReason.BrokenBaseComponent
};
}
return new CombatParticipantTowerValidationResult
{
TowerInstanceId = tower.InstanceId,
Tower = tower,
FailureReason = CombatParticipantTowerValidationFailureReason.None
};
}
public static CombatParticipantTowerValidationSummary ValidateParticipantTowers(BackpackInventoryData inventory)
{
List<TowerItemData> validTowers = new List<TowerItemData>();
List<CombatParticipantTowerValidationResult> invalidResults =
new List<CombatParticipantTowerValidationResult>();
HashSet<long> processedTowerIds = new HashSet<long>();
if (inventory?.ParticipantTowerInstanceIds == null || inventory.ParticipantTowerInstanceIds.Count <= 0)
{
return new CombatParticipantTowerValidationSummary
{
ValidTowers = validTowers,
InvalidResults = invalidResults
};
}
for (int i = 0; i < inventory.ParticipantTowerInstanceIds.Count; i++)
{
long towerInstanceId = inventory.ParticipantTowerInstanceIds[i];
if (towerInstanceId <= 0 || !processedTowerIds.Add(towerInstanceId))
{
continue;
}
CombatParticipantTowerValidationResult result = ValidateTower(inventory, towerInstanceId);
if (result.IsValid)
{
validTowers.Add(result.Tower);
}
else
{
invalidResults.Add(result);
}
}
return new CombatParticipantTowerValidationSummary
{
ValidTowers = validTowers,
InvalidResults = invalidResults
};
}
private static bool TryGetTowerById(BackpackInventoryData inventory, long towerInstanceId, out TowerItemData tower)
{
tower = null;
if (inventory?.Towers == null || towerInstanceId <= 0)
{
return false;
}
for (int i = 0; i < inventory.Towers.Count; i++)
{
TowerItemData candidate = inventory.Towers[i];
if (candidate != null && candidate.InstanceId == towerInstanceId)
{
tower = candidate;
return true;
}
}
return false;
}
private static bool TryGetComponentById<TComponent>(
IReadOnlyList<TComponent> components,
long componentInstanceId,
out TComponent resolvedComponent)
where TComponent : TowerCompItemData
{
resolvedComponent = null;
if (components == null || componentInstanceId <= 0)
{
return false;
}
for (int i = 0; i < components.Count; i++)
{
TComponent component = components[i];
if (component != null && component.InstanceId == componentInstanceId)
{
resolvedComponent = component;
return true;
}
}
return false;
}
}
}
@@ -0,0 +1,28 @@
namespace GeometryTD.Definition
{
public static class CombatParticipantTowerValidationText
{
public static string GetFailureReasonMessage(CombatParticipantTowerValidationFailureReason failureReason)
{
switch (failureReason)
{
case CombatParticipantTowerValidationFailureReason.TowerMissing:
return "已不存在,无法参战。";
case CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent:
return "缺少枪口组件。";
case CombatParticipantTowerValidationFailureReason.MissingBearingComponent:
return "缺少轴承组件。";
case CombatParticipantTowerValidationFailureReason.MissingBaseComponent:
return "缺少底座组件。";
case CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent:
return "枪口组件耐久为 0,无法参战。";
case CombatParticipantTowerValidationFailureReason.BrokenBearingComponent:
return "轴承组件耐久为 0,无法参战。";
case CombatParticipantTowerValidationFailureReason.BrokenBaseComponent:
return "底座组件耐久为 0,无法参战。";
default:
return "不满足当前参战条件。";
}
}
}
}
@@ -0,0 +1,40 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
namespace GeometryTD
{
public static partial class Constant
{
/// <summary>
/// 资源优先级。
/// </summary>
public static class AssetPriority
{
public const int ConfigAsset = 100;
public const int DataTableAsset = 100;
public const int DictionaryAsset = 100;
public const int FontAsset = 50;
public const int MusicAsset = 20;
public const int SceneAsset = 0;
public const int SoundAsset = 30;
public const int UIFormAsset = 50;
public const int UISoundAsset = 30;
public const int MyAircraftAsset = 90;
public const int PlayerAsset = 90;
public const int AircraftAsset = 80;
public const int ThrusterAsset = 30;
public const int WeaponAsset = 30;
public const int ArmorAsset = 30;
public const int BulletAsset = 80;
public const int AsteroiAsset = 80;
public const int EnemyAsset = 80;
public const int MapAsset = 85;
public const int EffectAsset = 80;
}
}
}
@@ -0,0 +1,29 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using UnityEngine;
namespace GeometryTD
{
public static partial class Constant
{
/// <summary>
/// 层。
/// </summary>
public static class Layer
{
public const string DefaultLayerName = "Default";
public static readonly int DefaultLayerId = LayerMask.NameToLayer(DefaultLayerName);
public const string UILayerName = "UI";
public static readonly int UILayerId = LayerMask.NameToLayer(UILayerName);
public const string TargetableObjectLayerName = "Targetable Object";
public static readonly int TargetableObjectLayerId = LayerMask.NameToLayer(TargetableObjectLayerName);
}
}
}
@@ -0,0 +1,25 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
namespace GeometryTD
{
public static partial class Constant
{
public static class Setting
{
public const string Language = "Setting.Language";
public const string SoundGroupMuted = "Setting.{0}Muted";
public const string SoundGroupVolume = "Setting.{0}Volume";
public const string MusicMuted = "Setting.MusicMuted";
public const string MusicVolume = "Setting.MusicVolume";
public const string SoundMuted = "Setting.SoundMuted";
public const string SoundVolume = "Setting.SoundVolume";
public const string UISoundMuted = "Setting.UISoundMuted";
public const string UISoundVolume = "Setting.UISoundVolume";
}
}
}
@@ -0,0 +1,30 @@
using System;
using GeometryTD.CustomUtility;
using UnityEngine;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class AttackPayload
{
public int BaseDamage { get; set; }
public AttackPropertyType AttackPropertyType { get; set; }
public int SourceEntityId { get; set; }
public int ProjectileEntityId { get; set; }
public Vector3 OriginPosition { get; set; }
public TagRuntimeData[] TagRuntimes { get; set; } = Array.Empty<TagRuntimeData>();
public AttackPayload Clone()
{
return new AttackPayload
{
BaseDamage = BaseDamage,
AttackPropertyType = AttackPropertyType,
SourceEntityId = SourceEntityId,
ProjectileEntityId = ProjectileEntityId,
OriginPosition = OriginPosition,
TagRuntimes = InventoryCloneUtility.CloneTagRuntimes(TagRuntimes)
};
}
}
}
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
namespace GeometryTD.Definition
{
/// <summary>
/// 玩家背包聚合数据。
/// </summary>
[Serializable]
public sealed class BackpackInventoryData
{
/// <summary>
/// 背包金币。
/// </summary>
public int Gold { get; set; }
/// <summary>
/// 背包中的枪口组件实例。
/// </summary>
public List<MuzzleCompItemData> MuzzleComponents { get; set; } = new List<MuzzleCompItemData>();
/// <summary>
/// 背包中的轴承组件实例。
/// </summary>
public List<BearingCompItemData> BearingComponents { get; set; } = new List<BearingCompItemData>();
/// <summary>
/// 背包中的底座组件实例。
/// </summary>
public List<BaseCompItemData> BaseComponents { get; set; } = new List<BaseCompItemData>();
/// <summary>
/// 背包中的防御塔实例。
/// </summary>
public List<TowerItemData> Towers { get; set; } = new List<TowerItemData>();
/// <summary>
/// 参与战斗的防御塔 Id。
/// </summary>
public List<long> ParticipantTowerInstanceIds { get; set; } = new List<long>();
}
}
@@ -0,0 +1,54 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
namespace GeometryTD.Definition
{
public class BuildInfo
{
public string GameVersion
{
get;
set;
}
public int InternalGameVersion
{
get;
set;
}
public string CheckVersionUrl
{
get;
set;
}
public string WindowsAppUrl
{
get;
set;
}
public string MacOSAppUrl
{
get;
set;
}
public string IOSAppUrl
{
get;
set;
}
public string AndroidAppUrl
{
get;
set;
}
}
}
@@ -0,0 +1,18 @@
namespace GeometryTD.Definition
{
public class EventItem
{
public EventItem(int id, string title, string description, EventOption[] options)
{
Id = id;
Title = title;
Description = description;
Options = options ?? System.Array.Empty<EventOption>();
}
public int Id { get; private set; }
public string Title { get; private set; }
public string Description { get; private set; }
public EventOption[] Options { get; private set; }
}
}
@@ -0,0 +1,25 @@
namespace GeometryTD.Definition
{
public class EventOption
{
public string OptionText { get; private set; }
public EventRequirementBase[] Requirements { get; private set; }
public EventEffectBase[] CostEffects { get; private set; }
public EventEffectBase[] RewardEffects { get; private set; }
public float Probability { get; private set; }
public EventOption(
string optionText,
EventRequirementBase[] requirements,
EventEffectBase[] costEffects,
EventEffectBase[] rewardEffects,
float probability = 1f)
{
OptionText = optionText;
Requirements = requirements ?? System.Array.Empty<EventRequirementBase>();
CostEffects = costEffects ?? System.Array.Empty<EventEffectBase>();
RewardEffects = rewardEffects ?? System.Array.Empty<EventEffectBase>();
Probability = probability;
}
}
}
@@ -0,0 +1,62 @@
using System;
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class HitStatusModifierContext
{
public float BonusBurnDurationSeconds { get; set; }
public float BonusBurnDamagePerSecond { get; set; }
public float BonusSlowDurationSeconds { get; set; }
public float BonusSlowRatio { get; set; }
public void Reset()
{
BonusBurnDurationSeconds = 0f;
BonusBurnDamagePerSecond = 0f;
BonusSlowDurationSeconds = 0f;
BonusSlowRatio = 0f;
}
}
[Serializable]
public sealed class HitContext
{
public AttackPayload AttackPayload { get; set; }
public int FinalDamage { get; set; }
public bool IsCriticalHit { get; set; }
public bool IsKilled { get; set; }
public int TargetEntityId { get; set; }
public Vector3 TargetPosition { get; set; }
public int TargetCurrentHealthBeforeHit { get; set; }
public int TargetCurrentHealthAfterHit { get; set; }
public int TargetMaxHealth { get; set; }
public float TargetMoveSpeedMultiplierBeforeHit { get; set; } = 1f;
public TagType[] TargetStatusTagsBeforeHit { get; set; } = Array.Empty<TagType>();
public EnemyTagStatusRuntime TargetStatusRuntime { get; set; }
public float? CritRoll { get; set; }
public HitStatusModifierContext StatusModifierContext { get; set; } = new HitStatusModifierContext();
public bool HasTargetStatus(TagType tagType)
{
if (tagType == TagType.None || TargetStatusTagsBeforeHit == null || TargetStatusTagsBeforeHit.Length <= 0)
{
return false;
}
for (int i = 0; i < TargetStatusTagsBeforeHit.Length; i++)
{
if (TargetStatusTagsBeforeHit[i] == tagType)
{
return true;
}
}
return false;
}
public bool HasSlowStatusBeforeHit => TargetMoveSpeedMultiplierBeforeHit < 0.999f || HasTargetStatus(TagType.Ice);
}
}
@@ -0,0 +1,24 @@
using System.Runtime.InteropServices;
namespace GeometryTD.Definition
{
[StructLayout(LayoutKind.Auto)]
public struct ImpactData
{
public ImpactData(CampType camp, int hp, int attack, int defense)
{
Camp = camp;
HP = hp;
Attack = attack;
Defense = defense;
}
public CampType Camp { get; }
public int HP { get; }
public int Attack { get; }
public int Defense { get; }
}
}
@@ -0,0 +1,99 @@
using System;
namespace GeometryTD.Definition
{
/// <summary>
/// 背包内组件实例基类(非 DataTable,表示玩家持有物)。
/// </summary>
[Serializable]
public abstract class TowerCompItemData
{
/// <summary>
/// 组件实例唯一 Id。
/// </summary>
public long InstanceId { get; set; }
/// <summary>
/// 组件配置 Id(对应 DataTable Id)。
/// </summary>
public int ConfigId { get; set; }
/// <summary>
/// 组件槽位类型。
/// </summary>
public TowerCompSlotType SlotType { get; protected set; }
/// <summary>
/// 组件名称。
/// </summary>
public string Name { get; set; }
/// <summary>
/// 组件品质。
/// </summary>
public RarityType Rarity { get; set; }
/// <summary>
/// 组件当前耐久(0~100)。
/// </summary>
public float Endurance { get; set; } = 100f;
public bool IsAssembledIntoTower { get; set; }
/// <summary>
/// 组件约束(先沿用 DataTable 原定义)。
/// </summary>
public string Constraint { get; set; }
/// <summary>
/// 组件当前 Tag(实例态)。
/// </summary>
public TagType[] Tags { get; set; }
}
[Serializable]
public sealed class MuzzleCompItemData : TowerCompItemData
{
public MuzzleCompItemData()
{
SlotType = TowerCompSlotType.Muzzle;
}
public int[] AttackDamage { get; set; }
public float DamageRandomRate { get; set; }
public AttackMethodType AttackMethodType { get; set; }
}
[Serializable]
public sealed class BearingCompItemData : TowerCompItemData
{
public BearingCompItemData()
{
SlotType = TowerCompSlotType.Bearing;
}
public float[] RotateSpeed { get; set; }
public float[] AttackRange { get; set; }
}
[Serializable]
public sealed class BaseCompItemData : TowerCompItemData
{
public BaseCompItemData()
{
SlotType = TowerCompSlotType.Base;
}
public float[] AttackSpeed { get; set; }
public AttackPropertyType AttackPropertyType { get; set; }
}
[Serializable]
public sealed class AccessoryItemData : TowerCompItemData
{
public AccessoryItemData()
{
SlotType = TowerCompSlotType.Accessory;
}
}
}
@@ -0,0 +1,54 @@
using System;
using Newtonsoft.Json;
using UnityEngine;
namespace GeometryTD.Definition
{
/// <summary>
/// 背包内防御塔实例数据。
/// </summary>
[Serializable]
public sealed class TowerItemData
{
/// <summary>
/// 防御塔实例唯一 Id。
/// </summary>
public long InstanceId { get; set; }
/// <summary>
/// 防御塔显示名称。
/// </summary>
public string Name { get; set; }
/// <summary>
/// 防御塔品质。
/// </summary>
public RarityType Rarity { get; set; }
public bool IsParticipatingInCombat { get; set; }
/// <summary>
/// 构成该防御塔的枪口组件实例 Id。
/// </summary>
public long MuzzleComponentInstanceId { get; set; }
/// <summary>
/// 构成该防御塔的轴承组件实例 Id。
/// </summary>
public long BearingComponentInstanceId { get; set; }
/// <summary>
/// 构成该防御塔的底座组件实例 Id。
/// </summary>
public long BaseComponentInstanceId { get; set; }
/// <summary>
/// 防御塔独立属性,不依赖组件对象引用。
/// </summary>
public TowerStatsData Stats { get; set; } = new TowerStatsData();
[JsonIgnore] public Sprite ComposedIconSprite { get; set; }
[JsonIgnore] public string ComposedIconKey { get; set; }
}
}
@@ -0,0 +1,22 @@
using System;
namespace GeometryTD.Definition
{
/// <summary>
/// 防御塔独立属性快照。
/// 注意:这里是塔实例的独立值,不通过组件引用实时计算。
/// </summary>
[Serializable]
public sealed class TowerStatsData
{
public int[] AttackDamage { get; set; }
public float DamageRandomRate { get; set; }
public float[] RotateSpeed { get; set; }
public float[] AttackRange { get; set; }
public float[] AttackSpeed { get; set; }
public AttackMethodType AttackMethodType { get; set; }
public AttackPropertyType AttackPropertyType { get; set; }
public TagRuntimeData[] TagRuntimes { get; set; }
public TagType[] Tags { get; set; }
}
}
@@ -0,0 +1,68 @@
namespace GeometryTD.Definition
{
public class VersionInfo
{
// 是否需要强制更新游戏应用
public bool ForceUpdateGame
{
get;
set;
}
// 最新的游戏版本号
public string LatestGameVersion
{
get;
set;
}
// 最新的游戏内部版本号
public int InternalGameVersion
{
get;
set;
}
// 最新的资源内部版本号
public int InternalResourceVersion
{
get;
set;
}
// 资源更新下载地址
public string UpdatePrefixUri
{
get;
set;
}
// 资源版本列表长度
public int VersionListLength
{
get;
set;
}
// 资源版本列表哈希值
public int VersionListHashCode
{
get;
set;
}
// 资源版本列表压缩后长度
public int VersionListCompressedLength
{
get;
set;
}
// 资源版本列表压缩后哈希值
public int VersionListCompressedHashCode
{
get;
set;
}
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum AttackMethodType : byte
{
None = 0,
NormalBullet = 1,
Range
}
}
@@ -0,0 +1,13 @@
namespace GeometryTD.Definition
{
public enum AttackPropertyType : byte
{
None = 0,
Physics = 1,
Fire = 2,
Water = 3,
Earth = 4,
Poison = 5,
Ice = 6
}
}
+40
View File
@@ -0,0 +1,40 @@
namespace GeometryTD.Definition
{
/// <summary>
/// 阵营类型。
/// </summary>
public enum CampType : byte
{
Unknown = 0,
/// <summary>
/// 第一玩家阵营。
/// </summary>
Player,
/// <summary>
/// 第一敌人阵营。
/// </summary>
Enemy,
/// <summary>
/// 第一中立阵营。
/// </summary>
Neutral,
/// <summary>
/// 第二玩家阵营。
/// </summary>
Player2,
/// <summary>
/// 第二敌人阵营。
/// </summary>
Enemy2,
/// <summary>
/// 第二中立阵营
/// </summary>
Neutral2,
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace GeometryTD.Definition
{
/// <summary>
/// 出怪条目类型
/// </summary>
public enum EntryType : byte
{
None = 0,
Stream = 1,
Burst = 2,
Boss = 3
}
}
@@ -0,0 +1,21 @@
namespace GeometryTD.Definition
{
public enum EventEffectType
{
None,
AddGold,
RemoveGold,
AddRandomComps,
RemoveRandomComps,
AddRandomCompsEndurance,
RemoveRandomCompsEndurance,
AddRandomTowersEndurance,
DamageRandomTowersEndurance,
TransformComponents,
}
}
@@ -0,0 +1,11 @@
namespace GeometryTD.Definition
{
public enum EventRequirementType
{
None,
GoldAtLeast,
CompCountAtLeast,
TowerCountAtLeast,
HasRelic,
}
}
@@ -0,0 +1,11 @@
namespace GeometryTD.Definition
{
public enum InventoryTagSourceType : byte
{
Seed = 1,
Shop = 2,
Drop = 3,
Reward = 4,
Event = 5,
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace GeometryTD.Definition
{
public enum LevelThemeType
{
None,
Plain,
Volcano,
Mountain
}
}
@@ -0,0 +1,10 @@
namespace GeometryTD.Definition
{
public enum LevelVictoryType
{
None,
PhasesCleared,
BossDead,
TimeElapsed
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace GeometryTD.Definition
{
/// <summary>
/// 关卡阶段结束类型
/// </summary>
public enum PhaseEndType : byte
{
None = 0,
TimeElapsed = 1,
EnemiesCleared = 2,
BossDead = 3
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainCombatEntryBlockReason
{
None = 0,
InventoryUnavailable = 1,
NoValidParticipantTower = 2
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainFlowPhase
{
Hub = 0,
NodeActive = 1,
RunCompletedPendingFinish = 2
}
}
@@ -0,0 +1,10 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainRunAdvanceResult
{
NoChange = 0,
NodeException = 1,
AdvancedToNextNode = 2,
RunCompleted = 3
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainRunCompletionResult
{
NoChange = 0,
ShowCompletionDialog = 1,
ReturnToMenu = 2
}
}
+12
View File
@@ -0,0 +1,12 @@
namespace GeometryTD.Definition
{
public enum RarityType
{
None = 0,
White = 1,
Green = 2,
Blue = 3,
Purple = 4,
Red = 5
}
}
+28
View File
@@ -0,0 +1,28 @@
namespace GeometryTD.Definition
{
/// <summary>
/// 关系类型。
/// </summary>
public enum RelationType : byte
{
/// <summary>
/// 未知的。
/// </summary>
Unknown,
/// <summary>
/// 友好的。
/// </summary>
Friendly,
/// <summary>
/// 中立的。
/// </summary>
Neutral,
/// <summary>
/// 敌对的。
/// </summary>
Hostile
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum RepoItemClickActionType
{
OpenDetail,
RemoveParticipant,
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum SceneType : byte
{
Launch = 0,
Menu = 1,
Main = 2,
}
}
+12
View File
@@ -0,0 +1,12 @@
namespace GeometryTD.Definition
{
public enum TagCategory : byte
{
None = 0,
Status = 1,
NumericModifier = 2,
AttackShape = 3,
// Enhances another applied status on the same hit, but does not create its own enemy-held state.
StatusModifier = 4
}
}
@@ -0,0 +1,11 @@
namespace GeometryTD.Definition
{
public enum TagTriggerPhase : byte
{
None = 0,
OnBeforeHit = 1,
OnHit = 2,
OnAfterHit = 3,
OnKill = 4
}
}
+31
View File
@@ -0,0 +1,31 @@
namespace GeometryTD.Definition
{
public enum TagType : byte
{
None = 0,
/// <summary>
/// 元素
/// </summary>
Fire,
BurnSpread,
IgniteBurst,
Inferno,
/// <summary>
/// 控制
/// </summary>
Ice,
FreezeMask,
Shatter,
AbsoluteZero,
/// <summary>
/// 穿透
/// </summary>
Pierce,
Crit,
Overpenetrate,
Execution,
}
}
@@ -0,0 +1,11 @@
namespace GeometryTD.Definition
{
public enum TowerCompSlotType : byte
{
None = 0,
Muzzle = 1,
Bearing = 2,
Base = 3,
Accessory = 4,
}
}
+85
View File
@@ -0,0 +1,85 @@
namespace GeometryTD.Definition
{
/// <summary>
/// 界面编号。
/// </summary>
public enum UIFormType : byte
{
Undefined = 0,
/// <summary>
/// 弹出框。
/// </summary>
DialogForm = 1,
/// <summary>
/// 主菜单。
/// </summary>
MenuForm = 100,
/// <summary>
/// 设置。
/// </summary>
SettingForm = 101,
/// <summary>
/// 关于。
/// </summary>
AboutForm = 102,
/// <summary>
/// 主界面。
/// </summary>
MainForm = 110,
/// <summary>
/// 仓库界面。
/// </summary>
RepoForm = 111,
/// <summary>
/// 节点地图界面。
/// </summary>
NodeMapForm = 112,
/// <summary>
/// 道具详细信息界面。
/// </summary>
ItemDescForm = 113,
/// <summary>
/// 奖励三选一界面。
/// </summary>
RewardSelectForm = 114,
/// <summary>
/// 事件节点界面。
/// </summary>
EventForm = 130,
/// <summary>
/// 战斗信息界面。
/// </summary>
CombatInfoForm = 140,
/// <summary>
/// 战斗结算界面。
/// </summary>
CombatFinishForm = 141,
/// <summary>
/// 战斗选择界面
/// </summary>
CombatSelectForm = 142,
/// <summary>
/// 商店界面。
/// </summary>
ShopForm = 150,
/// <summary>
/// 测试菜单。
/// </summary>
TestMenuForm = 200,
}
}
@@ -0,0 +1,25 @@
namespace GeometryTD.Definition
{
public class AddGoldEffect : EventEffectBase
{
public override EventEffectType EffectType => EventEffectType.AddGold;
public override EventEffectParam Param => _param;
private AddGoldParam _param;
public AddGoldEffect(AddGoldParam param, float? probability = null)
{
_param = param;
Probability = probability;
}
}
public class AddGoldParam : EventEffectParam
{
public int Count;
public AddGoldParam(int count)
{
Count = count;
}
}
}
@@ -0,0 +1,48 @@
namespace GeometryTD.Definition
{
public class AddRandomCompsEffect : EventEffectBase
{
public override EventEffectType EffectType => EventEffectType.AddRandomComps;
public override EventEffectParam Param => _param;
private AddRandomCompsParam _param;
public AddRandomCompsEffect(AddRandomCompsParam param, float? probability = null)
{
_param = param;
Probability = probability;
}
}
public class AddRandomCompsParam : EventEffectParam
{
public int Count;
public RarityType Rarity;
public RarityType MinRarity;
public RarityType MaxRarity;
public AddRandomCompsParam(int count, RarityType rarity)
{
Count = count;
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,34 @@
namespace GeometryTD.Definition
{
public class DamageRandomTowerEnduranceEffect : EventEffectBase
{
public override EventEffectType EffectType => EventEffectType.DamageRandomTowersEndurance;
public override EventEffectParam Param => _param;
private DamageRandomTowerEnduranceParam _param;
public DamageRandomTowerEnduranceEffect(DamageRandomTowerEnduranceParam param, float? probability = null)
{
_param = param;
Probability = probability;
}
}
public class DamageRandomTowerEnduranceParam : EventEffectParam
{
/// <summary>
/// 减少耐久的防御塔数量
/// </summary>
public int Count;
/// <summary>
/// 防御塔耐久减少的量
/// </summary>
public int Amount;
public DamageRandomTowerEnduranceParam(int count, int amount)
{
Count = count;
Amount = amount;
}
}
}
@@ -0,0 +1,14 @@
namespace GeometryTD.Definition
{
public abstract class EventEffectBase
{
public abstract EventEffectType EffectType { get; }
public abstract EventEffectParam Param { get; }
public float? Probability;
}
public class EventEffectParam
{
}
}
@@ -0,0 +1,27 @@
namespace GeometryTD.Definition
{
public class RemoveRandomCompsEffect : EventEffectBase
{
public override EventEffectType EffectType => EventEffectType.RemoveRandomComps;
public override EventEffectParam Param => _param;
private RemoveRandomCompsParam _param;
public RemoveRandomCompsEffect(RemoveRandomCompsParam param, float? probability = null)
{
_param = param;
Probability = probability;
}
}
public class RemoveRandomCompsParam : EventEffectParam
{
public int Count;
public RarityType Rarity;
public RemoveRandomCompsParam(int Count, RarityType Rarity)
{
this.Count = Count;
this.Rarity = Rarity;
}
}
}
@@ -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,26 @@
namespace GeometryTD.Definition
{
public class CompCountAtLeastRequirement : EventRequirementBase
{
public override EventRequirementType RequirementType => EventRequirementType.CompCountAtLeast;
public override EventRequirementParam Param => _param;
private CompCountAtLeastParam _param;
public CompCountAtLeastRequirement(CompCountAtLeastParam param)
{
_param = param;
}
}
public class CompCountAtLeastParam : EventRequirementParam
{
public int Count;
public RarityType Rarity;
public CompCountAtLeastParam(int count, RarityType rarity)
{
Count = count;
Rarity = rarity;
}
}
}
@@ -0,0 +1,12 @@
namespace GeometryTD.Definition
{
public abstract class EventRequirementBase
{
public abstract EventRequirementType RequirementType { get; }
public abstract EventRequirementParam Param { get; }
}
public class EventRequirementParam
{
}
}
@@ -0,0 +1,24 @@
namespace GeometryTD.Definition
{
public class GoldAtLeastRequirement : EventRequirementBase
{
public override EventRequirementType RequirementType => EventRequirementType.GoldAtLeast;
public override EventRequirementParam Param => _param;
private GoldAtLeastParam _param;
public GoldAtLeastRequirement(GoldAtLeastParam param)
{
_param = param;
}
}
public class GoldAtLeastParam : EventRequirementParam
{
public int Gold;
public GoldAtLeastParam(int gold)
{
Gold = gold;
}
}
}
@@ -0,0 +1,24 @@
namespace GeometryTD.Definition
{
public class HasRelicRequirement : EventRequirementBase
{
public override EventRequirementType RequirementType => EventRequirementType.HasRelic;
public override EventRequirementParam Param => _param;
private HasRelicParam _param;
public HasRelicRequirement(HasRelicParam param)
{
_param = param;
}
}
public class HasRelicParam : EventRequirementParam
{
public int RelicId;
public HasRelicParam(int relicId)
{
RelicId = relicId;
}
}
}
@@ -0,0 +1,24 @@
namespace GeometryTD.Definition
{
public class TowerCountAtLeastRequirement : EventRequirementBase
{
public override EventRequirementType RequirementType => EventRequirementType.TowerCountAtLeast;
public override EventRequirementParam Param => _param;
private TowerCountAtLeastParam _param;
public TowerCountAtLeastRequirement(TowerCountAtLeastParam param)
{
_param = param;
}
}
public class TowerCountAtLeastParam : EventRequirementParam
{
public int Count;
public TowerCountAtLeastParam(int count)
{
Count = count;
}
}
}
@@ -0,0 +1,32 @@
using UnityEngine;
namespace GeometryTD.Definition
{
public static class InventoryRarityRuleService
{
public static RarityType NormalizeComponentRarity(RarityType rarity)
{
if (rarity < RarityType.White || rarity > RarityType.Red)
{
return RarityType.White;
}
return rarity;
}
public static RarityType ResolveTowerRarity(
RarityType muzzleRarity,
RarityType bearingRarity,
RarityType baseRarity)
{
int normalizedMuzzle = (int)NormalizeComponentRarity(muzzleRarity);
int normalizedBearing = (int)NormalizeComponentRarity(bearingRarity);
int normalizedBase = (int)NormalizeComponentRarity(baseRarity);
float average = (normalizedMuzzle + normalizedBearing + normalizedBase) / 3f;
int floored = Mathf.FloorToInt(average);
int clamped = Mathf.Clamp(floored, (int)RarityType.White, (int)RarityType.Red);
return (RarityType)clamped;
}
}
}
@@ -0,0 +1,22 @@
namespace GeometryTD.Definition
{
public enum ParticipantTowerAssignFailureReason
{
None = 0,
TowerMissing = 1,
InvalidTower = 2,
AlreadyAssigned = 3,
ParticipantAreaFull = 4
}
public sealed class ParticipantTowerAssignResult
{
public long TowerInstanceId { get; set; }
public bool IsSuccess => FailureReason == ParticipantTowerAssignFailureReason.None;
public ParticipantTowerAssignFailureReason FailureReason { get; set; }
public CombatParticipantTowerValidationFailureReason ValidationFailureReason { get; set; }
}
}
@@ -0,0 +1,11 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class TagRuntimeData
{
public TagType TagType { get; set; }
public int TotalStack { get; set; }
}
}
@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
namespace GeometryTD.Definition
{
public static class TowerTagAggregationService
{
public static TagRuntimeData[] AggregateTowerTags(params TagType[][] componentTags)
{
Dictionary<TagType, int> stackByTag = new Dictionary<TagType, int>();
if (componentTags != null)
{
foreach (var tags in componentTags)
{
if (tags == null)
{
continue;
}
foreach (var tagType in tags)
{
if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType))
{
continue;
}
if (stackByTag.TryGetValue(tagType, out int stack))
{
stackByTag[tagType] = stack + 1;
}
else
{
stackByTag[tagType] = 1;
}
}
}
}
if (stackByTag.Count <= 0)
{
return Array.Empty<TagRuntimeData>();
}
List<TagRuntimeData> runtimes = new List<TagRuntimeData>(stackByTag.Count);
foreach (var pair in stackByTag)
{
runtimes.Add(new TagRuntimeData
{
TagType = pair.Key,
TotalStack = Math.Max(1, pair.Value)
});
}
runtimes.Sort((left, right) => left.TagType.CompareTo(right.TagType));
return runtimes.ToArray();
}
public static TagRuntimeData[] BuildRuntimeTagsFromUniqueTags(IReadOnlyList<TagType> tags)
{
if (tags == null || tags.Count <= 0)
{
return Array.Empty<TagRuntimeData>();
}
HashSet<TagType> uniqueTags = new HashSet<TagType>();
foreach (var tagType in tags)
{
if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType))
{
continue;
}
uniqueTags.Add(tagType);
}
if (uniqueTags.Count <= 0)
{
return Array.Empty<TagRuntimeData>();
}
TagType[] orderedTags = new TagType[uniqueTags.Count];
uniqueTags.CopyTo(orderedTags);
Array.Sort(orderedTags);
TagRuntimeData[] runtimes = new TagRuntimeData[orderedTags.Length];
for (int i = 0; i < orderedTags.Length; i++)
{
runtimes[i] = new TagRuntimeData
{
TagType = orderedTags[i],
TotalStack = 1
};
}
return runtimes;
}
public static TagType[] FlattenUniqueTags(IReadOnlyList<TagRuntimeData> tagRuntimes)
{
if (tagRuntimes == null || tagRuntimes.Count <= 0)
{
return Array.Empty<TagType>();
}
List<TagType> tags = new List<TagType>(tagRuntimes.Count);
foreach (var runtime in tagRuntimes)
{
if (runtime == null || runtime.TagType == TagType.None || !Enum.IsDefined(typeof(TagType), runtime.TagType))
{
continue;
}
if (runtime.TotalStack <= 0)
{
continue;
}
tags.Add(runtime.TagType);
}
if (tags.Count <= 0)
{
return Array.Empty<TagType>();
}
tags.Sort();
return tags.ToArray();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Definition
{
public static class EnemyStatusTagRegistry
{
// Only tags that create or tick enemy-held status state belong here.
private static readonly Dictionary<TagType, IEnemyStatusTagEffect> EffectsByTag =
new Dictionary<TagType, IEnemyStatusTagEffect>
{
[TagType.Fire] = new FireTagEffect(),
[TagType.Ice] = new IceTagEffect()
};
public static IReadOnlyDictionary<TagType, IEnemyStatusTagEffect> Effects => EffectsByTag;
public static bool TryGetEffect(TagType tagType, out IEnemyStatusTagEffect effect)
{
return EffectsByTag.TryGetValue(tagType, out effect);
}
}
}
@@ -0,0 +1,41 @@
namespace GeometryTD.Definition
{
public static class AttackShapeTagEffectHandler
{
public static void Apply(HitContext hitContext, TagTriggerPhase triggerPhase)
{
if (hitContext?.AttackPayload?.TagRuntimes == null || hitContext.AttackPayload.TagRuntimes.Length <= 0)
{
return;
}
for (int i = 0; i < hitContext.AttackPayload.TagRuntimes.Length; i++)
{
TagRuntimeData runtime = hitContext.AttackPayload.TagRuntimes[i];
if (runtime == null || runtime.TotalStack <= 0)
{
continue;
}
if (!TagDefinitionRegistry.TryGetDefinition(runtime.TagType, out TagDefinition definition) ||
definition == null ||
definition.Category != TagCategory.AttackShape ||
definition.TriggerPhase != triggerPhase ||
definition.Config == null)
{
continue;
}
switch (runtime.TagType)
{
case TagType.BurnSpread:
case TagType.IgniteBurst:
case TagType.FreezeMask:
case TagType.Pierce:
case TagType.Overpenetrate:
break;
}
}
}
}
}
@@ -0,0 +1,109 @@
using UnityEngine;
namespace GeometryTD.Definition
{
public static class NumericTagEffectHandler
{
public static void ApplyBeforeHit(HitContext hitContext)
{
if (hitContext == null || hitContext.AttackPayload == null)
{
return;
}
TagRuntimeData[] tagRuntimes = hitContext.AttackPayload.TagRuntimes;
if (tagRuntimes == null || tagRuntimes.Length <= 0)
{
return;
}
for (int i = 0; i < tagRuntimes.Length; i++)
{
TagRuntimeData runtime = tagRuntimes[i];
if (runtime == null || runtime.TotalStack <= 0)
{
continue;
}
if (!TagDefinitionRegistry.TryGetDefinition(runtime.TagType, out TagDefinition definition) ||
definition == null ||
definition.Category != TagCategory.NumericModifier ||
definition.TriggerPhase != TagTriggerPhase.OnBeforeHit ||
definition.Config == null)
{
continue;
}
switch (runtime.TagType)
{
case TagType.Crit:
ApplyCrit(hitContext, runtime.TotalStack, definition.Config as CritTagConfig);
break;
case TagType.Execution:
ApplyExecution(hitContext, runtime.TotalStack, definition.Config as ExecutionTagConfig);
break;
case TagType.Shatter:
ApplyShatter(hitContext, runtime.TotalStack, definition.Config as ShatterTagConfig);
break;
}
}
hitContext.IsKilled = hitContext.TargetCurrentHealthBeforeHit > 0 &&
hitContext.FinalDamage >= hitContext.TargetCurrentHealthBeforeHit;
}
private static void ApplyCrit(HitContext hitContext, int stack, CritTagConfig config)
{
if (config == null || !config.IsImplemented || stack <= 0)
{
return;
}
float chance = Mathf.Clamp01(stack * config.CritChancePerStack);
float resolvedCritRoll = hitContext.CritRoll ?? Random.value;
if (resolvedCritRoll > chance)
{
return;
}
hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * config.CritDamageMultiplier));
hitContext.IsCriticalHit = true;
}
private static void ApplyExecution(
HitContext hitContext,
int stack,
ExecutionTagConfig config)
{
if (config == null || !config.IsImplemented || stack <= 0 || hitContext.TargetMaxHealth <= 0)
{
return;
}
float healthRatio = Mathf.Clamp01((float)Mathf.Max(0, hitContext.TargetCurrentHealthBeforeHit) / hitContext.TargetMaxHealth);
if (healthRatio > config.TargetHealthThreshold)
{
return;
}
float multiplier = 1f + stack * config.DamageBonusPerStack;
hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * multiplier));
}
private static void ApplyShatter(HitContext hitContext, int stack, ShatterTagConfig config)
{
if (config == null || !config.IsImplemented || stack <= 0)
{
return;
}
if (config.RequiresSlowedTarget && !hitContext.HasSlowStatusBeforeHit)
{
return;
}
float multiplier = 1f + stack * config.DamageBonusPerStack;
hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * multiplier));
}
}
}
@@ -0,0 +1,7 @@
namespace GeometryTD.Definition
{
public abstract class EnemyStatusTagStateBase
{
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public sealed class FireTagState : EnemyStatusTagStateBase
{
public float RemainingDuration { get; set; }
public float DamagePerSecond { get; set; }
public float PendingDamage { get; set; }
}
}
@@ -0,0 +1,8 @@
namespace GeometryTD.Definition
{
public sealed class IceTagState : EnemyStatusTagStateBase
{
public float RemainingDuration { get; set; }
public float SlowMultiplier { get; set; } = 1f;
}
}
@@ -0,0 +1,30 @@
using System;
using GeometryTD.Entity;
namespace GeometryTD.Definition
{
public abstract class EnemyStatusTagEffectBase : IEnemyStatusTagEffect
{
public abstract TagType TagType { get; }
public virtual void Apply(HitContext hitContext, TagRuntimeData runtimeData)
{
_ = hitContext;
_ = runtimeData;
}
public virtual bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action<int> applyDamage)
{
_ = runtime;
_ = deltaTime;
_ = applyDamage;
return false;
}
public virtual float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime)
{
_ = runtime;
return 1f;
}
}
}
@@ -0,0 +1,75 @@
using System;
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Definition
{
public sealed class FireTagEffect : EnemyStatusTagEffectBase
{
public override TagType TagType => TagType.Fire;
public override void Apply(HitContext hitContext, TagRuntimeData runtimeData)
{
Debug.Assert(TagDefinitionRegistry.TryGetDefinition(TagType, out TagDefinition definition));
Debug.Assert(hitContext != null);
Debug.Assert(hitContext.TargetStatusRuntime != null);
FireTagConfig config = definition.Config as FireTagConfig;
Debug.Assert(config != null);
if (!config.IsImplemented)
{
return;
}
EnemyTagStatusRuntime runtime = hitContext.TargetStatusRuntime;
FireTagState state = runtime.GetOrCreateState<FireTagState>(TagType);
int effectiveStack = Mathf.Max(0, Mathf.Min(runtimeData.TotalStack, config.MaxEffectiveStack));
HitStatusModifierContext modifierContext = hitContext.StatusModifierContext ?? new HitStatusModifierContext();
float burnDamagePerSecond = Mathf.Max(
0f,
config.BurnDamagePerSecondPerStack * effectiveStack + modifierContext.BonusBurnDamagePerSecond);
if (burnDamagePerSecond <= 0f)
{
return;
}
state.RemainingDuration = Mathf.Max(
state.RemainingDuration,
config.BurnDurationSeconds + modifierContext.BonusBurnDurationSeconds);
state.DamagePerSecond = Mathf.Max(state.DamagePerSecond, burnDamagePerSecond);
runtime.Activate(TagType);
}
public override bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action<int> applyDamage)
{
FireTagState state = runtime.GetState<FireTagState>(TagType);
Debug.Assert(state != null);
float resolvedDeltaTime = Mathf.Max(0f, deltaTime);
if (resolvedDeltaTime <= 0f || state.RemainingDuration <= 0f)
{
return state.RemainingDuration > 0f;
}
float appliedDuration = Mathf.Min(resolvedDeltaTime, state.RemainingDuration);
state.RemainingDuration = Mathf.Max(0f, state.RemainingDuration - appliedDuration);
state.PendingDamage += state.DamagePerSecond * appliedDuration;
int resolvedDamage = Mathf.FloorToInt(state.PendingDamage);
if (resolvedDamage > 0)
{
applyDamage?.Invoke(resolvedDamage);
state.PendingDamage -= resolvedDamage;
}
if (state.RemainingDuration > 0f)
{
return true;
}
state.PendingDamage = 0f;
return false;
}
}
}
@@ -0,0 +1,13 @@
using System;
using GeometryTD.Entity;
namespace GeometryTD.Definition
{
public interface IEnemyStatusTagEffect
{
TagType TagType { get; }
void Apply(HitContext hitContext, TagRuntimeData runtimeData);
bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action<int> applyDamage);
float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime);
}
}
@@ -0,0 +1,54 @@
using System;
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Definition
{
public sealed class IceTagEffect : EnemyStatusTagEffectBase
{
public override TagType TagType => TagType.Ice;
public override void Apply(HitContext hitContext, TagRuntimeData runtimeData)
{
Debug.Assert(hitContext != null);
Debug.Assert(hitContext.TargetStatusRuntime != null);
Debug.Assert(runtimeData != null);
Debug.Assert(runtimeData.TotalStack > 0);
Debug.Assert(TagDefinitionRegistry.TryGetDefinition(TagType, out TagDefinition definition));
IceTagConfig config = definition.Config as IceTagConfig;
Debug.Assert(config != null);
if (!config.IsImplemented)
{
return;
}
EnemyTagStatusRuntime runtime = hitContext.TargetStatusRuntime;
IceTagState state = runtime.GetOrCreateState<IceTagState>(TagType);
HitStatusModifierContext modifierContext = hitContext.StatusModifierContext ?? new HitStatusModifierContext();
float slowRatio = runtimeData.TotalStack * config.SlowRatioPerStack + modifierContext.BonusSlowRatio;
float slowMultiplier = Mathf.Max(config.MinMoveSpeedMultiplier, 1f - slowRatio);
state.RemainingDuration = Mathf.Max(
state.RemainingDuration,
config.SlowDurationSeconds + modifierContext.BonusSlowDurationSeconds);
state.SlowMultiplier = Mathf.Min(state.SlowMultiplier, slowMultiplier);
runtime.Activate(TagType);
}
public override bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action<int> applyDamage)
{
_ = applyDamage;
IceTagState state = runtime.GetState<IceTagState>(TagType);
Debug.Assert(state != null);
state.RemainingDuration = Mathf.Max(0f, state.RemainingDuration - Mathf.Max(0f, deltaTime));
return state.RemainingDuration > 0f;
}
public override float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime)
{
IceTagState state = runtime.GetState<IceTagState>(TagType);
return state == null ? 1f : state.SlowMultiplier;
}
}
}
@@ -0,0 +1,177 @@
using GeometryTD.Entity;
using UnityEngine;
namespace GeometryTD.Definition
{
public static class TagEffectResolver
{
private static readonly HitStatusModifierContext EmptyStatusModifierContext = new HitStatusModifierContext();
public static HitContext ResolveBeforeHit(HitContext hitContext)
{
if (hitContext == null)
{
return new HitContext
{
AttackPayload = new AttackPayload()
};
}
AttackPayload resolvedPayload = hitContext.AttackPayload?.Clone() ?? new AttackPayload();
HitContext resolvedContext = new HitContext
{
AttackPayload = resolvedPayload,
FinalDamage = Mathf.Max(0, resolvedPayload.BaseDamage),
IsCriticalHit = false,
IsKilled = hitContext.TargetCurrentHealthBeforeHit > 0 &&
resolvedPayload.BaseDamage >= hitContext.TargetCurrentHealthBeforeHit,
TargetEntityId = hitContext.TargetEntityId,
TargetPosition = hitContext.TargetPosition,
TargetCurrentHealthBeforeHit = hitContext.TargetCurrentHealthBeforeHit,
TargetCurrentHealthAfterHit = hitContext.TargetCurrentHealthAfterHit,
TargetMaxHealth = hitContext.TargetMaxHealth,
TargetMoveSpeedMultiplierBeforeHit = hitContext.TargetMoveSpeedMultiplierBeforeHit,
TargetStatusTagsBeforeHit = hitContext.TargetStatusTagsBeforeHit ?? System.Array.Empty<TagType>(),
TargetStatusRuntime = hitContext.TargetStatusRuntime,
CritRoll = hitContext.CritRoll,
StatusModifierContext = new HitStatusModifierContext()
};
NumericTagEffectHandler.ApplyBeforeHit(resolvedContext);
return resolvedContext;
}
public static void ApplyAfterHit(HitContext hitContext)
{
if (hitContext?.StatusModifierContext == null)
{
if (hitContext != null)
{
hitContext.StatusModifierContext = new HitStatusModifierContext();
}
}
hitContext?.StatusModifierContext?.Reset();
ApplyStatusModifiers(hitContext);
if (hitContext?.AttackPayload != null && hitContext.TargetStatusRuntime != null)
{
TagRuntimeData[] tagRuntimes = hitContext.AttackPayload.TagRuntimes;
if (tagRuntimes != null && tagRuntimes.Length > 0)
{
foreach (var tag in tagRuntimes)
{
if (tag == null || tag.TotalStack <= 0)
{
continue;
}
if (!TagDefinitionRegistry.TryGetDefinition(tag.TagType, out TagDefinition definition) ||
definition == null ||
definition.Category != TagCategory.Status ||
definition.TriggerPhase != TagTriggerPhase.OnAfterHit ||
definition.Config == null ||
!EnemyStatusTagRegistry.TryGetEffect(tag.TagType, out IEnemyStatusTagEffect effect))
{
continue;
}
effect.Apply(hitContext, tag);
}
}
}
AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnAfterHit);
}
public static void ApplyOnHit(HitContext hitContext)
{
AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnHit);
}
public static void ApplyOnKill(HitContext hitContext)
{
AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnKill);
}
public static int GetTagStack(TagRuntimeData[] tagRuntimes, TagType tagType)
{
if (tagRuntimes == null || tagRuntimes.Length <= 0 || tagType == TagType.None)
{
return 0;
}
foreach (var tag in tagRuntimes)
{
if (tag == null || tag.TagType != tagType || tag.TotalStack <= 0)
{
continue;
}
return tag.TotalStack;
}
return 0;
}
private static void ApplyStatusModifiers(HitContext hitContext)
{
if (hitContext?.AttackPayload?.TagRuntimes == null || hitContext.AttackPayload.TagRuntimes.Length <= 0)
{
return;
}
HitStatusModifierContext modifierContext = hitContext.StatusModifierContext ?? EmptyStatusModifierContext;
for (int i = 0; i < hitContext.AttackPayload.TagRuntimes.Length; i++)
{
TagRuntimeData runtime = hitContext.AttackPayload.TagRuntimes[i];
if (runtime == null || runtime.TotalStack <= 0)
{
continue;
}
if (!TagDefinitionRegistry.TryGetDefinition(runtime.TagType, out TagDefinition definition) ||
definition == null ||
definition.Category != TagCategory.StatusModifier ||
definition.TriggerPhase != TagTriggerPhase.OnAfterHit ||
definition.Config == null ||
!definition.Config.IsImplemented)
{
continue;
}
switch (runtime.TagType)
{
case TagType.Inferno:
ApplyInfernoModifier(runtime.TotalStack, definition.Config as InfernoTagConfig, modifierContext);
break;
case TagType.AbsoluteZero:
ApplyAbsoluteZeroModifier(runtime.TotalStack, definition.Config as AbsoluteZeroTagConfig, modifierContext);
break;
}
}
}
private static void ApplyInfernoModifier(int stack, InfernoTagConfig config, HitStatusModifierContext modifierContext)
{
if (config == null || stack <= 0)
{
return;
}
modifierContext.BonusBurnDurationSeconds += stack * config.BonusBurnDurationSeconds;
modifierContext.BonusBurnDamagePerSecond += stack * config.BonusBurnDamagePerSecondPerStack;
}
private static void ApplyAbsoluteZeroModifier(int stack, AbsoluteZeroTagConfig config, HitStatusModifierContext modifierContext)
{
if (config == null || stack <= 0)
{
return;
}
modifierContext.BonusSlowDurationSeconds += stack * config.BonusSlowDurationSeconds;
modifierContext.BonusSlowRatio += stack * config.BonusSlowRatioPerStack;
}
}
}
@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Random = System.Random;
namespace GeometryTD.Definition
{
public static class ComponentTagGenerationService
{
public static TagType[] ResolveComponentTags(
IReadOnlyList<TagType> possibleTags,
RarityType rarity,
InventoryTagRandomContext randomContext,
IReadOnlyDictionary<TagType, TagGenerationRule> rulesByTag = null,
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> rarityTagBudgetRulesByRarity = null)
{
IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules;
TagType[] eligibleTags = GetEligibleTags(possibleTags, rarity, ruleLookup);
if (eligibleTags.Length <= 0)
{
return Array.Empty<TagType>();
}
Random random = new Random(BuildStableSeed(rarity, randomContext));
int tagBudget = ResolveRarityTagBudget(rarity, random, rarityTagBudgetRulesByRarity);
if (tagBudget <= 0)
{
return Array.Empty<TagType>();
}
int finalCount = Math.Min(tagBudget, eligibleTags.Length);
List<TagType> pool = new List<TagType>(eligibleTags);
TagType[] result = new TagType[finalCount];
for (int i = 0; i < finalCount; i++)
{
int index = RollWeightedIndex(pool, ruleLookup, random);
result[i] = pool[index];
pool.RemoveAt(index);
}
return result;
}
public static TagType[] ResolveComponentTags(
IReadOnlyList<TagType> possibleTags,
RarityType rarity,
InventoryTagSourceType sourceType,
long itemInstanceId,
int configId,
IReadOnlyDictionary<TagType, TagGenerationRule> rulesByTag = null,
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> rarityTagBudgetRulesByRarity = null)
{
return ResolveComponentTags(
possibleTags,
rarity,
new InventoryTagRandomContext(0, sourceType, itemInstanceId, configId),
rulesByTag,
rarityTagBudgetRulesByRarity);
}
public static TagType[] GetEligibleTags(
IReadOnlyList<TagType> possibleTags,
RarityType rarity,
IReadOnlyDictionary<TagType, TagGenerationRule> rulesByTag = null)
{
if (possibleTags == null || possibleTags.Count <= 0)
{
return Array.Empty<TagType>();
}
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules;
HashSet<TagType> uniqueTags = new HashSet<TagType>();
for (int i = 0; i < possibleTags.Count; i++)
{
TagType tagType = possibleTags[i];
if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType) || !IsSupportedLaunchTag(tagType))
{
continue;
}
if (!TryGetRule(tagType, ruleLookup, out TagGenerationRule rule))
{
continue;
}
if (normalizedRarity < InventoryRarityRuleService.NormalizeComponentRarity(rule.MinRarity))
{
continue;
}
uniqueTags.Add(tagType);
}
if (uniqueTags.Count <= 0)
{
return Array.Empty<TagType>();
}
TagType[] result = new TagType[uniqueTags.Count];
uniqueTags.CopyTo(result);
Array.Sort(result);
return result;
}
public static int ResolveRarityTagBudget(
RarityType rarity,
Random random,
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> rarityTagBudgetRulesByRarity = null)
{
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> ruleLookup = rarityTagBudgetRulesByRarity ?? RarityTagBudgetRuleRegistry.Rules;
RarityTagBudgetRule rule = GetRarityTagBudgetRule(normalizedRarity, ruleLookup);
Debug.Assert(rule.MinCount >= 0);
Debug.Assert(rule.MaxCount >= rule.MinCount);
if (rule.MinCount == rule.MaxCount)
{
return rule.MinCount;
}
random ??= new Random(0);
return random.Next(rule.MinCount, rule.MaxCount + 1);
}
private static bool IsSupportedLaunchTag(TagType tagType)
{
return TagDefinitionRegistry.TryGetDefinition(tagType, out TagDefinition definition) &&
definition.Config != null &&
definition.Config.IsImplemented;
}
private static bool TryGetRule(
TagType tagType,
IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup,
out TagGenerationRule rule)
{
if (ruleLookup != null && ruleLookup.TryGetValue(tagType, out rule))
{
return true;
}
rule = null;
return false;
}
private static RarityTagBudgetRule GetRarityTagBudgetRule(
RarityType rarity,
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> ruleLookup)
{
Debug.Assert(ruleLookup != null);
Debug.Assert(ruleLookup.TryGetValue(rarity, out _));
return ruleLookup[rarity];
}
private static int RollWeightedIndex(
IReadOnlyList<TagType> pool,
IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup,
Random random)
{
int totalWeight = 0;
for (int i = 0; i < pool.Count; i++)
{
TagGenerationRule rule = ruleLookup[pool[i]];
Debug.Assert(rule.Weight > 0);
totalWeight += rule.Weight;
}
Debug.Assert(totalWeight > 0);
int roll = random.Next(1, totalWeight + 1);
int cumulative = 0;
for (int i = 0; i < pool.Count; i++)
{
TagGenerationRule rule = ruleLookup[pool[i]];
cumulative += rule.Weight;
if (roll <= cumulative)
{
return i;
}
}
return pool.Count - 1;
}
private static int BuildStableSeed(RarityType rarity, InventoryTagRandomContext randomContext)
{
unchecked
{
int seed = 17;
seed = seed * 31 + randomContext.RunSeed;
seed = seed * 31 + (int)InventoryRarityRuleService.NormalizeComponentRarity(rarity);
seed = seed * 31 + (int)randomContext.SourceType;
seed = seed * 31 + randomContext.ConfigId;
seed = seed * 31 + (int)randomContext.ItemInstanceId;
seed = seed * 31 + (int)(randomContext.ItemInstanceId >> 32);
return seed;
}
}
}
}
@@ -0,0 +1,72 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public readonly struct InventoryTagRandomContext
{
public InventoryTagRandomContext(int runSeed, InventoryTagSourceType sourceType, long itemInstanceId, int configId)
{
RunSeed = runSeed;
SourceType = sourceType;
ItemInstanceId = itemInstanceId;
ConfigId = configId;
}
public int RunSeed { get; }
public InventoryTagSourceType SourceType { get; }
public long ItemInstanceId { get; }
public int ConfigId { get; }
public static InventoryTagRandomContext CreateSeed(int runSeed, long itemInstanceId, int configId)
{
return new InventoryTagRandomContext(runSeed, InventoryTagSourceType.Seed, itemInstanceId, configId);
}
public static InventoryTagRandomContext CreateShop(int runSeed, int nodeSequenceIndex, int goodsIndex, int configId)
{
return new InventoryTagRandomContext(
runSeed,
InventoryTagSourceType.Shop,
ComposeLocalItemInstanceId(nodeSequenceIndex, goodsIndex),
configId);
}
public static InventoryTagRandomContext CreateDrop(int runSeed, int nodeSequenceIndex, int dropOrdinal, int configId)
{
return new InventoryTagRandomContext(
runSeed,
InventoryTagSourceType.Drop,
ComposeLocalItemInstanceId(nodeSequenceIndex, dropOrdinal),
configId);
}
public static InventoryTagRandomContext CreateReward(int runSeed, int nodeSequenceIndex, int rewardOrdinal, int configId)
{
return new InventoryTagRandomContext(
runSeed,
InventoryTagSourceType.Reward,
ComposeLocalItemInstanceId(nodeSequenceIndex, rewardOrdinal),
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;
long normalizedOrdinal = Math.Max(0, localOrdinal) + 1L;
return (normalizedSequence << 32) | (uint)normalizedOrdinal;
}
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public sealed class RarityTagBudgetRule
{
public RarityType Rarity { get; set; }
public int MinCount { get; set; }
public int MaxCount { get; set; }
}
}
@@ -0,0 +1,73 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using UnityEngine;
namespace GeometryTD.Definition
{
public static class RarityTagBudgetRuleRegistry
{
private static readonly Dictionary<RarityType, RarityTagBudgetRule> RulesByRarity = CreateDefaultRules();
public static IReadOnlyDictionary<RarityType, RarityTagBudgetRule> Rules => RulesByRarity;
public static void ResetToDefaults()
{
RulesByRarity.Clear();
foreach (KeyValuePair<RarityType, RarityTagBudgetRule> pair in CreateDefaultRules())
{
RulesByRarity.Add(pair.Key, pair.Value);
}
}
public static void LoadFromRows(IEnumerable<DRRarityTagBudget> rows)
{
ResetToDefaults();
foreach (DRRarityTagBudget row in rows)
{
ApplyRow(row);
}
}
public static bool TryGetRule(RarityType rarity, out RarityTagBudgetRule rule)
{
return RulesByRarity.TryGetValue(rarity, out rule);
}
private static Dictionary<RarityType, RarityTagBudgetRule> CreateDefaultRules()
{
return new Dictionary<RarityType, RarityTagBudgetRule>
{
[RarityType.White] = CreateRule(RarityType.White, 0, 1),
[RarityType.Green] = CreateRule(RarityType.Green, 0, 2),
[RarityType.Blue] = CreateRule(RarityType.Blue, 1, 3),
[RarityType.Purple] = CreateRule(RarityType.Purple, 1, 3),
[RarityType.Red] = CreateRule(RarityType.Red, 2, 4)
};
}
private static RarityTagBudgetRule CreateRule(RarityType rarity, int minCount, int maxCount)
{
Debug.Assert(rarity >= RarityType.White && rarity <= RarityType.Red);
Debug.Assert(minCount >= 0);
Debug.Assert(maxCount >= minCount);
return new RarityTagBudgetRule
{
Rarity = rarity,
MinCount = minCount,
MaxCount = maxCount
};
}
private static void ApplyRow(DRRarityTagBudget row)
{
Debug.Assert(row != null);
Debug.Assert(row.Id > 0);
Debug.Assert(row.Rarity >= RarityType.White && row.Rarity <= RarityType.Red);
Debug.Assert(row.MinCount >= 0);
Debug.Assert(row.MaxCount >= row.MinCount);
RulesByRarity[row.Rarity] = CreateRule(row.Rarity, row.MinCount, row.MaxCount);
}
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public sealed class TagGenerationRule
{
public TagType TagType { get; set; }
public RarityType MinRarity { get; set; }
public int Weight { get; set; }
}
}
@@ -0,0 +1,74 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using UnityEngine;
namespace GeometryTD.Definition
{
public static class TagGenerationRuleRegistry
{
private static readonly Dictionary<TagType, TagGenerationRule> RulesByTag = CreateDefaultRules();
public static IReadOnlyDictionary<TagType, TagGenerationRule> Rules => RulesByTag;
public static void ResetToDefaults()
{
RulesByTag.Clear();
foreach (KeyValuePair<TagType, TagGenerationRule> pair in CreateDefaultRules())
{
RulesByTag.Add(pair.Key, pair.Value);
}
}
public static void LoadFromRows(IEnumerable<DRTag> rows)
{
ResetToDefaults();
foreach (DRTag row in rows)
{
ApplyRow(row);
}
}
public static bool TryGetRule(TagType tagType, out TagGenerationRule rule)
{
return RulesByTag.TryGetValue(tagType, out rule);
}
private static Dictionary<TagType, TagGenerationRule> CreateDefaultRules()
{
return new Dictionary<TagType, TagGenerationRule>
{
[TagType.Fire] = CreateRule(TagType.Fire, RarityType.White, 20),
[TagType.BurnSpread] = CreateRule(TagType.BurnSpread, RarityType.White, 20),
[TagType.IgniteBurst] = CreateRule(TagType.IgniteBurst, RarityType.Green, 15),
[TagType.Inferno] = CreateRule(TagType.Inferno, RarityType.Purple, 5),
[TagType.Ice] = CreateRule(TagType.Ice, RarityType.White, 1),
[TagType.FreezeMask] = CreateRule(TagType.FreezeMask, RarityType.White, 20),
[TagType.Shatter] = CreateRule(TagType.Shatter, RarityType.Green, 15),
[TagType.AbsoluteZero] = CreateRule(TagType.AbsoluteZero, RarityType.Purple, 1),
[TagType.Pierce] = CreateRule(TagType.Pierce, RarityType.White, 20),
[TagType.Crit] = CreateRule(TagType.Crit, RarityType.White, 20),
[TagType.Overpenetrate] = CreateRule(TagType.Overpenetrate, RarityType.Green, 15),
[TagType.Execution] = CreateRule(TagType.Execution, RarityType.Purple, 5)
};
}
private static TagGenerationRule CreateRule(TagType tagType, RarityType minRarity, int weight)
{
return new TagGenerationRule
{
TagType = tagType,
MinRarity = minRarity,
Weight = weight
};
}
private static void ApplyRow(DRTag row)
{
Debug.Assert(row != null);
Debug.Assert(row.Id > 0);
Debug.Assert(row.Weight > 0);
RulesByTag[row.TagType] = CreateRule(row.TagType, row.MinRarity, row.Weight);
}
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class AbsoluteZeroTagConfig : TagConfigBase
{
public AbsoluteZeroTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float BonusSlowDurationSeconds { get; set; } = 1f;
public float BonusSlowRatioPerStack { get; set; } = 0.1f;
}
}
@@ -0,0 +1,12 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class BurnSpreadTagConfig : TagConfigBase
{
public BurnSpreadTagConfig(bool isImplemented) : base(isImplemented)
{
}
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class CritTagConfig : TagConfigBase
{
public CritTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float CritChancePerStack { get; set; } = 0.1f;
public float CritDamageMultiplier { get; set; } = 1.5f;
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class ExecutionTagConfig : TagConfigBase
{
public ExecutionTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float TargetHealthThreshold { get; set; } = 0.3f;
public float DamageBonusPerStack { get; set; } = 0.5f;
}
}
@@ -0,0 +1,16 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class FireTagConfig : TagConfigBase
{
public FireTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float BurnDurationSeconds { get; set; } = 3f;
public float BurnDamagePerSecondPerStack { get; set; } = 20f;
public int MaxEffectiveStack { get; set; } = 5;
}
}
@@ -0,0 +1,12 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class FreezeMaskTagConfig : TagConfigBase
{
public FreezeMaskTagConfig(bool isImplemented) : base(isImplemented)
{
}
}
}
@@ -0,0 +1,16 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class IceTagConfig : TagConfigBase
{
public IceTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float SlowDurationSeconds { get; set; } = 2f;
public float SlowRatioPerStack { get; set; } = 0.2f;
public float MinMoveSpeedMultiplier { get; set; } = 0.4f;
}
}
@@ -0,0 +1,12 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class IgniteBurstTagConfig : TagConfigBase
{
public IgniteBurstTagConfig(bool isImplemented) : base(isImplemented)
{
}
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class InfernoTagConfig : TagConfigBase
{
public InfernoTagConfig(bool isImplemented) : base(isImplemented)
{
}
public float BonusBurnDurationSeconds { get; set; } = 2f;
public float BonusBurnDamagePerSecondPerStack { get; set; } = 20f;
}
}
@@ -0,0 +1,12 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class OverpenetrateTagConfig : TagConfigBase
{
public OverpenetrateTagConfig(bool isImplemented) : base(isImplemented)
{
}
}
}
@@ -0,0 +1,12 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class PierceTagConfig : TagConfigBase
{
public PierceTagConfig(bool isImplemented) : base(isImplemented)
{
}
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class ShatterTagConfig : TagConfigBase
{
public ShatterTagConfig(bool isImplemented) : base(isImplemented)
{
}
public bool RequiresSlowedTarget { get; set; } = true;
public float DamageBonusPerStack { get; set; } = 0.25f;
}
}
@@ -0,0 +1,15 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public abstract class TagConfigBase
{
public bool IsImplemented { get; set; }
protected TagConfigBase(bool isImplemented)
{
IsImplemented = isImplemented;
}
}
}
@@ -0,0 +1,14 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public sealed class TagDefinition
{
public TagType TagType { get; set; }
public TagCategory Category { get; set; }
public TagTriggerPhase TriggerPhase { get; set; }
public string Description { get; set; }
public TagConfigBase Config { get; set; }
}
}
@@ -0,0 +1,214 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace GeometryTD.Definition
{
public static class TagDefinitionRegistry
{
private static readonly Dictionary<TagType, TagDefinition> DefinitionsByTag = CreateDefaultDefinitions();
public static IReadOnlyDictionary<TagType, TagDefinition> Definitions => DefinitionsByTag;
public static void ResetToDefaults()
{
DefinitionsByTag.Clear();
foreach (KeyValuePair<TagType, TagDefinition> pair in CreateDefaultDefinitions())
{
DefinitionsByTag.Add(pair.Key, pair.Value);
}
}
public static void LoadFromRows(IEnumerable<DRTagConfig> rows)
{
ReloadFromRows(rows, null);
}
public static bool TryGetDefinition(TagType tagType, out TagDefinition definition)
{
return DefinitionsByTag.TryGetValue(tagType, out definition);
}
public static void ApplyTagRows(IEnumerable<DRTag> rows)
{
if (rows == null)
{
return;
}
foreach (DRTag row in rows)
{
ApplyTagRow(row);
}
}
public static void ReloadFromRows(IEnumerable<DRTagConfig> tagConfigRows, IEnumerable<DRTag> tagRows)
{
ResetToDefaults();
if (tagConfigRows != null)
{
foreach (DRTagConfig row in tagConfigRows)
{
ApplyRow(row);
}
}
if (tagRows != null)
{
foreach (DRTag row in tagRows)
{
ApplyTagRow(row);
}
}
}
private static Dictionary<TagType, TagDefinition> CreateDefaultDefinitions()
{
return new Dictionary<TagType, TagDefinition>
{
[TagType.Fire] = CreateDefinition(TagType.Fire, TagCategory.Status, TagTriggerPhase.OnAfterHit, new FireTagConfig(true)),
[TagType.BurnSpread] = CreateDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.None, new BurnSpreadTagConfig(false)),
[TagType.IgniteBurst] = CreateDefinition(TagType.IgniteBurst, TagCategory.AttackShape, TagTriggerPhase.None, new IgniteBurstTagConfig(false)),
[TagType.Inferno] = CreateDefinition(TagType.Inferno, TagCategory.StatusModifier, TagTriggerPhase.OnAfterHit, new InfernoTagConfig(true)),
[TagType.Ice] = CreateDefinition(TagType.Ice, TagCategory.Status, TagTriggerPhase.OnAfterHit, new IceTagConfig(true)),
[TagType.FreezeMask] = CreateDefinition(TagType.FreezeMask, TagCategory.AttackShape, TagTriggerPhase.None, new FreezeMaskTagConfig(false)),
[TagType.Shatter] = CreateDefinition(TagType.Shatter, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new ShatterTagConfig(true)),
[TagType.AbsoluteZero] = CreateDefinition(TagType.AbsoluteZero, TagCategory.StatusModifier, TagTriggerPhase.OnAfterHit, new AbsoluteZeroTagConfig(true)),
[TagType.Pierce] = CreateDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.None, new PierceTagConfig(false)),
[TagType.Crit] = CreateDefinition(TagType.Crit, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new CritTagConfig(true)),
[TagType.Overpenetrate] = CreateDefinition(TagType.Overpenetrate, TagCategory.AttackShape, TagTriggerPhase.None, new OverpenetrateTagConfig(false)),
[TagType.Execution] = CreateDefinition(TagType.Execution, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new ExecutionTagConfig(true))
};
}
private static TagDefinition CreateDefinition(
TagType tagType,
TagCategory category,
TagTriggerPhase triggerPhase,
TagConfigBase config)
{
return new TagDefinition
{
TagType = tagType,
Category = category,
TriggerPhase = triggerPhase,
Description = string.Empty,
Config = config
};
}
private static void ApplyRow(DRTagConfig row)
{
Debug.Assert(row != null);
Debug.Assert(DefinitionsByTag.TryGetValue(row.TagType, out TagDefinition definition));
definition.TriggerPhase = row.TriggerPhase;
definition.Description = row.Description ?? string.Empty;
if (string.IsNullOrWhiteSpace(row.ParamJson))
{
return;
}
JObject param = JObject.Parse(row.ParamJson);
switch (row.TagType)
{
case TagType.Fire:
ApplyFireConfig((FireTagConfig)definition.Config, param);
break;
case TagType.Inferno:
ApplyInfernoConfig((InfernoTagConfig)definition.Config, param);
break;
case TagType.Ice:
ApplyIceConfig((IceTagConfig)definition.Config, param);
break;
case TagType.Shatter:
ApplyShatterConfig((ShatterTagConfig)definition.Config, param);
break;
case TagType.AbsoluteZero:
ApplyAbsoluteZeroConfig((AbsoluteZeroTagConfig)definition.Config, param);
break;
case TagType.Crit:
ApplyCritConfig((CritTagConfig)definition.Config, param);
break;
case TagType.Execution:
ApplyExecutionConfig((ExecutionTagConfig)definition.Config, param);
break;
}
}
private static void ApplyTagRow(DRTag row)
{
Debug.Assert(row != null);
Debug.Assert(row.Id > 0);
Debug.Assert(DefinitionsByTag.TryGetValue(row.TagType, out TagDefinition definition));
Debug.Assert(definition.Config != null);
definition.Config.IsImplemented = row.IsImplemented;
}
private static void ApplyFireConfig(FireTagConfig config, JObject param)
{
config.BurnDurationSeconds = ReadFloat(param, nameof(FireTagConfig.BurnDurationSeconds), config.BurnDurationSeconds);
config.BurnDamagePerSecondPerStack = ReadFloat(param, nameof(FireTagConfig.BurnDamagePerSecondPerStack), config.BurnDamagePerSecondPerStack);
config.MaxEffectiveStack = ReadInt(param, nameof(FireTagConfig.MaxEffectiveStack), config.MaxEffectiveStack);
}
private static void ApplyInfernoConfig(InfernoTagConfig config, JObject param)
{
config.BonusBurnDurationSeconds = ReadFloat(param, nameof(InfernoTagConfig.BonusBurnDurationSeconds), config.BonusBurnDurationSeconds);
config.BonusBurnDamagePerSecondPerStack = ReadFloat(param, nameof(InfernoTagConfig.BonusBurnDamagePerSecondPerStack), config.BonusBurnDamagePerSecondPerStack);
}
private static void ApplyIceConfig(IceTagConfig config, JObject param)
{
config.SlowDurationSeconds = ReadFloat(param, nameof(IceTagConfig.SlowDurationSeconds), config.SlowDurationSeconds);
config.SlowRatioPerStack = ReadFloat(param, nameof(IceTagConfig.SlowRatioPerStack), config.SlowRatioPerStack);
config.MinMoveSpeedMultiplier = ReadFloat(param, nameof(IceTagConfig.MinMoveSpeedMultiplier), config.MinMoveSpeedMultiplier);
}
private static void ApplyShatterConfig(ShatterTagConfig config, JObject param)
{
config.RequiresSlowedTarget = ReadBool(param, nameof(ShatterTagConfig.RequiresSlowedTarget), config.RequiresSlowedTarget);
config.DamageBonusPerStack = ReadFloat(param, nameof(ShatterTagConfig.DamageBonusPerStack), config.DamageBonusPerStack);
}
private static void ApplyAbsoluteZeroConfig(AbsoluteZeroTagConfig config, JObject param)
{
config.BonusSlowDurationSeconds = ReadFloat(param, nameof(AbsoluteZeroTagConfig.BonusSlowDurationSeconds), config.BonusSlowDurationSeconds);
config.BonusSlowRatioPerStack = ReadFloat(param, nameof(AbsoluteZeroTagConfig.BonusSlowRatioPerStack), config.BonusSlowRatioPerStack);
}
private static void ApplyCritConfig(CritTagConfig config, JObject param)
{
config.CritChancePerStack = ReadFloat(param, nameof(CritTagConfig.CritChancePerStack), config.CritChancePerStack);
config.CritDamageMultiplier = ReadFloat(param, nameof(CritTagConfig.CritDamageMultiplier), config.CritDamageMultiplier);
}
private static void ApplyExecutionConfig(ExecutionTagConfig config, JObject param)
{
config.TargetHealthThreshold = ReadFloat(param, nameof(ExecutionTagConfig.TargetHealthThreshold), config.TargetHealthThreshold);
config.DamageBonusPerStack = ReadFloat(param, nameof(ExecutionTagConfig.DamageBonusPerStack), config.DamageBonusPerStack);
}
private static float ReadFloat(JObject param, string key, float defaultValue)
{
JToken token = param[key];
return token == null ? defaultValue : token.Value<float>();
}
private static int ReadInt(JObject param, string key, int defaultValue)
{
JToken token = param[key];
return token == null ? defaultValue : token.Value<int>();
}
private static bool ReadBool(JObject param, string key, bool defaultValue)
{
JToken token = param[key];
return token == null ? defaultValue : token.Value<bool>();
}
}
}
@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Definition;
namespace GeometryTD.CustomUtility
{
public static class TagDisplayUtility
{
public static string ResolveTagName(TagType tagType)
{
if (tagType == TagType.None)
{
return string.Empty;
}
if (GameEntry.DataTable != null)
{
var tagTable = GameEntry.DataTable.GetDataTable<DRTag>();
if (tagTable != null)
{
DRTag tagRow = tagTable.GetDataRow((int)tagType);
if (tagRow != null && !string.IsNullOrWhiteSpace(tagRow.Name))
{
return tagRow.Name;
}
}
}
return tagType.ToString();
}
public static string[] BuildTagTexts(IReadOnlyList<TagType> tags)
{
if (tags == null || tags.Count <= 0)
{
return Array.Empty<string>();
}
List<string> results = new List<string>(tags.Count);
for (int i = 0; i < tags.Count; i++)
{
string tagName = ResolveTagName(tags[i]);
if (!string.IsNullOrWhiteSpace(tagName))
{
results.Add(tagName);
}
}
return results.ToArray();
}
public static string[] BuildTagTexts(IReadOnlyList<TagRuntimeData> tagRuntimes)
{
if (tagRuntimes == null || tagRuntimes.Count <= 0)
{
return Array.Empty<string>();
}
List<string> results = new List<string>(tagRuntimes.Count);
for (int i = 0; i < tagRuntimes.Count; i++)
{
TagRuntimeData tagRuntime = tagRuntimes[i];
if (tagRuntime == null || tagRuntime.TagType == TagType.None || tagRuntime.TotalStack <= 0)
{
continue;
}
string tagName = ResolveTagName(tagRuntime.TagType);
if (string.IsNullOrWhiteSpace(tagName))
{
continue;
}
results.Add(tagRuntime.TotalStack > 1 ? $"{tagName} x{tagRuntime.TotalStack}" : tagName);
}
return results.ToArray();
}
public static string[] BuildTowerTagTexts(TowerStatsData towerStats)
{
if (towerStats == null)
{
return Array.Empty<string>();
}
if (towerStats.TagRuntimes != null && towerStats.TagRuntimes.Length > 0)
{
return BuildTagTexts(towerStats.TagRuntimes);
}
return BuildTagTexts(towerStats.Tags);
}
public static string BuildTagDescriptionText(IReadOnlyList<TagType> tags)
{
if (tags == null || tags.Count <= 0)
{
return string.Empty;
}
List<string> results = new List<string>(tags.Count);
for (int i = 0; i < tags.Count; i++)
{
TagType tagType = tags[i];
if (tagType == TagType.None)
{
continue;
}
string tagName = ResolveTagName(tagType);
string tagDescription = ResolveTagDescription(tagType);
if (string.IsNullOrWhiteSpace(tagDescription))
{
continue;
}
results.Add(string.IsNullOrWhiteSpace(tagName) ? tagDescription : $"{tagName}: {tagDescription}");
}
return string.Join("\n", results);
}
public static string BuildTagDescriptionText(IReadOnlyList<TagRuntimeData> tagRuntimes)
{
if (tagRuntimes == null || tagRuntimes.Count <= 0)
{
return string.Empty;
}
List<string> results = new List<string>(tagRuntimes.Count);
for (int i = 0; i < tagRuntimes.Count; i++)
{
TagRuntimeData runtime = tagRuntimes[i];
if (runtime == null || runtime.TagType == TagType.None || runtime.TotalStack <= 0)
{
continue;
}
string tagName = ResolveTagName(runtime.TagType);
if (runtime.TotalStack > 1 && !string.IsNullOrWhiteSpace(tagName))
{
tagName = $"{tagName} x{runtime.TotalStack}";
}
string tagDescription = ResolveTagDescription(runtime.TagType);
if (string.IsNullOrWhiteSpace(tagDescription))
{
continue;
}
results.Add(string.IsNullOrWhiteSpace(tagName) ? tagDescription : $"{tagName}: {tagDescription}");
}
return string.Join("\n", results);
}
public static string ResolveTagDescription(TagType tagType)
{
if (tagType == TagType.None)
{
return string.Empty;
}
if (TagDefinitionRegistry.TryGetDefinition(tagType, out TagDefinition definition) &&
!string.IsNullOrWhiteSpace(definition.Description))
{
return definition.Description;
}
return string.Empty;
}
}
}