仓库组件 + 防御塔升级
- `PlayerInventoryComponent`:玩家库存组件,收集金币/组件/防御塔等道具 - 补全防御塔的升级逻辑,最高 5 级
This commit is contained in:
parent
5ba94828a8
commit
daba9cbdf9
|
|
@ -5,14 +5,12 @@
|
|||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 游戏入口。
|
||||
/// </summary>
|
||||
public partial class GameEntry : MonoBehaviour
|
||||
public partial class GameEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取游戏基础组件。
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ public partial class GameEntry
|
|||
{
|
||||
public static BuiltinDataComponent BuiltinData { get; private set; }
|
||||
|
||||
public static PlayerInventoryComponent PlayerInventory { get; private set; }
|
||||
|
||||
public static HPBarComponent HPBar { get; private set; }
|
||||
|
||||
public static UIRouterComponent UIRouter { get; private set; }
|
||||
|
|
@ -23,16 +25,12 @@ public partial class GameEntry
|
|||
private static void InitCustomComponents()
|
||||
{
|
||||
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
|
||||
PlayerInventory = UnityGameFramework.Runtime.GameEntry.GetComponent<PlayerInventoryComponent>();
|
||||
HPBar = UnityGameFramework.Runtime.GameEntry.GetComponent<HPBarComponent>();
|
||||
UIRouter = UnityGameFramework.Runtime.GameEntry.GetComponent<UIRouterComponent>();
|
||||
EventNode = UnityGameFramework.Runtime.GameEntry.GetComponent<EventNodeComponent>();
|
||||
CombatNode = UnityGameFramework.Runtime.GameEntry.GetComponent<CombatNodeComponent>();
|
||||
ShopNode = UnityGameFramework.Runtime.GameEntry.GetComponent<ShopNodeComponent>();
|
||||
ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent<ResolutionAdapterComponent>();
|
||||
if (ResolutionAdapter == null)
|
||||
{
|
||||
UnityGameFramework.Runtime.Log.Warning(
|
||||
"ResolutionAdapterComponent is missing. Please add it in Launcher scene and inject UI roots.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ namespace Components
|
|||
public class DefenseTowerController : MonoBehaviour
|
||||
{
|
||||
private const string AttackRangeIndicatorObjectName = "AttackRangeIndicator";
|
||||
private const int MinTowerLevel = 0;
|
||||
private const int MaxTowerLevel = 4;
|
||||
private static Material s_AttackRangeSharedMaterial;
|
||||
|
||||
[SerializeField] private ShooterMuzzleComp _muzzleComp;
|
||||
|
|
@ -15,7 +17,6 @@ namespace Components
|
|||
[SerializeField] private BasicBaseComp _baseComp;
|
||||
[SerializeField] private Transform _scanOrigin;
|
||||
[SerializeField] [Min(0.02f)] private float _retargetInterval = 0.1f;
|
||||
[SerializeField] private bool _autoUpdate = true;
|
||||
[SerializeField] private LineRenderer _attackRangeRenderer;
|
||||
[SerializeField] [Min(12)] private int _attackRangeSegments = 64;
|
||||
[SerializeField] [Min(0.005f)] private float _attackRangeLineWidth = 0.08f;
|
||||
|
|
@ -25,6 +26,7 @@ namespace Components
|
|||
private Transform _currentTarget;
|
||||
private float _retargetTimer;
|
||||
private float _attackRange;
|
||||
private int _towerLevel;
|
||||
|
||||
public Transform CurrentTarget => _currentTarget;
|
||||
|
||||
|
|
@ -33,17 +35,12 @@ namespace Components
|
|||
ResolveComponents();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
public void OnInit(DefenseTowerStatsData stats)
|
||||
{
|
||||
if (!_autoUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnUpdate(Time.deltaTime);
|
||||
OnInit(stats, MinTowerLevel);
|
||||
}
|
||||
|
||||
public void OnInit(DefenseTowerStatsData stats)
|
||||
public void OnInit(DefenseTowerStatsData stats, int towerLevel)
|
||||
{
|
||||
ResolveComponents();
|
||||
if (stats == null)
|
||||
|
|
@ -51,10 +48,16 @@ namespace Components
|
|||
return;
|
||||
}
|
||||
|
||||
_muzzleComp?.OnInit(stats.AttackDamage, stats.AttackMethodType);
|
||||
_bearingComp?.OnInit(stats.RotateSpeed, stats.AttackRange);
|
||||
_baseComp?.OnInit(stats.AttackSpeed, stats.AttackPropertyType);
|
||||
SetAttackRange(stats.AttackRange);
|
||||
_towerLevel = Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel);
|
||||
int attackDamage = ResolveIntValue(stats.AttackDamage, _towerLevel, 1, 1);
|
||||
float rotateSpeed = ResolveFloatValue(stats.RotateSpeed, _towerLevel, 180f, 1f);
|
||||
float attackRange = ResolveFloatValue(stats.AttackRange, _towerLevel, 5f, 0.1f);
|
||||
float attackSpeed = ResolveFloatValue(stats.AttackSpeed, _towerLevel, 1f, 0.01f);
|
||||
|
||||
_muzzleComp?.OnInit(attackDamage, stats.AttackMethodType);
|
||||
_bearingComp?.OnInit(rotateSpeed, attackRange);
|
||||
_baseComp?.OnInit(attackSpeed, stats.AttackPropertyType);
|
||||
SetAttackRange(attackRange);
|
||||
SetAttackRangeVisible(false);
|
||||
_currentTarget = null;
|
||||
_retargetTimer = 0f;
|
||||
|
|
@ -65,6 +68,7 @@ namespace Components
|
|||
SetAttackRangeVisible(false);
|
||||
_currentTarget = null;
|
||||
_retargetTimer = 0f;
|
||||
_towerLevel = MinTowerLevel;
|
||||
_muzzleComp?.OnReset();
|
||||
_bearingComp?.OnReset();
|
||||
_baseComp?.OnReset();
|
||||
|
|
@ -86,11 +90,6 @@ namespace Components
|
|||
RebuildAttackRangeGeometry();
|
||||
}
|
||||
|
||||
public void SetAutoUpdate(bool autoUpdate)
|
||||
{
|
||||
_autoUpdate = autoUpdate;
|
||||
}
|
||||
|
||||
public void SetTarget(Transform target)
|
||||
{
|
||||
_currentTarget = target;
|
||||
|
|
@ -195,6 +194,30 @@ namespace Components
|
|||
return target != null && target.gameObject.activeInHierarchy;
|
||||
}
|
||||
|
||||
private static int ResolveIntValue(int[] values, int level, int fallback, int minValue)
|
||||
{
|
||||
int resolved = Mathf.Max(minValue, fallback);
|
||||
if (values == null || values.Length <= 0)
|
||||
{
|
||||
return resolved;
|
||||
}
|
||||
|
||||
int index = Mathf.Clamp(level, 0, values.Length - 1);
|
||||
return Mathf.Max(minValue, values[index]);
|
||||
}
|
||||
|
||||
private static float ResolveFloatValue(float[] values, int level, float fallback, float minValue)
|
||||
{
|
||||
float resolved = Mathf.Max(minValue, fallback);
|
||||
if (values == null || values.Length <= 0)
|
||||
{
|
||||
return resolved;
|
||||
}
|
||||
|
||||
int index = Mathf.Clamp(level, 0, values.Length - 1);
|
||||
return Mathf.Max(minValue, values[index]);
|
||||
}
|
||||
|
||||
private void EnsureAttackRangeRenderer()
|
||||
{
|
||||
if (_attackRangeRenderer == null)
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ namespace GeometryTD.CustomComponent
|
|||
{
|
||||
int defeatedEnemyCount = _enemyManager.DefeatedEnemyCount;
|
||||
BackpackInventoryData rewardInventory = _combatResourceManager.GetRewardInventorySnapshot();
|
||||
GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
|
||||
|
||||
// Step 1: stop runtime and clear enemy entities only.
|
||||
_enemyManager.EndPhase();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,408 @@
|
|||
using System;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class PlayerInventoryComponent : GameFrameworkComponent
|
||||
{
|
||||
private BackpackInventoryData _inventory = new BackpackInventoryData();
|
||||
private long _nextInstanceId = 1;
|
||||
private bool _initialized;
|
||||
|
||||
public int Gold
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _inventory.Gold;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnInit()
|
||||
{
|
||||
_inventory = CloneInventory(RepoFormUseCase.SampleInventory());
|
||||
RebuildNextInstanceId();
|
||||
_initialized = true;
|
||||
Log.Info(
|
||||
"PlayerInventory initialized. Gold={0}, Tower={1}, Muzzle={2}, Bearing={3}, Base={4}.",
|
||||
_inventory.Gold,
|
||||
_inventory.Towers.Count,
|
||||
_inventory.MuzzleComponents.Count,
|
||||
_inventory.BearingComponents.Count,
|
||||
_inventory.BaseComponents.Count);
|
||||
}
|
||||
|
||||
public BackpackInventoryData GetInventorySnapshot()
|
||||
{
|
||||
EnsureInitialized();
|
||||
return CloneInventory(_inventory);
|
||||
}
|
||||
|
||||
public void MergeInventory(BackpackInventoryData gainedInventory)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (gainedInventory == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int gainedGold = Mathf.Max(0, gainedInventory.Gold);
|
||||
int gainedMuzzleCount = 0;
|
||||
int gainedBearingCount = 0;
|
||||
int gainedBaseCount = 0;
|
||||
int gainedTowerCount = 0;
|
||||
|
||||
if (gainedGold > 0)
|
||||
{
|
||||
_inventory.Gold += gainedGold;
|
||||
}
|
||||
|
||||
if (gainedInventory.MuzzleComponents != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.MuzzleComponents.Count; i++)
|
||||
{
|
||||
MuzzleCompItemData source = gainedInventory.MuzzleComponents[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MuzzleCompItemData cloned = CloneMuzzleComp(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_inventory.MuzzleComponents.Add(cloned);
|
||||
gainedMuzzleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (gainedInventory.BearingComponents != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.BearingComponents.Count; i++)
|
||||
{
|
||||
BearingCompItemData source = gainedInventory.BearingComponents[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BearingCompItemData cloned = CloneBearingComp(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_inventory.BearingComponents.Add(cloned);
|
||||
gainedBearingCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (gainedInventory.BaseComponents != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.BaseComponents.Count; i++)
|
||||
{
|
||||
BaseCompItemData source = gainedInventory.BaseComponents[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BaseCompItemData cloned = CloneBaseComp(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_inventory.BaseComponents.Add(cloned);
|
||||
gainedBaseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (gainedInventory.Towers != null)
|
||||
{
|
||||
for (int i = 0; i < gainedInventory.Towers.Count; i++)
|
||||
{
|
||||
DefenseTowerItemData source = gainedInventory.Towers[i];
|
||||
if (source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DefenseTowerItemData cloned = CloneTower(source);
|
||||
cloned.InstanceId = AllocateInstanceId();
|
||||
_inventory.Towers.Add(cloned);
|
||||
gainedTowerCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (gainedGold > 0 || gainedMuzzleCount > 0 || gainedBearingCount > 0 || gainedBaseCount > 0 ||
|
||||
gainedTowerCount > 0)
|
||||
{
|
||||
Log.Info(
|
||||
"PlayerInventory merged reward. Gold+{0}, Tower+{1}, Muzzle+{2}, Bearing+{3}, Base+{4}.",
|
||||
gainedGold,
|
||||
gainedTowerCount,
|
||||
gainedMuzzleCount,
|
||||
gainedBearingCount,
|
||||
gainedBaseCount);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryConsumeGold(int costGold)
|
||||
{
|
||||
EnsureInitialized();
|
||||
int resolvedCost = Mathf.Max(0, costGold);
|
||||
if (resolvedCost <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_inventory.Gold < resolvedCost)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_inventory.Gold -= resolvedCost;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddGold(int gainGold)
|
||||
{
|
||||
EnsureInitialized();
|
||||
int resolvedGain = Mathf.Max(0, gainGold);
|
||||
if (resolvedGain <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_inventory.Gold += resolvedGain;
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnInit();
|
||||
}
|
||||
|
||||
private long AllocateInstanceId()
|
||||
{
|
||||
if (_nextInstanceId < 1)
|
||||
{
|
||||
_nextInstanceId = 1;
|
||||
}
|
||||
|
||||
return _nextInstanceId++;
|
||||
}
|
||||
|
||||
private void RebuildNextInstanceId()
|
||||
{
|
||||
long maxInstanceId = 0;
|
||||
if (_inventory.Towers != null)
|
||||
{
|
||||
for (int i = 0; i < _inventory.Towers.Count; i++)
|
||||
{
|
||||
DefenseTowerItemData item = _inventory.Towers[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_inventory.MuzzleComponents != null)
|
||||
{
|
||||
for (int i = 0; i < _inventory.MuzzleComponents.Count; i++)
|
||||
{
|
||||
MuzzleCompItemData item = _inventory.MuzzleComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_inventory.BearingComponents != null)
|
||||
{
|
||||
for (int i = 0; i < _inventory.BearingComponents.Count; i++)
|
||||
{
|
||||
BearingCompItemData item = _inventory.BearingComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_inventory.BaseComponents != null)
|
||||
{
|
||||
for (int i = 0; i < _inventory.BaseComponents.Count; i++)
|
||||
{
|
||||
BaseCompItemData item = _inventory.BaseComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_nextInstanceId = Math.Max(1, maxInstanceId + 1);
|
||||
}
|
||||
|
||||
private static BackpackInventoryData CloneInventory(BackpackInventoryData source)
|
||||
{
|
||||
BackpackInventoryData cloned = new BackpackInventoryData();
|
||||
if (source == null)
|
||||
{
|
||||
return cloned;
|
||||
}
|
||||
|
||||
cloned.Gold = Mathf.Max(0, source.Gold);
|
||||
|
||||
if (source.MuzzleComponents != null)
|
||||
{
|
||||
for (int i = 0; i < source.MuzzleComponents.Count; i++)
|
||||
{
|
||||
MuzzleCompItemData item = source.MuzzleComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
cloned.MuzzleComponents.Add(CloneMuzzleComp(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source.BearingComponents != null)
|
||||
{
|
||||
for (int i = 0; i < source.BearingComponents.Count; i++)
|
||||
{
|
||||
BearingCompItemData item = source.BearingComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
cloned.BearingComponents.Add(CloneBearingComp(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source.BaseComponents != null)
|
||||
{
|
||||
for (int i = 0; i < source.BaseComponents.Count; i++)
|
||||
{
|
||||
BaseCompItemData item = source.BaseComponents[i];
|
||||
if (item != null)
|
||||
{
|
||||
cloned.BaseComponents.Add(CloneBaseComp(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source.Towers != null)
|
||||
{
|
||||
for (int i = 0; i < source.Towers.Count; i++)
|
||||
{
|
||||
DefenseTowerItemData item = source.Towers[i];
|
||||
if (item != null)
|
||||
{
|
||||
cloned.Towers.Add(CloneTower(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
private static MuzzleCompItemData CloneMuzzleComp(MuzzleCompItemData source)
|
||||
{
|
||||
return new MuzzleCompItemData
|
||||
{
|
||||
InstanceId = source.InstanceId,
|
||||
ConfigId = source.ConfigId,
|
||||
Name = source.Name,
|
||||
Rarity = source.Rarity,
|
||||
Endurance = source.Endurance,
|
||||
Constraint = source.Constraint,
|
||||
Tags = CloneTags(source.Tags),
|
||||
AttackDamage = CloneIntArray(source.AttackDamage),
|
||||
DamageRandomRate = source.DamageRandomRate,
|
||||
AttackMethodType = source.AttackMethodType
|
||||
};
|
||||
}
|
||||
|
||||
private static BearingCompItemData CloneBearingComp(BearingCompItemData source)
|
||||
{
|
||||
return new BearingCompItemData
|
||||
{
|
||||
InstanceId = source.InstanceId,
|
||||
ConfigId = source.ConfigId,
|
||||
Name = source.Name,
|
||||
Rarity = source.Rarity,
|
||||
Endurance = source.Endurance,
|
||||
Constraint = source.Constraint,
|
||||
Tags = CloneTags(source.Tags),
|
||||
RotateSpeed = CloneFloatArray(source.RotateSpeed),
|
||||
AttackRange = CloneFloatArray(source.AttackRange)
|
||||
};
|
||||
}
|
||||
|
||||
private static BaseCompItemData CloneBaseComp(BaseCompItemData source)
|
||||
{
|
||||
return new BaseCompItemData
|
||||
{
|
||||
InstanceId = source.InstanceId,
|
||||
ConfigId = source.ConfigId,
|
||||
Name = source.Name,
|
||||
Rarity = source.Rarity,
|
||||
Endurance = source.Endurance,
|
||||
Constraint = source.Constraint,
|
||||
Tags = CloneTags(source.Tags),
|
||||
AttackSpeed = CloneFloatArray(source.AttackSpeed),
|
||||
AttackPropertyType = source.AttackPropertyType
|
||||
};
|
||||
}
|
||||
|
||||
private static DefenseTowerItemData CloneTower(DefenseTowerItemData source)
|
||||
{
|
||||
return new DefenseTowerItemData
|
||||
{
|
||||
InstanceId = source.InstanceId,
|
||||
Name = source.Name,
|
||||
Rarity = source.Rarity,
|
||||
Endurance = source.Endurance,
|
||||
MuzzleComponentInstanceId = source.MuzzleComponentInstanceId,
|
||||
BearingComponentInstanceId = source.BearingComponentInstanceId,
|
||||
BaseComponentInstanceId = source.BaseComponentInstanceId,
|
||||
Stats = CloneTowerStats(source.Stats)
|
||||
};
|
||||
}
|
||||
|
||||
private static DefenseTowerStatsData CloneTowerStats(DefenseTowerStatsData source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return new DefenseTowerStatsData();
|
||||
}
|
||||
|
||||
return new DefenseTowerStatsData
|
||||
{
|
||||
AttackDamage = source.AttackDamage,
|
||||
DamageRandomRate = source.DamageRandomRate,
|
||||
RotateSpeed = source.RotateSpeed,
|
||||
AttackRange = source.AttackRange,
|
||||
AttackSpeed = source.AttackSpeed,
|
||||
AttackMethodType = source.AttackMethodType,
|
||||
AttackPropertyType = source.AttackPropertyType,
|
||||
Tags = CloneTags(source.Tags)
|
||||
};
|
||||
}
|
||||
|
||||
private static int[] CloneIntArray(int[] source)
|
||||
{
|
||||
return source != null ? (int[])source.Clone() : Array.Empty<int>();
|
||||
}
|
||||
|
||||
private static float[] CloneFloatArray(float[] source)
|
||||
{
|
||||
return source != null ? (float[])source.Clone() : Array.Empty<float>();
|
||||
}
|
||||
|
||||
private static TagType[] CloneTags(TagType[] source)
|
||||
{
|
||||
return source != null ? (TagType[])source.Clone() : Array.Empty<TagType>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5db8d7bb243947fdbb9fe75a9d234d10
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -9,11 +9,11 @@ namespace GeometryTD.Definition
|
|||
[Serializable]
|
||||
public sealed class DefenseTowerStatsData
|
||||
{
|
||||
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 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 TagType[] Tags { get; set; }
|
||||
|
|
|
|||
|
|
@ -8,13 +8,16 @@ namespace GeometryTD.Entity.EntityData
|
|||
public class DefenseTowerData : EntityDataBase
|
||||
{
|
||||
[SerializeField] private DefenseTowerStatsData _stats = new DefenseTowerStatsData();
|
||||
[SerializeField] private int _towerLevel = 0;
|
||||
|
||||
public DefenseTowerData(int entityId, int typeId, Vector3 position, Quaternion rotation, DefenseTowerStatsData stats)
|
||||
public DefenseTowerData(int entityId, int typeId, Vector3 position, Quaternion rotation,
|
||||
DefenseTowerStatsData stats, int towerLevel = 0)
|
||||
: base(entityId, typeId)
|
||||
{
|
||||
Position = position;
|
||||
Rotation = rotation;
|
||||
_stats = stats ?? new DefenseTowerStatsData();
|
||||
_towerLevel = Mathf.Max(0, towerLevel);
|
||||
}
|
||||
|
||||
public DefenseTowerStatsData Stats
|
||||
|
|
@ -22,5 +25,11 @@ namespace GeometryTD.Entity.EntityData
|
|||
get => _stats;
|
||||
set => _stats = value ?? new DefenseTowerStatsData();
|
||||
}
|
||||
|
||||
public int TowerLevel
|
||||
{
|
||||
get => _towerLevel;
|
||||
set => _towerLevel = Mathf.Max(0, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,15 @@ namespace GeometryTD.Entity
|
|||
{
|
||||
public sealed class CombatSelectInputService
|
||||
{
|
||||
public bool TryBuildUserData(Tilemap tilemap, Transform mapTransform,
|
||||
IReadOnlyDictionary<Vector3Int, int> towerEntityIdByFoundationCell, Func<Vector3Int, bool> isFoundationCell,
|
||||
int upgradeCost, int destroyGain, out CombatSelectFormUserData userData)
|
||||
public bool TryBuildUserData(
|
||||
Tilemap tilemap,
|
||||
Transform mapTransform,
|
||||
IReadOnlyDictionary<Vector3Int, int> towerEntityIdByFoundationCell,
|
||||
Func<Vector3Int, bool> isFoundationCell,
|
||||
Func<int, bool> isTowerAtMaxLevel,
|
||||
int upgradeCost,
|
||||
int destroyGain,
|
||||
out CombatSelectFormUserData userData)
|
||||
{
|
||||
userData = null;
|
||||
if (tilemap == null || !TryGetPointerWorldPosition(tilemap, mapTransform, out Vector3 worldPosition,
|
||||
|
|
@ -44,13 +50,15 @@ namespace GeometryTD.Entity
|
|||
WorldPosition = worldPosition,
|
||||
CellPosition = clickedCell,
|
||||
TowerEntityId = towerEntityId,
|
||||
IsTowerAtMaxLevel = towerEntityId != 0 && isTowerAtMaxLevel != null && isTowerAtMaxLevel(towerEntityId),
|
||||
UpgradeCost = Mathf.Max(0, upgradeCost),
|
||||
DestroyGain = Mathf.Max(0, destroyGain)
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetPointerWorldPosition(Tilemap tilemap, Transform mapTransform, out Vector3 worldPosition,
|
||||
private static bool TryGetPointerWorldPosition(Tilemap tilemap, Transform mapTransform,
|
||||
out Vector3 worldPosition,
|
||||
out Vector2 contentPosition)
|
||||
{
|
||||
worldPosition = Vector3.zero;
|
||||
|
|
@ -63,7 +71,9 @@ namespace GeometryTD.Entity
|
|||
}
|
||||
|
||||
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
|
||||
float mapPlaneZ = tilemap != null ? tilemap.transform.position.z : (mapTransform != null ? mapTransform.position.z : 0f);
|
||||
float mapPlaneZ = tilemap != null
|
||||
? tilemap.transform.position.z
|
||||
: (mapTransform != null ? mapTransform.position.z : 0f);
|
||||
Vector3 planeNormal = mainCamera.transform.forward.sqrMagnitude > Mathf.Epsilon
|
||||
? -mainCamera.transform.forward
|
||||
: Vector3.forward;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Components;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
|
|
@ -13,6 +14,17 @@ namespace GeometryTD.Entity
|
|||
_towerController?.SetAttackRangeVisible(visible);
|
||||
}
|
||||
|
||||
public bool TryApplyStats(DefenseTowerStatsData stats, int towerLevel)
|
||||
{
|
||||
if (_towerController == null || stats == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_towerController.OnInit(stats, towerLevel);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnInit(object userData)
|
||||
{
|
||||
base.OnInit(userData);
|
||||
|
|
@ -41,8 +53,7 @@ namespace GeometryTD.Entity
|
|||
return;
|
||||
}
|
||||
|
||||
_towerController.SetAutoUpdate(false);
|
||||
_towerController.OnInit(towerData.Stats);
|
||||
_towerController.OnInit(towerData.Stats, towerData.TowerLevel);
|
||||
}
|
||||
|
||||
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
|
|
|
|||
|
|
@ -242,7 +242,8 @@ namespace GeometryTD.Entity
|
|||
if (_combatSelectInputService == null ||
|
||||
!_combatSelectInputService.TryBuildUserData(Tilemap, CachedTransform,
|
||||
_towerPlacementService != null ? _towerPlacementService.TowerEntityIdByFoundationCell : null,
|
||||
IsFoundationCell, _upgradeCost, _destroyGain, out CombatSelectFormUserData userData))
|
||||
IsFoundationCell, IsTowerAtMaxLevel, _upgradeCost, _destroyGain,
|
||||
out CombatSelectFormUserData userData))
|
||||
{
|
||||
userData = new CombatSelectFormUserData
|
||||
{
|
||||
|
|
@ -254,6 +255,11 @@ namespace GeometryTD.Entity
|
|||
GameEntry.UIRouter.OpenUI(UIFormType.CombatSelectForm, userData);
|
||||
}
|
||||
|
||||
private bool IsTowerAtMaxLevel(int towerEntityId)
|
||||
{
|
||||
return _towerPlacementService != null && _towerPlacementService.IsTowerAtMaxLevel(towerEntityId);
|
||||
}
|
||||
|
||||
private void ApplySelectedObject(CombatSelectFormUserData userData)
|
||||
{
|
||||
_towerSelectionPresenter?.ApplySelectedObject(userData);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ namespace GeometryTD.Procedure
|
|||
GameEntry.EventNode.OnInit();
|
||||
GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
|
||||
GameEntry.ShopNode.OnInit();
|
||||
GameEntry.PlayerInventory?.OnInit();
|
||||
|
||||
_repoFormUseCase = new RepoFormUseCase();
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,44 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using GeometryTD.Pathfinding;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Tilemaps;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.Map
|
||||
{
|
||||
public sealed class TowerPlacementService
|
||||
{
|
||||
private const int DefaultTowerTypeId = 401;
|
||||
private const int MinTowerLevel = 0;
|
||||
private const int MaxTowerLevel = 4;
|
||||
|
||||
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, int> _towerLevelByEntityId = new();
|
||||
private readonly List<int> _towerEntityIdBuffer = new();
|
||||
|
||||
public IReadOnlyDictionary<Vector3Int, int> TowerEntityIdByFoundationCell => _towerEntityIdByFoundationCell;
|
||||
public IReadOnlyDictionary<int, Vector3Int> FoundationCellByTowerEntityId => _foundationCellByTowerEntityId;
|
||||
|
||||
public bool IsTowerAtMaxLevel(int towerEntityId)
|
||||
{
|
||||
if (towerEntityId == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DefenseTowerStatsData towerStats =
|
||||
_towerStatsByEntityId.TryGetValue(towerEntityId, out DefenseTowerStatsData cachedStats)
|
||||
? cachedStats
|
||||
: null;
|
||||
int currentLevel = GetTowerLevel(towerEntityId);
|
||||
int maxLevel = ResolveMaxTowerLevel(towerStats);
|
||||
return currentLevel >= maxLevel;
|
||||
}
|
||||
|
||||
public bool TryBuildTower(Vector3Int foundationCell, Func<Vector3Int, bool> isFoundationCell, int buildIndex,
|
||||
int[] buildTowerCosts, int towerTypeId, Tilemap tilemap, Func<int, bool> tryConsumeCoin,
|
||||
Action<int> addCoin,
|
||||
|
|
@ -53,6 +71,7 @@ namespace GeometryTD.Map
|
|||
_towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId;
|
||||
_foundationCellByTowerEntityId[newTowerEntityId] = foundationCell;
|
||||
_towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(towerStats);
|
||||
_towerLevelByEntityId[newTowerEntityId] = MinTowerLevel;
|
||||
towerEntityId = newTowerEntityId;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -68,42 +87,36 @@ namespace GeometryTD.Map
|
|||
return false;
|
||||
}
|
||||
|
||||
int requiredUpgradeCost = Mathf.Max(0, upgradeCost);
|
||||
if (tryConsumeCoin != null && !tryConsumeCoin(requiredUpgradeCost))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DefenseTowerStatsData oldStats =
|
||||
DefenseTowerStatsData towerStats =
|
||||
_towerStatsByEntityId.TryGetValue(towerEntityId, out DefenseTowerStatsData cachedStats)
|
||||
? CloneTowerStats(cachedStats)
|
||||
: BuildTowerStats(0);
|
||||
DefenseTowerStatsData upgradedStats = CloneTowerStats(oldStats);
|
||||
ApplyUpgradeToStats(upgradedStats);
|
||||
|
||||
HideTowerEntity(towerEntityId);
|
||||
_towerEntityIdByFoundationCell.Remove(foundationCell);
|
||||
_foundationCellByTowerEntityId.Remove(towerEntityId);
|
||||
_towerStatsByEntityId.Remove(towerEntityId);
|
||||
|
||||
if (!TryShowTowerEntity(foundationCell, upgradedStats, towerTypeId, tilemap, out int newTowerEntityId))
|
||||
int currentTowerLevel = GetTowerLevel(towerEntityId);
|
||||
int maxTowerLevel = ResolveMaxTowerLevel(towerStats);
|
||||
if (currentTowerLevel >= maxTowerLevel)
|
||||
{
|
||||
if (TryShowTowerEntity(foundationCell, oldStats, towerTypeId, tilemap, out int fallbackTowerEntityId))
|
||||
{
|
||||
_towerEntityIdByFoundationCell[foundationCell] = fallbackTowerEntityId;
|
||||
_foundationCellByTowerEntityId[fallbackTowerEntityId] = foundationCell;
|
||||
_towerStatsByEntityId[fallbackTowerEntityId] = CloneTowerStats(oldStats);
|
||||
resultTowerEntityId = fallbackTowerEntityId;
|
||||
}
|
||||
|
||||
addCoin?.Invoke(requiredUpgradeCost);
|
||||
resultTowerEntityId = towerEntityId;
|
||||
return false;
|
||||
}
|
||||
|
||||
_towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId;
|
||||
_foundationCellByTowerEntityId[newTowerEntityId] = foundationCell;
|
||||
_towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(upgradedStats);
|
||||
resultTowerEntityId = newTowerEntityId;
|
||||
int requiredUpgradeCost = Mathf.Max(0, upgradeCost);
|
||||
if (tryConsumeCoin != null && !tryConsumeCoin(requiredUpgradeCost))
|
||||
{
|
||||
resultTowerEntityId = towerEntityId;
|
||||
return false;
|
||||
}
|
||||
|
||||
int nextTowerLevel = Mathf.Clamp(currentTowerLevel + 1, MinTowerLevel, maxTowerLevel);
|
||||
if (!TryApplyTowerStats(towerEntityId, towerStats, nextTowerLevel))
|
||||
{
|
||||
addCoin?.Invoke(requiredUpgradeCost);
|
||||
resultTowerEntityId = towerEntityId;
|
||||
return false;
|
||||
}
|
||||
|
||||
_towerStatsByEntityId[towerEntityId] = CloneTowerStats(towerStats);
|
||||
_towerLevelByEntityId[towerEntityId] = nextTowerLevel;
|
||||
resultTowerEntityId = towerEntityId;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -120,6 +133,7 @@ namespace GeometryTD.Map
|
|||
_towerEntityIdByFoundationCell.Remove(foundationCell);
|
||||
_foundationCellByTowerEntityId.Remove(towerEntityId);
|
||||
_towerStatsByEntityId.Remove(towerEntityId);
|
||||
_towerLevelByEntityId.Remove(towerEntityId);
|
||||
addCoin?.Invoke(Mathf.Max(0, destroyGain));
|
||||
return true;
|
||||
}
|
||||
|
|
@ -140,6 +154,7 @@ namespace GeometryTD.Map
|
|||
_towerEntityIdByFoundationCell.Clear();
|
||||
_foundationCellByTowerEntityId.Clear();
|
||||
_towerStatsByEntityId.Clear();
|
||||
_towerLevelByEntityId.Clear();
|
||||
_towerEntityIdBuffer.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -148,6 +163,7 @@ namespace GeometryTD.Map
|
|||
_towerEntityIdByFoundationCell.Clear();
|
||||
_foundationCellByTowerEntityId.Clear();
|
||||
_towerStatsByEntityId.Clear();
|
||||
_towerLevelByEntityId.Clear();
|
||||
_towerEntityIdBuffer.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +191,8 @@ 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 DefenseTowerData(entityId, typeId, towerPosition, Quaternion.identity, towerStats,
|
||||
MinTowerLevel);
|
||||
GameEntry.Entity.ShowDefenseTower(towerData);
|
||||
|
||||
towerEntityId = entityId;
|
||||
|
|
@ -203,11 +220,11 @@ namespace GeometryTD.Map
|
|||
case 0:
|
||||
return new DefenseTowerStatsData
|
||||
{
|
||||
AttackDamage = 200,
|
||||
DamageRandomRate = 0f,
|
||||
RotateSpeed = 200f,
|
||||
AttackRange = 4.5f,
|
||||
AttackSpeed = 1.5f,
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 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 },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Physics,
|
||||
Tags = Array.Empty<TagType>()
|
||||
|
|
@ -215,11 +232,11 @@ namespace GeometryTD.Map
|
|||
case 1:
|
||||
return new DefenseTowerStatsData
|
||||
{
|
||||
AttackDamage = 260,
|
||||
DamageRandomRate = 0f,
|
||||
RotateSpeed = 160f,
|
||||
AttackRange = 5f,
|
||||
AttackSpeed = 1.2f,
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 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 },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Fire,
|
||||
Tags = Array.Empty<TagType>()
|
||||
|
|
@ -227,11 +244,11 @@ namespace GeometryTD.Map
|
|||
case 2:
|
||||
return new DefenseTowerStatsData
|
||||
{
|
||||
AttackDamage = 340,
|
||||
DamageRandomRate = 0f,
|
||||
RotateSpeed = 140f,
|
||||
AttackRange = 5.5f,
|
||||
AttackSpeed = 0.95f,
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 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 },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Ice,
|
||||
Tags = Array.Empty<TagType>()
|
||||
|
|
@ -239,11 +256,11 @@ namespace GeometryTD.Map
|
|||
case 3:
|
||||
return new DefenseTowerStatsData
|
||||
{
|
||||
AttackDamage = 440,
|
||||
DamageRandomRate = 0f,
|
||||
RotateSpeed = 120f,
|
||||
AttackRange = 6f,
|
||||
AttackSpeed = 0.75f,
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 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 },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Poison,
|
||||
Tags = Array.Empty<TagType>()
|
||||
|
|
@ -251,11 +268,11 @@ namespace GeometryTD.Map
|
|||
default:
|
||||
return new DefenseTowerStatsData
|
||||
{
|
||||
AttackDamage = 200,
|
||||
DamageRandomRate = 0f,
|
||||
RotateSpeed = 180f,
|
||||
AttackRange = 5f,
|
||||
AttackSpeed = 1f,
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 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 },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Physics,
|
||||
Tags = Array.Empty<TagType>()
|
||||
|
|
@ -273,28 +290,63 @@ namespace GeometryTD.Map
|
|||
TagType[] copiedTags = source.Tags != null ? (TagType[])source.Tags.Clone() : Array.Empty<TagType>();
|
||||
return new DefenseTowerStatsData
|
||||
{
|
||||
AttackDamage = source.AttackDamage,
|
||||
DamageRandomRate = source.DamageRandomRate,
|
||||
RotateSpeed = source.RotateSpeed,
|
||||
AttackRange = source.AttackRange,
|
||||
AttackSpeed = source.AttackSpeed,
|
||||
AttackDamage = source.AttackDamage != null ? (int[])source.AttackDamage.Clone() : Array.Empty<int>(),
|
||||
DamageRandomRate = source.DamageRandomRate != null
|
||||
? (float[])source.DamageRandomRate.Clone()
|
||||
: Array.Empty<float>(),
|
||||
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>(),
|
||||
AttackMethodType = source.AttackMethodType,
|
||||
AttackPropertyType = source.AttackPropertyType,
|
||||
Tags = copiedTags
|
||||
};
|
||||
}
|
||||
|
||||
private static void ApplyUpgradeToStats(DefenseTowerStatsData stats)
|
||||
private int GetTowerLevel(int towerEntityId)
|
||||
{
|
||||
if (stats == null)
|
||||
if (towerEntityId == 0 || !_towerLevelByEntityId.TryGetValue(towerEntityId, out int towerLevel))
|
||||
{
|
||||
return;
|
||||
return MinTowerLevel;
|
||||
}
|
||||
|
||||
stats.AttackDamage = Mathf.Max(1, stats.AttackDamage + 3);
|
||||
stats.AttackSpeed = Mathf.Max(0.01f, stats.AttackSpeed + 0.2f);
|
||||
stats.AttackRange = Mathf.Max(0.1f, stats.AttackRange + 0.4f);
|
||||
stats.RotateSpeed = Mathf.Max(1f, stats.RotateSpeed + 10f);
|
||||
return Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel);
|
||||
}
|
||||
|
||||
private static int ResolveMaxTowerLevel(DefenseTowerStatsData stats)
|
||||
{
|
||||
int maxCount = Mathf.Max(
|
||||
GetLength(stats?.AttackDamage),
|
||||
GetLength(stats?.DamageRandomRate),
|
||||
GetLength(stats?.RotateSpeed),
|
||||
GetLength(stats?.AttackRange),
|
||||
GetLength(stats?.AttackSpeed));
|
||||
if (maxCount <= 0)
|
||||
{
|
||||
return MinTowerLevel;
|
||||
}
|
||||
|
||||
return Mathf.Clamp(maxCount - 1, MinTowerLevel, MaxTowerLevel);
|
||||
}
|
||||
|
||||
private static bool TryApplyTowerStats(int towerEntityId, DefenseTowerStatsData towerStats, int towerLevel)
|
||||
{
|
||||
if (towerEntityId == 0 || towerStats == null || GameEntry.Entity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GameEntry.Entity.GetGameEntity(towerEntityId) is not DefenseTowerEntity towerEntity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return towerEntity.TryApplyStats(towerStats, towerLevel);
|
||||
}
|
||||
|
||||
private static int GetLength<T>(T[] values)
|
||||
{
|
||||
return values != null ? values.Length : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,10 +98,12 @@ namespace GeometryTD.UI
|
|||
switch (userData.ClickObjectType)
|
||||
{
|
||||
case CombatSelectClickObjectType.Foundation:
|
||||
_useCase.SetUpgradeVisible(true);
|
||||
_useCase.ShowForFoundation(userData.ContentPosition);
|
||||
break;
|
||||
case CombatSelectClickObjectType.Tower:
|
||||
_useCase.SetUpgradePrice(userData.UpgradeCost);
|
||||
_useCase.SetUpgradeVisible(!userData.IsTowerAtMaxLevel);
|
||||
_useCase.SetDestroyGain(userData.DestroyGain);
|
||||
_useCase.ShowForTower(userData.ContentPosition);
|
||||
break;
|
||||
|
|
@ -133,6 +135,7 @@ namespace GeometryTD.UI
|
|||
return null;
|
||||
}
|
||||
|
||||
_useCase.SetUpgradeVisible(true);
|
||||
_useCase.ShowForFoundation(contentPosition);
|
||||
return OpenUI(_useCase.TryRefresh());
|
||||
}
|
||||
|
|
@ -146,6 +149,7 @@ namespace GeometryTD.UI
|
|||
}
|
||||
|
||||
_useCase.SetUpgradePrice(upgradeCost);
|
||||
_useCase.SetUpgradeVisible(true);
|
||||
_useCase.SetDestroyGain(destroyGain);
|
||||
_useCase.ShowForTower(contentPosition);
|
||||
return OpenUI(_useCase.TryRefresh());
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ namespace GeometryTD.UI
|
|||
public Vector3 WorldPosition;
|
||||
public Vector3Int CellPosition;
|
||||
public int TowerEntityId;
|
||||
public bool IsTowerAtMaxLevel;
|
||||
public int UpgradeCost;
|
||||
public int DestroyGain;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ namespace GeometryTD.UI
|
|||
{
|
||||
public class CombatFinishFormUseCase : IUIUseCase
|
||||
{
|
||||
private readonly RepoFormUseCase _repoFormUseCase = new RepoFormUseCase();
|
||||
private CombatScheduler _combatScheduler;
|
||||
private int _defeatedEnemyCount;
|
||||
private int _gainedGold;
|
||||
|
|
@ -57,8 +56,7 @@ namespace GeometryTD.UI
|
|||
BackpackInventoryData rewardInventory = _rewardInventory;
|
||||
if (rewardInventory == null)
|
||||
{
|
||||
RepoFormRawData repoRawData = _repoFormUseCase.CreateInitialModel();
|
||||
rewardInventory = repoRawData != null ? repoRawData.Inventory : null;
|
||||
rewardInventory = RepoFormUseCase.SampleInventory();
|
||||
}
|
||||
|
||||
return new CombatFinishFormRawData
|
||||
|
|
|
|||
|
|
@ -134,6 +134,11 @@ namespace GeometryTD.UI
|
|||
_upgradeOption.Price = Mathf.Max(0, cost);
|
||||
}
|
||||
|
||||
public void SetUpgradeVisible(bool visible)
|
||||
{
|
||||
_upgradeOption.IsVisible = visible;
|
||||
}
|
||||
|
||||
public void SetDestroyGain(int gain)
|
||||
{
|
||||
_destroyOption.Price = Mathf.Max(0, gain);
|
||||
|
|
|
|||
|
|
@ -7,14 +7,16 @@ namespace GeometryTD.UI
|
|||
{
|
||||
public RepoFormRawData CreateInitialModel()
|
||||
{
|
||||
BackpackInventoryData sample = BuildSampleInventory();
|
||||
BackpackInventoryData sample = GameEntry.PlayerInventory != null
|
||||
? GameEntry.PlayerInventory.GetInventorySnapshot()
|
||||
: SampleInventory();
|
||||
return new RepoFormRawData
|
||||
{
|
||||
Inventory = sample
|
||||
};
|
||||
}
|
||||
|
||||
private static BackpackInventoryData BuildSampleInventory()
|
||||
public static BackpackInventoryData SampleInventory()
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData
|
||||
{
|
||||
|
|
@ -75,11 +77,11 @@ namespace GeometryTD.UI
|
|||
BaseComponentInstanceId = baseComp.InstanceId,
|
||||
Stats = new DefenseTowerStatsData
|
||||
{
|
||||
AttackDamage = 30,
|
||||
DamageRandomRate = 0.05f,
|
||||
RotateSpeed = 12f,
|
||||
AttackRange = 2f,
|
||||
AttackSpeed = 1.5f,
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 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 },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Fire,
|
||||
Tags = new[] { TagType.Fire, TagType.BurnSpread }
|
||||
|
|
@ -98,11 +100,11 @@ namespace GeometryTD.UI
|
|||
BaseComponentInstanceId = 0,
|
||||
Stats = new DefenseTowerStatsData
|
||||
{
|
||||
AttackDamage = 50,
|
||||
DamageRandomRate = 0.03f,
|
||||
RotateSpeed = 20f,
|
||||
AttackRange = 4f,
|
||||
AttackSpeed = 1f,
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 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 },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Physics,
|
||||
Tags = new[] { TagType.Pierce }
|
||||
|
|
|
|||
|
|
@ -153,11 +153,9 @@ Transform:
|
|||
m_Children:
|
||||
- {fileID: 513208573}
|
||||
- {fileID: 1604812193}
|
||||
- {fileID: 159392563}
|
||||
- {fileID: 1549230541}
|
||||
- {fileID: 1758164286}
|
||||
- {fileID: 2007255511}
|
||||
- {fileID: 428539048}
|
||||
- {fileID: 1322505022}
|
||||
m_Father: {fileID: 1852670053}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &120093239
|
||||
|
|
@ -275,12 +273,12 @@ Transform:
|
|||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 159392562}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 119167776}
|
||||
m_Father: {fileID: 1322505022}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &159392564
|
||||
MonoBehaviour:
|
||||
|
|
@ -883,6 +881,85 @@ LightingSettings:
|
|||
m_PVRTiledBaking: 0
|
||||
m_NumRaysToShootPerTexel: -1
|
||||
m_RespectSceneVisibilityWhenBakingGI: 0
|
||||
--- !u!1 &1239157580
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1239157581}
|
||||
- component: {fileID: 1239157582}
|
||||
m_Layer: 0
|
||||
m_Name: PlayerInventory
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1239157581
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1239157580}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1322505022}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1239157582
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1239157580}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5db8d7bb243947fdbb9fe75a9d234d10, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
--- !u!1 &1322505021
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1322505022}
|
||||
m_Layer: 0
|
||||
m_Name: TowerDefense
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1322505022
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1322505021}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1239157581}
|
||||
- {fileID: 159392563}
|
||||
- {fileID: 1549230541}
|
||||
- {fileID: 1758164286}
|
||||
m_Father: {fileID: 119167776}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1454214586
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -991,12 +1068,12 @@ Transform:
|
|||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1549230540}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 119167776}
|
||||
m_Father: {fileID: 1322505022}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1549230542
|
||||
MonoBehaviour:
|
||||
|
|
@ -1184,12 +1261,12 @@ Transform:
|
|||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1758164285}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 119167776}
|
||||
m_Father: {fileID: 1322505022}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1758164287
|
||||
MonoBehaviour:
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@
|
|||
| 状态 | ID | 任务 | 交付物路径 | 验收标准 |
|
||||
|-----|-------|-------------------------------------|----------------------------------------------------------------------------------------|---------------------------|
|
||||
| [x] | P0-01 | 冻结 MVP 范围(只保留:战斗节点/事件节点/商店节点/节点后组装) | `docs/MVP-Scope.md` | 明确“做/不做”清单,团队评审通过 |
|
||||
| [ ] | P0-02 | 补齐数据表:组件、配件、敌人、波次、节点、事件、商店商品 | `Assets/GameMain/DataTables/*.txt` | 游戏启动可无报错加载全部新增表 |
|
||||
| [ ] | P0-03 | 新增/完善 DataRow 解析类 | `Assets/GameMain/Scripts/DataTable/*.cs` | 每个新增表都有对应 DR 类并成功解析 |
|
||||
| [ ] | P0-04 | 建立单局 Run 状态模型(金币、生命、库存、当前节点) | `Assets/GameMain/Scripts/Procedure/` | 进入/退出节点时状态可持续传递 |
|
||||
| [x] | P0-02 | 补齐数据表:组件、配件、敌人、波次、节点、事件、商店商品 | `Assets/GameMain/DataTables/*.txt` | 游戏启动可无报错加载全部新增表 |
|
||||
| [x] | P0-03 | 新增/完善 DataRow 解析类 | `Assets/GameMain/Scripts/DataTable/*.cs` | 每个新增表都有对应 DR 类并成功解析 |
|
||||
| [x] | P0-04 | 建立单局 Run 状态模型(金币、生命、库存、当前节点) | `Assets/GameMain/Scripts/Procedure/` | 进入/退出节点时状态可持续传递 |
|
||||
| [ ] | P0-05 | 实现 10 节点地图生成(最后节点固定 Boss) | `Assets/GameMain/Scripts/Scene/` | 每局都生成 10 节点且第 10 节点为 Boss |
|
||||
| [ ] | P0-06 | 实现节点选择与跳转流程(战斗/事件/商店) | `Assets/GameMain/Scripts/Procedure/` | 节点完成后可返回地图并进入下个节点 |
|
||||
| [x] | P0-06 | 实现节点选择与跳转流程(战斗/事件/商店) | `Assets/GameMain/Scripts/Procedure/` | 节点完成后可返回地图并进入下个节点 |
|
||||
| [ ] | 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/` | 战斗结束能发放掉落并写入库存 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue