补全防御塔组装与介绍描述功能

- RepoForm.CombineArea:防御塔组装,直接继承所有组件的属性
- ItemDescUtility:根据属性构建不同的 ItemDescForm 的文本
This commit is contained in:
SepComet 2026-03-04 18:17:10 +08:00
parent e12d2e73b2
commit 1e5803f4c5
29 changed files with 832 additions and 168 deletions

1
.gitignore vendored
View File

@ -105,3 +105,4 @@ AGENTS.md
/[Aa]ssets/RawResources
/[Aa]ssets/RawResources.meta
/docs/TODO.md
/.dotnet

View File

@ -5,7 +5,7 @@ using UnityEngine;
namespace Components
{
[DisallowMultipleComponent]
public class DefenseTowerController : MonoBehaviour
public class TowerController : MonoBehaviour
{
private const string AttackRangeIndicatorObjectName = "AttackRangeIndicator";
private const int MinTowerLevel = 0;
@ -35,12 +35,12 @@ namespace Components
ResolveComponents();
}
public void OnInit(DefenseTowerStatsData stats)
public void OnInit(TowerStatsData stats)
{
OnInit(stats, MinTowerLevel);
}
public void OnInit(DefenseTowerStatsData stats, int towerLevel)
public void OnInit(TowerStatsData stats, int towerLevel)
{
ResolveComponents();
if (stats == null)

View File

@ -54,7 +54,7 @@ namespace GeometryTD.CustomComponent
MuzzleComponents = new List<MuzzleCompItemData>(_rewardInventory.MuzzleComponents),
BearingComponents = new List<BearingCompItemData>(_rewardInventory.BearingComponents),
BaseComponents = new List<BaseCompItemData>(_rewardInventory.BaseComponents),
Towers = new List<DefenseTowerItemData>(_rewardInventory.Towers)
Towers = new List<TowerItemData>(_rewardInventory.Towers)
};
}

View File

@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using GameFramework.DataTable;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityEngine;
@ -8,9 +11,13 @@ namespace GeometryTD.CustomComponent
{
public class PlayerInventoryComponent : GameFrameworkComponent
{
private const int TowerLevelCount = 5;
private BackpackInventoryData _inventory = new BackpackInventoryData();
private long _nextInstanceId = 1;
private bool _initialized;
private IDataTable<DRMuzzleComp> _drMuzzleComp;
private IDataTable<DRBearingComp> _drBearingComp;
private IDataTable<DRBaseComp> _drBaseComp;
public int Gold
{
@ -115,13 +122,13 @@ namespace GeometryTD.CustomComponent
{
for (int i = 0; i < gainedInventory.Towers.Count; i++)
{
DefenseTowerItemData source = gainedInventory.Towers[i];
TowerItemData source = gainedInventory.Towers[i];
if (source == null)
{
continue;
}
DefenseTowerItemData cloned = CloneTower(source);
TowerItemData cloned = CloneTower(source);
cloned.InstanceId = AllocateInstanceId();
_inventory.Towers.Add(cloned);
gainedTowerCount++;
@ -171,6 +178,58 @@ namespace GeometryTD.CustomComponent
_inventory.Gold += resolvedGain;
}
public bool TryAssembleTower(
long muzzleInstanceId,
long bearingInstanceId,
long baseInstanceId,
out TowerItemData assembledTower)
{
EnsureInitialized();
assembledTower = null;
if (muzzleInstanceId <= 0 || bearingInstanceId <= 0 || baseInstanceId <= 0)
{
return false;
}
if (!TryGetComponentById(_inventory.MuzzleComponents, muzzleInstanceId,
out MuzzleCompItemData muzzleComp) ||
!TryGetComponentById(_inventory.BearingComponents, bearingInstanceId,
out BearingCompItemData bearingComp) ||
!TryGetComponentById(_inventory.BaseComponents, baseInstanceId, out BaseCompItemData baseComp))
{
return false;
}
if (muzzleComp.IsAssembledIntoTower || bearingComp.IsAssembledIntoTower || baseComp.IsAssembledIntoTower)
{
return false;
}
if (!TryBuildTowerStats(muzzleComp, bearingComp, baseComp, out TowerStatsData stats))
{
return false;
}
long towerInstanceId = AllocateInstanceId();
TowerItemData tower = new TowerItemData
{
InstanceId = towerInstanceId,
Name = $"组装防御塔-{towerInstanceId}",
Rarity = ResolveAverageRarity(muzzleComp.Rarity, bearingComp.Rarity, baseComp.Rarity),
MuzzleComponentInstanceId = muzzleComp.InstanceId,
BearingComponentInstanceId = bearingComp.InstanceId,
BaseComponentInstanceId = baseComp.InstanceId,
Stats = stats
};
muzzleComp.IsAssembledIntoTower = true;
bearingComp.IsAssembledIntoTower = true;
baseComp.IsAssembledIntoTower = true;
_inventory.Towers.Add(tower);
assembledTower = CloneTower(tower);
return true;
}
public int ReduceAllTowerEndurance(float enduranceLoss)
{
EnsureInitialized();
@ -180,27 +239,250 @@ namespace GeometryTD.CustomComponent
return 0;
}
Dictionary<long, MuzzleCompItemData> muzzleMap = BuildComponentMap(_inventory.MuzzleComponents);
Dictionary<long, BearingCompItemData> bearingMap = BuildComponentMap(_inventory.BearingComponents);
Dictionary<long, BaseCompItemData> baseMap = BuildComponentMap(_inventory.BaseComponents);
int affectedCount = 0;
for (int i = 0; i < _inventory.Towers.Count; i++)
foreach (var tower in _inventory.Towers)
{
DefenseTowerItemData tower = _inventory.Towers[i];
if (tower == null)
{
continue;
}
float originalEndurance = tower.Endurance;
float nextEndurance = Mathf.Clamp(originalEndurance - resolvedLoss, 0f, 100f);
bool towerAffected = false;
if (muzzleMap.TryGetValue(tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComp))
{
towerAffected |= TryReduceComponentEndurance(muzzleComp, resolvedLoss);
}
if (bearingMap.TryGetValue(tower.BearingComponentInstanceId, out BearingCompItemData bearingComp))
{
towerAffected |= TryReduceComponentEndurance(bearingComp, resolvedLoss);
}
if (baseMap.TryGetValue(tower.BaseComponentInstanceId, out BaseCompItemData baseComp))
{
towerAffected |= TryReduceComponentEndurance(baseComp, resolvedLoss);
}
if (towerAffected)
{
affectedCount++;
}
}
return affectedCount;
}
private static bool TryReduceComponentEndurance(TowerCompItemData component, float enduranceLoss)
{
if (component == null)
{
return false;
}
float originalEndurance = component.Endurance;
float nextEndurance = Mathf.Clamp(originalEndurance - Mathf.Max(0f, enduranceLoss), 0f, 100f);
if (nextEndurance >= originalEndurance)
{
return false;
}
component.Endurance = nextEndurance;
return true;
}
private static Dictionary<long, TComp> BuildComponentMap<TComp>(List<TComp> components)
where TComp : TowerCompItemData
{
Dictionary<long, TComp> map = new Dictionary<long, TComp>();
if (components == null || components.Count <= 0)
{
return map;
}
foreach (var component in components)
{
if (component == null || component.InstanceId <= 0)
{
continue;
}
tower.Endurance = nextEndurance;
affectedCount++;
map[component.InstanceId] = component;
}
return affectedCount;
return map;
}
private static bool TryGetComponentById<TComp>(List<TComp> components, long instanceId, out TComp result)
where TComp : TowerCompItemData
{
result = null;
if (components == null || instanceId <= 0)
{
return false;
}
foreach (TComp component in components)
{
if (component != null && component.InstanceId == instanceId)
{
result = component;
return true;
}
}
return false;
}
private bool TryBuildTowerStats(
MuzzleCompItemData muzzleComp,
BearingCompItemData bearingComp,
BaseCompItemData baseComp,
out TowerStatsData stats)
{
stats = null;
if (muzzleComp == null || bearingComp == null || baseComp == null)
{
return false;
}
DRMuzzleComp muzzleConfig = EnsureMuzzleTable()?.GetDataRow(muzzleComp.ConfigId);
DRBearingComp bearingConfig = EnsureBearingTable()?.GetDataRow(bearingComp.ConfigId);
DRBaseComp baseConfig = EnsureBaseTable()?.GetDataRow(baseComp.ConfigId);
if (muzzleConfig == null || bearingConfig == null || baseConfig == null)
{
return false;
}
stats = new TowerStatsData
{
AttackDamage = BuildLevelIntArray(muzzleComp.AttackDamage, muzzleComp.Rarity,
muzzleConfig.AttackDamagePerLevel),
DamageRandomRate = Mathf.Max(0f, muzzleComp.DamageRandomRate),
RotateSpeed = BuildLevelFloatArray(bearingComp.RotateSpeed, bearingComp.Rarity,
bearingConfig.RotateSpeedPerLevel),
AttackRange = BuildLevelFloatArray(bearingComp.AttackRange, bearingComp.Rarity,
bearingConfig.AttackRangePerLevel),
AttackSpeed =
BuildLevelFloatArray(baseComp.AttackSpeed, baseComp.Rarity, baseConfig.AttackSpeedPerLevel),
AttackMethodType = muzzleComp.AttackMethodType,
AttackPropertyType = baseComp.AttackPropertyType,
Tags = MergeTags(muzzleComp.Tags, bearingComp.Tags, baseComp.Tags)
};
return true;
}
private static int[] BuildLevelIntArray(int[] rarityBaseArray, RarityType rarity, int perLevel)
{
int baseValue = ResolveRarityBaseValue(rarityBaseArray, rarity);
int[] values = new int[TowerLevelCount];
for (int i = 0; i < values.Length; i++)
{
values[i] = baseValue + perLevel * i;
}
return values;
}
private static float[] BuildLevelFloatArray(float[] rarityBaseArray, RarityType rarity, float perLevel)
{
float baseValue = ResolveRarityBaseValue(rarityBaseArray, rarity);
float[] values = new float[TowerLevelCount];
for (int i = 0; i < values.Length; i++)
{
values[i] = baseValue + perLevel * i;
}
return values;
}
private static float[] BuildConstantLevelFloatArray(float value)
{
float[] values = new float[TowerLevelCount];
for (int i = 0; i < values.Length; i++)
{
values[i] = value;
}
return values;
}
private static int ResolveRarityBaseValue(int[] rarityBaseArray, RarityType rarity)
{
if (rarityBaseArray == null || rarityBaseArray.Length <= 0)
{
return 0;
}
int rarityIndex = Mathf.Clamp((int)rarity - 1, 0, rarityBaseArray.Length - 1);
return rarityBaseArray[rarityIndex];
}
private static float ResolveRarityBaseValue(float[] rarityBaseArray, RarityType rarity)
{
if (rarityBaseArray == null || rarityBaseArray.Length <= 0)
{
return 0f;
}
int rarityIndex = Mathf.Clamp((int)rarity - 1, 0, rarityBaseArray.Length - 1);
return rarityBaseArray[rarityIndex];
}
private static TagType[] MergeTags(params TagType[][] sources)
{
HashSet<TagType> uniqueTags = new HashSet<TagType>();
if (sources != null)
{
for (int i = 0; i < sources.Length; i++)
{
TagType[] tags = sources[i];
if (tags == null)
{
continue;
}
for (int j = 0; j < tags.Length; j++)
{
uniqueTags.Add(tags[j]);
}
}
}
TagType[] mergedTags = new TagType[uniqueTags.Count];
uniqueTags.CopyTo(mergedTags);
return mergedTags;
}
private static RarityType ResolveAverageRarity(RarityType muzzleRarity, RarityType bearingRarity,
RarityType baseRarity)
{
float avg = ((int)muzzleRarity + (int)bearingRarity + (int)baseRarity) / 3f;
int rounded = Mathf.RoundToInt(avg);
int clamped = Mathf.Clamp(rounded, (int)RarityType.White, (int)RarityType.Red);
return (RarityType)clamped;
}
private IDataTable<DRMuzzleComp> EnsureMuzzleTable()
{
_drMuzzleComp ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
return _drMuzzleComp;
}
private IDataTable<DRBearingComp> EnsureBearingTable()
{
_drBearingComp ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
return _drBearingComp;
}
private IDataTable<DRBaseComp> EnsureBaseTable()
{
_drBaseComp ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
return _drBaseComp;
}
private void EnsureInitialized()
@ -228,9 +510,8 @@ namespace GeometryTD.CustomComponent
long maxInstanceId = 0;
if (_inventory.Towers != null)
{
for (int i = 0; i < _inventory.Towers.Count; i++)
foreach (var item in _inventory.Towers)
{
DefenseTowerItemData item = _inventory.Towers[i];
if (item != null)
{
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
@ -240,9 +521,8 @@ namespace GeometryTD.CustomComponent
if (_inventory.MuzzleComponents != null)
{
for (int i = 0; i < _inventory.MuzzleComponents.Count; i++)
foreach (var item in _inventory.MuzzleComponents)
{
MuzzleCompItemData item = _inventory.MuzzleComponents[i];
if (item != null)
{
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
@ -252,9 +532,8 @@ namespace GeometryTD.CustomComponent
if (_inventory.BearingComponents != null)
{
for (int i = 0; i < _inventory.BearingComponents.Count; i++)
foreach (var item in _inventory.BearingComponents)
{
BearingCompItemData item = _inventory.BearingComponents[i];
if (item != null)
{
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
@ -264,9 +543,8 @@ namespace GeometryTD.CustomComponent
if (_inventory.BaseComponents != null)
{
for (int i = 0; i < _inventory.BaseComponents.Count; i++)
foreach (var item in _inventory.BaseComponents)
{
BaseCompItemData item = _inventory.BaseComponents[i];
if (item != null)
{
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
@ -327,7 +605,7 @@ namespace GeometryTD.CustomComponent
{
for (int i = 0; i < source.Towers.Count; i++)
{
DefenseTowerItemData item = source.Towers[i];
TowerItemData item = source.Towers[i];
if (item != null)
{
cloned.Towers.Add(CloneTower(item));
@ -347,6 +625,7 @@ namespace GeometryTD.CustomComponent
Name = source.Name,
Rarity = source.Rarity,
Endurance = source.Endurance,
IsAssembledIntoTower = source.IsAssembledIntoTower,
Constraint = source.Constraint,
Tags = CloneTags(source.Tags),
AttackDamage = CloneIntArray(source.AttackDamage),
@ -364,6 +643,7 @@ namespace GeometryTD.CustomComponent
Name = source.Name,
Rarity = source.Rarity,
Endurance = source.Endurance,
IsAssembledIntoTower = source.IsAssembledIntoTower,
Constraint = source.Constraint,
Tags = CloneTags(source.Tags),
RotateSpeed = CloneFloatArray(source.RotateSpeed),
@ -380,6 +660,7 @@ namespace GeometryTD.CustomComponent
Name = source.Name,
Rarity = source.Rarity,
Endurance = source.Endurance,
IsAssembledIntoTower = source.IsAssembledIntoTower,
Constraint = source.Constraint,
Tags = CloneTags(source.Tags),
AttackSpeed = CloneFloatArray(source.AttackSpeed),
@ -387,14 +668,13 @@ namespace GeometryTD.CustomComponent
};
}
private static DefenseTowerItemData CloneTower(DefenseTowerItemData source)
private static TowerItemData CloneTower(TowerItemData source)
{
return new DefenseTowerItemData
return new TowerItemData
{
InstanceId = source.InstanceId,
Name = source.Name,
Rarity = source.Rarity,
Endurance = source.Endurance,
MuzzleComponentInstanceId = source.MuzzleComponentInstanceId,
BearingComponentInstanceId = source.BearingComponentInstanceId,
BaseComponentInstanceId = source.BaseComponentInstanceId,
@ -402,20 +682,20 @@ namespace GeometryTD.CustomComponent
};
}
private static DefenseTowerStatsData CloneTowerStats(DefenseTowerStatsData source)
private static TowerStatsData CloneTowerStats(TowerStatsData source)
{
if (source == null)
{
return new DefenseTowerStatsData();
return new TowerStatsData();
}
return new DefenseTowerStatsData
return new TowerStatsData
{
AttackDamage = source.AttackDamage,
AttackDamage = CloneIntArray(source.AttackDamage),
DamageRandomRate = source.DamageRandomRate,
RotateSpeed = source.RotateSpeed,
AttackRange = source.AttackRange,
AttackSpeed = source.AttackSpeed,
RotateSpeed = CloneFloatArray(source.RotateSpeed),
AttackRange = CloneFloatArray(source.AttackRange),
AttackSpeed = CloneFloatArray(source.AttackSpeed),
AttackMethodType = source.AttackMethodType,
AttackPropertyType = source.AttackPropertyType,
Tags = CloneTags(source.Tags)

View File

@ -32,6 +32,6 @@ namespace GeometryTD.Definition
/// <summary>
/// 背包中的防御塔实例。
/// </summary>
public List<DefenseTowerItemData> Towers { get; set; } = new List<DefenseTowerItemData>();
public List<TowerItemData> Towers { get; set; } = new List<TowerItemData>();
}
}

View File

@ -5,49 +5,20 @@ namespace GeometryTD.Definition
[StructLayout(LayoutKind.Auto)]
public struct ImpactData
{
private readonly CampType m_Camp;
private readonly int m_HP;
private readonly int m_Attack;
private readonly int m_Defense;
public ImpactData(CampType camp, int hp, int attack, int defense)
{
m_Camp = camp;
m_HP = hp;
m_Attack = attack;
m_Defense = defense;
Camp = camp;
HP = hp;
Attack = attack;
Defense = defense;
}
public CampType Camp
{
get
{
return m_Camp;
}
}
public CampType Camp { get; }
public int HP
{
get
{
return m_HP;
}
}
public int HP { get; }
public int Attack
{
get
{
return m_Attack;
}
}
public int Attack { get; }
public int Defense
{
get
{
return m_Defense;
}
}
public int Defense { get; }
}
}

View File

@ -37,6 +37,7 @@ namespace GeometryTD.Definition
/// 组件当前耐久0~100
/// </summary>
public float Endurance { get; set; } = 100f;
public bool IsAssembledIntoTower { get; set; }
/// <summary>
/// 组件约束(先沿用 DataTable 原定义)。

View File

@ -7,10 +7,10 @@ namespace GeometryTD.Definition
/// 注意:这里是塔实例的独立值,不通过组件引用实时计算。
/// </summary>
[Serializable]
public sealed class DefenseTowerStatsData
public sealed class TowerStatsData
{
public int[] AttackDamage { get; set; }
public float[] DamageRandomRate { get; set; }
public float DamageRandomRate { get; set; }
public float[] RotateSpeed { get; set; }
public float[] AttackRange { get; set; }
public float[] AttackSpeed { get; set; }
@ -23,7 +23,7 @@ namespace GeometryTD.Definition
/// 背包内防御塔实例数据。
/// </summary>
[Serializable]
public sealed class DefenseTowerItemData
public sealed class TowerItemData
{
/// <summary>
/// 防御塔实例唯一 Id。
@ -40,11 +40,6 @@ namespace GeometryTD.Definition
/// </summary>
public RarityType Rarity { get; set; }
/// <summary>
/// 当前耐久0~100
/// </summary>
public float Endurance { get; set; } = 100f;
/// <summary>
/// 构成该防御塔的枪口组件实例 Id。
/// </summary>
@ -63,6 +58,6 @@ namespace GeometryTD.Definition
/// <summary>
/// 防御塔独立属性,不依赖组件对象引用。
/// </summary>
public DefenseTowerStatsData Stats { get; set; } = new DefenseTowerStatsData();
public TowerStatsData Stats { get; set; } = new TowerStatsData();
}
}

View File

@ -5,25 +5,25 @@ using UnityEngine;
namespace GeometryTD.Entity.EntityData
{
[Serializable]
public class DefenseTowerData : EntityDataBase
public class TowerData : EntityDataBase
{
[SerializeField] private DefenseTowerStatsData _stats = new DefenseTowerStatsData();
[SerializeField] private TowerStatsData _stats = new TowerStatsData();
[SerializeField] private int _towerLevel = 0;
public DefenseTowerData(int entityId, int typeId, Vector3 position, Quaternion rotation,
DefenseTowerStatsData stats, int towerLevel = 0)
public TowerData(int entityId, int typeId, Vector3 position, Quaternion rotation,
TowerStatsData stats, int towerLevel = 0)
: base(entityId, typeId)
{
Position = position;
Rotation = rotation;
_stats = stats ?? new DefenseTowerStatsData();
_stats = stats ?? new TowerStatsData();
_towerLevel = Mathf.Max(0, towerLevel);
}
public DefenseTowerStatsData Stats
public TowerStatsData Stats
{
get => _stats;
set => _stats = value ?? new DefenseTowerStatsData();
set => _stats = value ?? new TowerStatsData();
}
public int TowerLevel

View File

@ -46,9 +46,9 @@ namespace GeometryTD
entityComponent.ShowEntity(typeof(EnemyEntity), "Enemy", Constant.AssetPriority.EnemyAsset, data);
}
public static void ShowDefenseTower(this EntityComponent entityComponent, DefenseTowerData data)
public static void ShowDefenseTower(this EntityComponent entityComponent, TowerData data)
{
entityComponent.ShowEntity(typeof(DefenseTowerEntity), "Tower", Constant.AssetPriority.EnemyAsset, data);
entityComponent.ShowEntity(typeof(TowerEntity), "Tower", Constant.AssetPriority.EnemyAsset, data);
}
public static void ShowBullet(this EntityComponent entityComponent, BulletData data)

View File

@ -5,16 +5,16 @@ using UnityGameFramework.Runtime;
namespace GeometryTD.Entity
{
public class DefenseTowerEntity : EntityBase
public class TowerEntity : EntityBase
{
private DefenseTowerController _towerController;
private TowerController _towerController;
public void SetAttackRangeVisible(bool visible)
{
_towerController?.SetAttackRangeVisible(visible);
}
public bool TryApplyStats(DefenseTowerStatsData stats, int towerLevel)
public bool TryApplyStats(TowerStatsData stats, int towerLevel)
{
if (_towerController == null || stats == null)
{
@ -29,7 +29,7 @@ namespace GeometryTD.Entity
{
base.OnInit(userData);
_towerController = GetComponent<DefenseTowerController>();
_towerController = GetComponent<TowerController>();
if (_towerController == null)
{
Log.Error("DefenseTowerController is missing on tower entity '{0}'.", name);
@ -45,7 +45,7 @@ namespace GeometryTD.Entity
return;
}
if (userData is not DefenseTowerData towerData)
if (userData is not TowerData towerData)
{
Log.Warning("DefenseTowerData is invalid for tower entity '{0}'.", Id);
_towerController.OnReset();

View File

@ -0,0 +1,32 @@
using GameFramework;
using GameFramework.Event;
namespace GeometryTD.CustomEvent
{
public sealed class RepoCombineRequestedEventArgs : GameEventArgs
{
public static int EventId => typeof(RepoCombineRequestedEventArgs).GetHashCode();
public override int Id => EventId;
public long MuzzleItemId { get; private set; }
public long BearingItemId { get; private set; }
public long BaseItemId { get; private set; }
public static RepoCombineRequestedEventArgs Create(long muzzleItemId, long bearingItemId, long baseItemId)
{
RepoCombineRequestedEventArgs args = ReferencePool.Acquire<RepoCombineRequestedEventArgs>();
args.MuzzleItemId = muzzleItemId;
args.BearingItemId = bearingItemId;
args.BaseItemId = baseItemId;
return args;
}
public override void Clear()
{
MuzzleItemId = 0;
BearingItemId = 0;
BaseItemId = 0;
}
}
}

View File

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

View File

@ -16,7 +16,7 @@ namespace GeometryTD.Map
private readonly Dictionary<Vector3Int, int> _towerEntityIdByFoundationCell = new();
private readonly Dictionary<int, Vector3Int> _foundationCellByTowerEntityId = new();
private readonly Dictionary<int, DefenseTowerStatsData> _towerStatsByEntityId = new();
private readonly Dictionary<int, TowerStatsData> _towerStatsByEntityId = new();
private readonly Dictionary<int, int> _towerLevelByEntityId = new();
private readonly List<int> _towerEntityIdBuffer = new();
@ -30,8 +30,8 @@ namespace GeometryTD.Map
return false;
}
DefenseTowerStatsData towerStats =
_towerStatsByEntityId.TryGetValue(towerEntityId, out DefenseTowerStatsData cachedStats)
TowerStatsData towerStats =
_towerStatsByEntityId.TryGetValue(towerEntityId, out TowerStatsData cachedStats)
? cachedStats
: null;
int currentLevel = GetTowerLevel(towerEntityId);
@ -61,7 +61,7 @@ namespace GeometryTD.Map
return false;
}
DefenseTowerStatsData towerStats = BuildTowerStats(buildIndex);
TowerStatsData towerStats = BuildTowerStats(buildIndex);
if (!TryShowTowerEntity(foundationCell, towerStats, towerTypeId, tilemap, out int newTowerEntityId))
{
addCoin?.Invoke(buildCost);
@ -87,8 +87,8 @@ namespace GeometryTD.Map
return false;
}
DefenseTowerStatsData towerStats =
_towerStatsByEntityId.TryGetValue(towerEntityId, out DefenseTowerStatsData cachedStats)
TowerStatsData towerStats =
_towerStatsByEntityId.TryGetValue(towerEntityId, out TowerStatsData cachedStats)
? CloneTowerStats(cachedStats)
: BuildTowerStats(0);
int currentTowerLevel = GetTowerLevel(towerEntityId);
@ -177,7 +177,7 @@ namespace GeometryTD.Map
return Mathf.Max(0, buildTowerCosts[buildIndex]);
}
private static bool TryShowTowerEntity(Vector3Int foundationCell, DefenseTowerStatsData towerStats,
private static bool TryShowTowerEntity(Vector3Int foundationCell, TowerStatsData towerStats,
int towerTypeId,
Tilemap tilemap, out int towerEntityId)
{
@ -191,7 +191,7 @@ namespace GeometryTD.Map
int typeId = towerTypeId > 0 ? towerTypeId : DefaultTowerTypeId;
Vector3 towerPosition = tilemap != null ? tilemap.GetCellCenterWorld(foundationCell) : foundationCell;
towerPosition.z = 0f;
var towerData = new DefenseTowerData(entityId, typeId, towerPosition, Quaternion.identity, towerStats,
var towerData = new TowerData(entityId, typeId, towerPosition, Quaternion.identity, towerStats,
MinTowerLevel);
GameEntry.Entity.ShowDefenseTower(towerData);
@ -213,15 +213,15 @@ namespace GeometryTD.Map
}
}
private static DefenseTowerStatsData BuildTowerStats(int buildIndex)
private static TowerStatsData BuildTowerStats(int buildIndex)
{
switch (buildIndex)
{
case 0:
return new DefenseTowerStatsData
return new TowerStatsData
{
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
DamageRandomRate = 0f,
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
@ -230,10 +230,10 @@ namespace GeometryTD.Map
Tags = Array.Empty<TagType>()
};
case 1:
return new DefenseTowerStatsData
return new TowerStatsData
{
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
DamageRandomRate = 0f,
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
@ -242,10 +242,10 @@ namespace GeometryTD.Map
Tags = Array.Empty<TagType>()
};
case 2:
return new DefenseTowerStatsData
return new TowerStatsData
{
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
DamageRandomRate = 0f,
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
@ -254,10 +254,10 @@ namespace GeometryTD.Map
Tags = Array.Empty<TagType>()
};
case 3:
return new DefenseTowerStatsData
return new TowerStatsData
{
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
DamageRandomRate = 0f,
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
@ -266,10 +266,10 @@ namespace GeometryTD.Map
Tags = Array.Empty<TagType>()
};
default:
return new DefenseTowerStatsData
return new TowerStatsData
{
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
DamageRandomRate = 0f,
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
@ -280,7 +280,7 @@ namespace GeometryTD.Map
}
}
private static DefenseTowerStatsData CloneTowerStats(DefenseTowerStatsData source)
private static TowerStatsData CloneTowerStats(TowerStatsData source)
{
if (source == null)
{
@ -288,12 +288,10 @@ namespace GeometryTD.Map
}
TagType[] copiedTags = source.Tags != null ? (TagType[])source.Tags.Clone() : Array.Empty<TagType>();
return new DefenseTowerStatsData
return new TowerStatsData
{
AttackDamage = source.AttackDamage != null ? (int[])source.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = source.DamageRandomRate != null
? (float[])source.DamageRandomRate.Clone()
: Array.Empty<float>(),
DamageRandomRate = source.DamageRandomRate,
RotateSpeed = source.RotateSpeed != null ? (float[])source.RotateSpeed.Clone() : Array.Empty<float>(),
AttackRange = source.AttackRange != null ? (float[])source.AttackRange.Clone() : Array.Empty<float>(),
AttackSpeed = source.AttackSpeed != null ? (float[])source.AttackSpeed.Clone() : Array.Empty<float>(),
@ -313,11 +311,10 @@ namespace GeometryTD.Map
return Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel);
}
private static int ResolveMaxTowerLevel(DefenseTowerStatsData stats)
private static int ResolveMaxTowerLevel(TowerStatsData stats)
{
int maxCount = Mathf.Max(
GetLength(stats?.AttackDamage),
GetLength(stats?.DamageRandomRate),
GetLength(stats?.RotateSpeed),
GetLength(stats?.AttackRange),
GetLength(stats?.AttackSpeed));
@ -329,14 +326,14 @@ namespace GeometryTD.Map
return Mathf.Clamp(maxCount - 1, MinTowerLevel, MaxTowerLevel);
}
private static bool TryApplyTowerStats(int towerEntityId, DefenseTowerStatsData towerStats, int towerLevel)
private static bool TryApplyTowerStats(int towerEntityId, TowerStatsData towerStats, int towerLevel)
{
if (towerEntityId == 0 || towerStats == null || GameEntry.Entity == null)
{
return false;
}
if (GameEntry.Entity.GetGameEntity(towerEntityId) is not DefenseTowerEntity towerEntity)
if (GameEntry.Entity.GetGameEntity(towerEntityId) is not TowerEntity towerEntity)
{
return false;
}

View File

@ -120,7 +120,7 @@ namespace GeometryTD.Map
}
EntityBase gameEntity = GameEntry.Entity.GetGameEntity(towerEntityId);
if (gameEntity is not DefenseTowerEntity towerEntity)
if (gameEntity is not TowerEntity towerEntity)
{
return false;
}

View File

@ -17,6 +17,7 @@ namespace GeometryTD.UI
{
public string Title;
public string TypeText;
public string Description;
public TagType[] Tags;
}
@ -128,7 +129,11 @@ namespace GeometryTD.UI
IconAreaContext = BuildIconAreaContext(tower)
});
AddItemDescSeed(tower.InstanceId, tower.Name, "Tower",
AddItemDescSeed(
tower.InstanceId,
tower.Name,
"Tower",
ItemDescUtility.BuildTowerDesc(tower.Stats) ?? string.Empty,
tower.Stats != null ? tower.Stats.Tags : null);
}
}
@ -151,7 +156,12 @@ namespace GeometryTD.UI
IconAreaContext = BuildIconAreaContext(item)
});
AddItemDescSeed(item.InstanceId, item.Name, BuildComponentTypeText(item.SlotType), item.Tags);
AddItemDescSeed(
item.InstanceId,
item.Name,
BuildComponentTypeText(item.SlotType),
ItemDescUtility.BuildMuzzleDesc(item) ?? string.Empty,
item.Tags);
}
}
@ -172,7 +182,12 @@ namespace GeometryTD.UI
IconAreaContext = BuildIconAreaContext(item)
});
AddItemDescSeed(item.InstanceId, item.Name, BuildComponentTypeText(item.SlotType), item.Tags);
AddItemDescSeed(
item.InstanceId,
item.Name,
BuildComponentTypeText(item.SlotType),
ItemDescUtility.BuildBearingDesc(item) ?? string.Empty,
item.Tags);
}
}
@ -194,14 +209,19 @@ namespace GeometryTD.UI
IconAreaContext = BuildIconAreaContext(item)
});
AddItemDescSeed(item.InstanceId, item.Name, BuildComponentTypeText(item.SlotType), item.Tags);
AddItemDescSeed(
item.InstanceId,
item.Name,
BuildComponentTypeText(item.SlotType),
ItemDescUtility.BuildBaseDesc(item) ?? string.Empty,
item.Tags);
}
}
return itemContexts.ToArray();
}
private void AddItemDescSeed(long itemId, string title, string typeText, TagType[] tags)
private void AddItemDescSeed(long itemId, string title, string typeText, string description, TagType[] tags)
{
if (itemId <= 0)
{
@ -212,6 +232,7 @@ namespace GeometryTD.UI
{
Title = string.IsNullOrWhiteSpace(title) ? $"Item {itemId}" : title,
TypeText = typeText ?? string.Empty,
Description = description ?? string.Empty,
Tags = CloneTags(tags)
};
}
@ -233,7 +254,7 @@ namespace GeometryTD.UI
};
}
private static IconAreaContext BuildIconAreaContext(DefenseTowerItemData tower)
private static IconAreaContext BuildIconAreaContext(TowerItemData tower)
{
if (tower == null)
{
@ -308,7 +329,7 @@ namespace GeometryTD.UI
{
Title = seed.Title,
TypeText = seed.TypeText,
Description = string.Empty,
Description = seed.Description ?? string.Empty,
Price = 0,
TargetPos = args.TargetPos,
Tags = CloneTags(seed.Tags)

View File

@ -19,6 +19,7 @@ namespace GeometryTD.UI
{
public string Title;
public string TypeText;
public string Description;
public TagType[] Tags;
}
@ -34,6 +35,7 @@ namespace GeometryTD.UI
GameEntry.Event.Subscribe(RepoItemDetailRequestedEventArgs.EventId, OnRepoItemDetailRequested);
GameEntry.Event.Subscribe(RepoItemDragEndedEventArgs.EventId, OnRepoItemDragEnded);
GameEntry.Event.Subscribe(CombineSlotClickedEventArgs.EventId, OnCombineSlotClicked);
GameEntry.Event.Subscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested);
GameEntry.Event.Subscribe(RepoFormReturnEventArgs.EventId, OnRepoFormReturn);
}
@ -42,6 +44,7 @@ namespace GeometryTD.UI
GameEntry.Event.Unsubscribe(RepoItemDetailRequestedEventArgs.EventId, OnRepoItemDetailRequested);
GameEntry.Event.Unsubscribe(RepoItemDragEndedEventArgs.EventId, OnRepoItemDragEnded);
GameEntry.Event.Unsubscribe(CombineSlotClickedEventArgs.EventId, OnCombineSlotClicked);
GameEntry.Event.Unsubscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested);
GameEntry.Event.Unsubscribe(RepoFormReturnEventArgs.EventId, OnRepoFormReturn);
}
@ -103,7 +106,7 @@ namespace GeometryTD.UI
if (rawData.Inventory.Towers != null)
{
foreach (DefenseTowerItemData tower in rawData.Inventory.Towers)
foreach (TowerItemData tower in rawData.Inventory.Towers)
{
if (tower == null)
{
@ -117,7 +120,11 @@ namespace GeometryTD.UI
ComponentSlotType = TowerCompSlotType.None,
IconAreaContext = BuildIconAreaContext(tower)
});
AddItemDescSeed(tower.InstanceId, tower.Name, "Tower",
AddItemDescSeed(
tower.InstanceId,
tower.Name,
"Tower",
ItemDescUtility.BuildTowerDesc(tower.Stats) ?? string.Empty,
tower.Stats != null ? tower.Stats.Tags : null);
}
}
@ -126,7 +133,7 @@ namespace GeometryTD.UI
{
foreach (MuzzleCompItemData item in rawData.Inventory.MuzzleComponents)
{
if (item == null)
if (item == null || item.IsAssembledIntoTower)
{
continue;
}
@ -138,7 +145,12 @@ namespace GeometryTD.UI
ComponentSlotType = TowerCompSlotType.Muzzle,
IconAreaContext = BuildIconAreaContext(item)
});
AddItemDescSeed(item.InstanceId, item.Name, BuildComponentTypeText(item.SlotType), item.Tags);
AddItemDescSeed(
item.InstanceId,
item.Name,
BuildComponentTypeText(item.SlotType),
ItemDescUtility.BuildMuzzleDesc(item) ?? string.Empty,
item.Tags);
}
}
@ -146,7 +158,7 @@ namespace GeometryTD.UI
{
foreach (BearingCompItemData item in rawData.Inventory.BearingComponents)
{
if (item == null)
if (item == null || item.IsAssembledIntoTower)
{
continue;
}
@ -158,7 +170,12 @@ namespace GeometryTD.UI
ComponentSlotType = TowerCompSlotType.Bearing,
IconAreaContext = BuildIconAreaContext(item)
});
AddItemDescSeed(item.InstanceId, item.Name, BuildComponentTypeText(item.SlotType), item.Tags);
AddItemDescSeed(
item.InstanceId,
item.Name,
BuildComponentTypeText(item.SlotType),
ItemDescUtility.BuildBearingDesc(item) ?? string.Empty,
item.Tags);
}
}
@ -166,7 +183,7 @@ namespace GeometryTD.UI
{
foreach (BaseCompItemData item in rawData.Inventory.BaseComponents)
{
if (item == null)
if (item == null || item.IsAssembledIntoTower)
{
continue;
}
@ -178,7 +195,12 @@ namespace GeometryTD.UI
ComponentSlotType = TowerCompSlotType.Base,
IconAreaContext = BuildIconAreaContext(item)
});
AddItemDescSeed(item.InstanceId, item.Name, BuildComponentTypeText(item.SlotType), item.Tags);
AddItemDescSeed(
item.InstanceId,
item.Name,
BuildComponentTypeText(item.SlotType),
ItemDescUtility.BuildBaseDesc(item) ?? string.Empty,
item.Tags);
}
}
@ -203,7 +225,7 @@ namespace GeometryTD.UI
_itemContextMap[itemContext.InstanceId] = itemContext;
}
private void AddItemDescSeed(long itemId, string title, string typeText, TagType[] tags)
private void AddItemDescSeed(long itemId, string title, string typeText, string description, TagType[] tags)
{
if (itemId <= 0)
{
@ -214,6 +236,7 @@ namespace GeometryTD.UI
{
Title = string.IsNullOrWhiteSpace(title) ? $"Item {itemId}" : title,
TypeText = typeText ?? string.Empty,
Description = description ?? string.Empty,
Tags = CloneTags(tags)
};
}
@ -235,7 +258,7 @@ namespace GeometryTD.UI
};
}
private static IconAreaContext BuildIconAreaContext(DefenseTowerItemData tower)
private static IconAreaContext BuildIconAreaContext(TowerItemData tower)
{
if (tower == null)
{
@ -302,7 +325,7 @@ namespace GeometryTD.UI
{
Title = seed.Title,
TypeText = seed.TypeText,
Description = string.Empty,
Description = seed.Description ?? string.Empty,
Price = 0,
TargetPos = args.TargetPos,
Tags = CloneTags(seed.Tags)
@ -367,6 +390,36 @@ namespace GeometryTD.UI
this.CloseUI();
}
private void OnRepoCombineRequested(object sender, GameEventArgs e)
{
if (!IsEventFromCurrentForm(sender))
{
return;
}
if (!(e is RepoCombineRequestedEventArgs args))
{
return;
}
if (_useCase == null || Form == null)
{
return;
}
if (!_useCase.TryAssembleTower(args.MuzzleItemId, args.BearingItemId, args.BaseItemId))
{
return;
}
RepoFormRawData latestRawData = _useCase.CreateInitialModel();
RepoFormContext latestContext = BuildContext(latestRawData);
if (latestContext != null)
{
Form.RefreshUI(latestContext);
}
}
private bool IsEventFromCurrentForm(object sender)
{
if (Form == null)

View File

@ -15,6 +15,20 @@ namespace GeometryTD.UI
};
}
public bool TryAssembleTower(long muzzleItemId, long bearingItemId, long baseItemId)
{
if (GameEntry.PlayerInventory == null)
{
return false;
}
return GameEntry.PlayerInventory.TryAssembleTower(
muzzleItemId,
bearingItemId,
baseItemId,
out _);
}
public static BackpackInventoryData SampleInventory()
{
BackpackInventoryData inventory = new BackpackInventoryData
@ -29,6 +43,7 @@ namespace GeometryTD.UI
Name = "元素枪口",
Rarity = RarityType.Green,
Endurance = 90f,
IsAssembledIntoTower = true,
AttackDamage = new[] { 20, 30, 40, 50, 80 },
DamageRandomRate = 0.05f,
AttackMethodType = AttackMethodType.NormalBullet,
@ -36,6 +51,34 @@ namespace GeometryTD.UI
Tags = new[] { TagType.Fire }
};
inventory.MuzzleComponents.Add(muzzle);
inventory.MuzzleComponents.Add(new MuzzleCompItemData
{
InstanceId = 10002,
ConfigId = 2,
Name = "控制枪口",
Rarity = RarityType.Blue,
Endurance = 100f,
IsAssembledIntoTower = false,
AttackDamage = new[] { 30, 50, 70, 90, 100 },
DamageRandomRate = 0.01f,
AttackMethodType = AttackMethodType.NormalBullet,
Constraint = string.Empty,
Tags = new[] { TagType.Ice, TagType.FreezeMask }
});
inventory.MuzzleComponents.Add(new MuzzleCompItemData
{
InstanceId = 10003,
ConfigId = 3,
Name = "穿透枪口",
Rarity = RarityType.Purple,
Endurance = 97f,
IsAssembledIntoTower = false,
AttackDamage = new[] { 50, 55, 60, 80, 90 },
DamageRandomRate = 0.02f,
AttackMethodType = AttackMethodType.NormalBullet,
Constraint = string.Empty,
Tags = new[] { TagType.Pierce, TagType.Crit }
});
BearingCompItemData bearing = new BearingCompItemData
{
@ -44,12 +87,39 @@ namespace GeometryTD.UI
Name = "元素轴承",
Rarity = RarityType.Green,
Endurance = 95f,
IsAssembledIntoTower = true,
RotateSpeed = new[] { 10f, 12f, 13f, 14f, 15f },
AttackRange = new[] { 2f, 2f, 2f, 2f, 2f },
Constraint = string.Empty,
Tags = new[] { TagType.Fire }
};
inventory.BearingComponents.Add(bearing);
inventory.BearingComponents.Add(new BearingCompItemData
{
InstanceId = 20002,
ConfigId = 2,
Name = "控制轴承",
Rarity = RarityType.Blue,
Endurance = 100f,
IsAssembledIntoTower = false,
RotateSpeed = new[] { 20f, 25f, 30f, 32f, 35f },
AttackRange = new[] { 6f, 6.5f, 7f, 8f, 8f },
Constraint = string.Empty,
Tags = new[] { TagType.Ice, TagType.Shatter }
});
inventory.BearingComponents.Add(new BearingCompItemData
{
InstanceId = 20003,
ConfigId = 3,
Name = "穿透轴承",
Rarity = RarityType.Purple,
Endurance = 96f,
IsAssembledIntoTower = false,
RotateSpeed = new[] { 60f, 70f, 80f, 90f, 100f },
AttackRange = new[] { 4f, 4.5f, 5f, 5.5f, 6f },
Constraint = string.Empty,
Tags = new[] { TagType.Pierce, TagType.Overpenetrate }
});
BaseCompItemData baseComp = new BaseCompItemData
{
@ -58,26 +128,52 @@ namespace GeometryTD.UI
Name = "元素底座",
Rarity = RarityType.Green,
Endurance = 88f,
IsAssembledIntoTower = true,
AttackSpeed = new[] { 2f, 1.5f, 1f, 0.8f, 0.7f },
AttackPropertyType = AttackPropertyType.Fire,
Constraint = string.Empty,
Tags = new[] { TagType.Fire }
};
inventory.BaseComponents.Add(baseComp);
inventory.BaseComponents.Add(new BaseCompItemData
{
InstanceId = 30002,
ConfigId = 2,
Name = "控制底座",
Rarity = RarityType.Blue,
Endurance = 100f,
IsAssembledIntoTower = false,
AttackSpeed = new[] { 4f, 4.2f, 4.4f, 4.6f, 4.8f },
AttackPropertyType = AttackPropertyType.Ice,
Constraint = string.Empty,
Tags = new[] { TagType.Ice, TagType.AbsoluteZero }
});
inventory.BaseComponents.Add(new BaseCompItemData
{
InstanceId = 30003,
ConfigId = 3,
Name = "穿透底座",
Rarity = RarityType.Purple,
Endurance = 95f,
IsAssembledIntoTower = false,
AttackSpeed = new[] { 1f, 1f, 1f, 1f, 1f },
AttackPropertyType = AttackPropertyType.Physics,
Constraint = string.Empty,
Tags = new[] { TagType.Pierce, TagType.Execution }
});
DefenseTowerItemData tower = new DefenseTowerItemData
TowerItemData tower = new TowerItemData
{
InstanceId = 90001,
Name = "测试防御塔-A",
Rarity = RarityType.Green,
Endurance = 92f,
MuzzleComponentInstanceId = muzzle.InstanceId,
BearingComponentInstanceId = bearing.InstanceId,
BaseComponentInstanceId = baseComp.InstanceId,
Stats = new DefenseTowerStatsData
Stats = new TowerStatsData
{
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
DamageRandomRate = 0f,
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
@ -88,19 +184,18 @@ namespace GeometryTD.UI
};
inventory.Towers.Add(tower);
inventory.Towers.Add(new DefenseTowerItemData
inventory.Towers.Add(new TowerItemData
{
InstanceId = 90002,
Name = "测试防御塔-B",
Rarity = RarityType.Blue,
Endurance = 100f,
MuzzleComponentInstanceId = 0,
BearingComponentInstanceId = 0,
BaseComponentInstanceId = 0,
Stats = new DefenseTowerStatsData
Stats = new TowerStatsData
{
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
DamageRandomRate = 0.1f,
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },

View File

@ -1,4 +1,5 @@
using GeometryTD.Definition;
using GeometryTD.CustomEvent;
using UnityEngine;
using UnityEngine.EventSystems;
@ -42,7 +43,14 @@ namespace GeometryTD.UI
public void OnCombineClick()
{
// TODO: Combine logic.
if (!TryGetBoundItemId(TowerCompSlotType.Muzzle, out long muzzleItemId) ||
!TryGetBoundItemId(TowerCompSlotType.Bearing, out long bearingItemId) ||
!TryGetBoundItemId(TowerCompSlotType.Base, out long baseItemId))
{
return;
}
GameEntry.Event.Fire(this, RepoCombineRequestedEventArgs.Create(muzzleItemId, bearingItemId, baseItemId));
}
public bool TryAssignItem(RepoItemContext itemContext)
@ -127,5 +135,18 @@ namespace GeometryTD.UI
return null;
}
private bool TryGetBoundItemId(TowerCompSlotType slotType, out long boundItemId)
{
boundItemId = 0;
CombineSlotItem slot = FindSlot(slotType);
if (slot == null || !slot.HasItem || slot.BoundItemId <= 0)
{
return false;
}
boundItemId = slot.BoundItemId;
return true;
}
}
}

View File

@ -61,17 +61,17 @@ namespace GeometryTD.CustomUtility
item.SlotType);
}
public static Color GenerateForTower(DefenseTowerItemData tower)
public static Color GenerateForTower(TowerItemData tower)
{
if (tower?.Stats == null)
{
return Color.white;
}
DefenseTowerStatsData stats = tower.Stats;
TowerStatsData stats = tower.Stats;
return GenerateColor(
ResolveArrayValue(stats.AttackDamage),
ResolveArrayValue(stats.DamageRandomRate),
stats.DamageRandomRate,
ResolveArrayValue(stats.RotateSpeed),
ResolveArrayValue(stats.AttackRange),
ResolveArrayValue(stats.AttackSpeed),
@ -127,14 +127,16 @@ namespace GeometryTD.CustomUtility
return;
}
if (!s_HasMuzzleRange && TryResolveMuzzleRange(out StatRange attackDamageRange, out StatRange damageRandomRateRange))
if (!s_HasMuzzleRange &&
TryResolveMuzzleRange(out StatRange attackDamageRange, out StatRange damageRandomRateRange))
{
_attackDamageRange = attackDamageRange;
_damageRandomRateRange = damageRandomRateRange;
s_HasMuzzleRange = true;
}
if (!s_HasBearingRange && TryResolveBearingRange(out StatRange rotateSpeedRange, out StatRange attackRangeRange))
if (!s_HasBearingRange &&
TryResolveBearingRange(out StatRange rotateSpeedRange, out StatRange attackRangeRange))
{
_rotateSpeedRange = rotateSpeedRange;
_attackRangeRange = attackRangeRange;
@ -181,7 +183,8 @@ namespace GeometryTD.CustomUtility
}
IncludeValues(row.AttackDamage, ref minAttackDamage, ref maxAttackDamage, ref hasAttackDamage);
IncludeValue(Mathf.Max(0f, row.DamageRandomRate), ref minDamageRandomRate, ref maxDamageRandomRate, ref hasDamageRandomRate);
IncludeValue(Mathf.Max(0f, row.DamageRandomRate), ref minDamageRandomRate, ref maxDamageRandomRate,
ref hasDamageRandomRate);
}
if (!hasAttackDamage || !hasDamageRandomRate)

View File

@ -0,0 +1,142 @@
using System;
using System.Text;
using GeometryTD.Definition;
namespace GeometryTD.CustomUtility
{
public static class ItemDescUtility
{
public static string BuildTowerDesc(TowerStatsData towerData)
{
StringBuilder sb = new StringBuilder();
// MuzzleComp
sb.Append("攻击伤害:");
for (int i = 0; i < towerData.AttackDamage.Length; i++)
{
if (i != 0) sb.Append("|");
sb.Append(towerData.AttackDamage[i]);
}
sb.Append('\n');
sb.Append($"伤害浮动:{towerData.DamageRandomRate:P0}\n");
sb.Append($"攻击方式:{ConvertAttackMethod(towerData.AttackMethodType)}\n");
// BearingComp
sb.Append("旋转速度:");
for (int i = 0; i < towerData.RotateSpeed.Length; i++)
{
if (i != 0) sb.Append("|");
sb.Append(towerData.RotateSpeed[i]);
}
sb.Append('\n');
sb.Append("攻击范围:");
for (int i = 0; i < towerData.AttackRange.Length; i++)
{
if (i != 0) sb.Append("|");
sb.Append(towerData.AttackRange[i]);
}
sb.Append('\n');
// BaseComp
sb.Append("攻击速度:");
for (int i = 0; i < towerData.AttackSpeed.Length; i++)
{
if (i != 0) sb.Append("|");
sb.Append(towerData.AttackSpeed[i]);
}
sb.Append('\n');
sb.Append($"伤害属性:{ConvertAttackProperty(towerData.AttackPropertyType)}\n");
return sb.ToString();
}
public static string BuildMuzzleDesc(MuzzleCompItemData muzzleData)
{
StringBuilder sb = new StringBuilder();
sb.Append("攻击伤害:");
for (int i = 0; i < muzzleData.AttackDamage.Length; i++)
{
if (i != 0) sb.Append("|");
sb.Append(muzzleData.AttackDamage[i]);
}
sb.Append('\n');
sb.Append($"伤害浮动:{muzzleData.DamageRandomRate:P0}\n");
sb.Append($"攻击方式:{ConvertAttackMethod(muzzleData.AttackMethodType)}\n");
return sb.ToString();
}
public static string BuildBearingDesc(BearingCompItemData bearingData)
{
StringBuilder sb = new StringBuilder();
sb.Append("旋转速度:");
for (int i = 0; i < bearingData.RotateSpeed.Length; i++)
{
if (i != 0) sb.Append("|");
sb.Append(bearingData.RotateSpeed[i]);
}
sb.Append('\n');
sb.Append("攻击范围:");
for (int i = 0; i < bearingData.AttackRange.Length; i++)
{
if (i != 0) sb.Append("|");
sb.Append(bearingData.AttackRange[i]);
}
sb.Append('\n');
return sb.ToString();
}
public static string BuildBaseDesc(BaseCompItemData baseData)
{
StringBuilder sb = new StringBuilder();
sb.Append("攻击速度:");
for (int i = 0; i < baseData.AttackSpeed.Length; i++)
{
if (i != 0) sb.Append("|");
sb.Append(baseData.AttackSpeed[i]);
}
sb.Append('\n');
sb.Append($"伤害属性:{ConvertAttackProperty(baseData.AttackPropertyType)}\n");
return sb.ToString();
}
public static string ConvertAttackMethod(AttackMethodType type)
{
return type switch
{
AttackMethodType.None => "无效",
AttackMethodType.NormalBullet => "发射子弹",
AttackMethodType.Range => "范围攻击",
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
public static string ConvertAttackProperty(AttackPropertyType type)
{
return type switch
{
AttackPropertyType.None => "无效",
AttackPropertyType.Physics => "<color=#ffff00ff>物理</color>", //yellow
AttackPropertyType.Fire => "<color=#ff0000ff>火</color>", //red
AttackPropertyType.Water => "<color=#00ffffff>水</color>", //cyan
AttackPropertyType.Earth => "<color=#00ff00ff>自然</color>", //lime
AttackPropertyType.Poison => "<color=#008000ff>毒</color>", //green
AttackPropertyType.Ice => "<color=#0000a0ff>冰</color>", //darkblue
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 855b508f90404cf38b99179d596426df
timeCreated: 1772614668

View File

@ -65,6 +65,9 @@ MonoBehaviour:
_screenEdgePadding: 0
_sideGap: 16
_anchorItemWidth: 0
_blankAreaGraphic: {fileID: 0}
_disableBlankAreaRaycast: 1
_closeOnPointerDownOutsideContent: 1
--- !u!1 &518771420464217804
GameObject:
m_ObjectHideFlags: 0
@ -595,7 +598,7 @@ MonoBehaviour:
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacing: 15
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0

View File

@ -49,10 +49,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: f3b1aede0e52479995cf71ba52a69d63, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Content: {fileID: 1805918912036091081}
m_ItemTemplate: {fileID: 8394974685918372820, guid: 2ead8e403e355dd4b9296a9d0a871a83,
_content: {fileID: 1805918912036091081}
_itemTemplate: {fileID: 8394974685918372820, guid: 2ead8e403e355dd4b9296a9d0a871a83,
type: 3}
m_InstancePoolCapacity: 64
_instancePoolCapacity: 64
--- !u!1 &2380285751925872027
GameObject:
m_ObjectHideFlags: 0
@ -1736,6 +1736,41 @@ PrefabInstance:
propertyPath: m_hasFontAssetChanged
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4067353614215461310, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: _onClick.m_PersistentCalls.m_Calls.Array.data[1].m_Mode
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4067353614215461310, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: _onClick.m_PersistentCalls.m_Calls.Array.data[0].m_Target
value:
objectReference: {fileID: 207314004719697847}
- target: {fileID: 4067353614215461310, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: _onClick.m_PersistentCalls.m_Calls.Array.data[1].m_Target
value:
objectReference: {fileID: 2687805356437946940}
- target: {fileID: 4067353614215461310, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: _onHover.m_PersistentCalls.m_Calls.Array.data[0].m_Target
value:
objectReference: {fileID: 207314004719697847}
- target: {fileID: 4067353614215461310, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: _onClick.m_PersistentCalls.m_Calls.Array.data[1].m_MethodName
value: OnCombineClick
objectReference: {fileID: 0}
- target: {fileID: 4067353614215461310, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: _onClick.m_PersistentCalls.m_Calls.Array.data[1].m_TargetAssemblyTypeName
value: GeometryTD.UI.CombineArea, Assembly-CSharp
objectReference: {fileID: 0}
- target: {fileID: 4067353614215461310, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: _onClick.m_PersistentCalls.m_Calls.Array.data[1].m_Arguments.m_ObjectArgumentAssemblyTypeName
value: UnityEngine.Object, UnityEngine
objectReference: {fileID: 0}
- target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc,
type: 3}
propertyPath: m_Pivot.x

View File

@ -20,8 +20,8 @@
| [ ] | P0-05 | 实现 10 节点地图生成(最后节点固定 Boss | `Assets/GameMain/Scripts/Scene/` | 每局都生成 10 节点且第 10 节点为 Boss |
| [x] | P0-06 | 实现节点选择与跳转流程(战斗/事件/商店) | `Assets/GameMain/Scripts/Procedure/` | 节点完成后可返回地图并进入下个节点 |
| [x] | P0-07 | 战斗节点基础玩法:放置塔、出怪、基地扣血、胜负判定 | `Assets/GameMain/Scripts/Entity/`<br>`Assets/GameMain/Scripts/Scene/` | 可完整打一场并得到胜利/失败结果 |
| [ ] | P0-08 | 胜利波次与结算规则100/80/50/<50 | `Assets/GameMain/Scripts/Procedure/` | 结算奖励与惩罚严格匹配设计文档 |
| [ ] | P0-09 | 敌人掉落与关卡奖励(组件/配件/金币) | `Assets/GameMain/Scripts/Entity/` | 战斗结束能发放掉落并写入库存 |
| [x] | P0-08 | 胜利波次与结算规则100/80/50/<50 | `Assets/GameMain/Scripts/Procedure/` | 结算奖励与惩罚严格匹配设计文档 |
| [x] | P0-09 | 敌人掉落与关卡奖励(组件/配件/金币) | `Assets/GameMain/Scripts/Entity/` | 战斗结束能发放掉落并写入库存 |
| [ ] | P0-10 | 节点后组装:枪口/轴承/底座三组件约束 | `Assets/GameMain/Scripts/Entity/`<br>`Assets/GameMain/Scripts/UI/Templates/GameScene/` | 未满足三组件时禁止出战 |
| [ ] | P0-11 | 品质/槽位/Tag 计算落地(白绿蓝紫红) | `Assets/GameMain/Scripts/Definition/`<br>`Assets/GameMain/Scripts/Entity/` | 计算结果可复现并与规则一致 |
| [ ] | P0-12 | 组件/配件耐久生效与 0 耐久销毁 | `Assets/GameMain/Scripts/Entity/` | 耐久影响属性,归零后物品移除 |