CombatNode 相关逻辑补充

- CombatFinishForm:战斗节点的结算页面(杀死的敌人、获得的金币、获得的道具)
- CombatSelectForm:战斗节点建造防御塔的选择 UI 以及对防御塔进行操作的 UI(只实现了建造逻辑,在 MapEntity 里做射线检测)
- 构建了基本的防御塔三组件(ShooterMuzzleComp/BasicBearingComp/BasicBaseComp)和防御塔主控(DefenseTowerController)以及相应的实体类(DefenseTowerEntity)
This commit is contained in:
SepComet
2026-03-01 23:11:52 +08:00
parent 92cca14503
commit 564817d752
96 changed files with 7294 additions and 121 deletions
@@ -0,0 +1,66 @@
using GeometryTD.Definition;
using UnityEngine;
namespace Components
{
[DisallowMultipleComponent]
public class BasicBaseComp : MonoBehaviour
{
[SerializeField] [Min(0.01f)] private float _attackSpeed = 1f;
[SerializeField] private AttackPropertyType _attackPropertyType = AttackPropertyType.Physics;
private float _cooldownRemaining;
public float AttackSpeed => _attackSpeed;
public AttackPropertyType AttackPropertyType => _attackPropertyType;
public bool CanAttack => _cooldownRemaining <= 0f;
public void OnInit(float attackSpeed, AttackPropertyType attackPropertyType)
{
_attackSpeed = Mathf.Max(0.01f, attackSpeed);
_attackPropertyType = attackPropertyType;
_cooldownRemaining = 0f;
}
public void OnReset()
{
_attackSpeed = 1f;
_attackPropertyType = AttackPropertyType.None;
_cooldownRemaining = 0f;
}
public void Tick(float deltaTime)
{
if (_cooldownRemaining <= 0f)
{
return;
}
_cooldownRemaining = Mathf.Max(0f, _cooldownRemaining - Mathf.Max(0f, deltaTime));
}
public bool TryAttack(BasicBearingComp bearingComp, ShooterMuzzleComp shooterMuzzleComp, Transform target, float deltaTime)
{
if (bearingComp == null || shooterMuzzleComp == null || target == null)
{
return false;
}
Tick(deltaTime);
bool isAligned = bearingComp.TrackTarget(target, deltaTime);
if (!isAligned || !CanAttack)
{
return false;
}
bool fired = shooterMuzzleComp.Attack(target, _attackPropertyType);
if (!fired)
{
return false;
}
_cooldownRemaining = 1f / _attackSpeed;
return true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 11e3dd172f9d486194b6ca5d7f4dd5e6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,111 @@
using UnityEngine;
namespace Components
{
[DisallowMultipleComponent]
public class BasicBearingComp : MonoBehaviour
{
[SerializeField] [Min(1f)] private float _rotateSpeed = 180f;
[SerializeField] [Min(0.1f)] private float _attackRange = 5f;
[SerializeField] [Min(0.1f)] private float _aimToleranceAngle = 2f;
[SerializeField] private Transform _rotateRoot;
[SerializeField] private bool _yawOnly = true;
public float RotateSpeed => _rotateSpeed;
public float AttackRange => _attackRange;
public float AimToleranceAngle => _aimToleranceAngle;
public void OnInit(float rotateSpeed, float attackRange = 5f)
{
_rotateSpeed = Mathf.Max(1f, rotateSpeed);
_attackRange = Mathf.Max(0.1f, attackRange);
}
public void OnReset()
{
_rotateSpeed = 1f;
_attackRange = 5f;
}
public bool IsTargetInRange(Transform target, Transform origin = null)
{
if (target == null)
{
return false;
}
Transform originTransform = origin != null ? origin : transform;
Vector3 delta = target.position - originTransform.position;
return delta.sqrMagnitude <= _attackRange * _attackRange;
}
public bool TrackTarget(Transform target, float deltaTime)
{
if (target == null)
{
return false;
}
Transform rotateRoot = _rotateRoot != null ? _rotateRoot : transform;
Vector3 toTarget = target.position - rotateRoot.position;
if (_yawOnly)
{
toTarget.y = 0f;
}
if (toTarget.sqrMagnitude <= Mathf.Epsilon)
{
return true;
}
Quaternion targetRotation = Quaternion.LookRotation(toTarget.normalized, Vector3.up);
float rotateAngle = _rotateSpeed * Mathf.Max(0f, deltaTime);
rotateRoot.rotation = Quaternion.RotateTowards(rotateRoot.rotation, targetRotation, rotateAngle);
return IsTargetAligned(target);
}
public bool IsTargetAligned(Transform target)
{
if (target == null)
{
return false;
}
Transform rotateRoot = _rotateRoot != null ? _rotateRoot : transform;
Vector3 toTarget = target.position - rotateRoot.position;
if (_yawOnly)
{
toTarget.y = 0f;
}
if (toTarget.sqrMagnitude <= Mathf.Epsilon)
{
return true;
}
float angle = Vector3.Angle(rotateRoot.forward, toTarget.normalized);
return angle <= _aimToleranceAngle;
}
public bool TryAttack(ShooterMuzzleComp shooterMuzzleComp, Transform target, float deltaTime)
{
if (shooterMuzzleComp == null || target == null)
{
return false;
}
if (!IsTargetInRange(target))
{
return false;
}
bool isAligned = TrackTarget(target, deltaTime);
if (!isAligned)
{
return false;
}
return shooterMuzzleComp.Attack(target);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c8b3d58a8bb41f493706b40fc451558
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,168 @@
using GeometryTD.Definition;
using GeometryTD.Entity;
using UnityEngine;
namespace Components
{
[DisallowMultipleComponent]
public class DefenseTowerController : MonoBehaviour
{
[SerializeField] private ShooterMuzzleComp _muzzleComp;
[SerializeField] private BasicBearingComp _bearingComp;
[SerializeField] private BasicBaseComp _baseComp;
[SerializeField] private Transform _scanOrigin;
[SerializeField] [Min(0.02f)] private float _retargetInterval = 0.1f;
[SerializeField] private bool _autoUpdate = true;
private Transform _currentTarget;
private float _retargetTimer;
public Transform CurrentTarget => _currentTarget;
private void Awake()
{
ResolveComponents();
}
private void Update()
{
if (!_autoUpdate)
{
return;
}
OnUpdate(Time.deltaTime);
}
public void OnInit(DefenseTowerStatsData stats)
{
ResolveComponents();
if (stats == null)
{
return;
}
_muzzleComp?.OnInit(stats.AttackDamage, stats.AttackMethodType);
_bearingComp?.OnInit(stats.RotateSpeed, stats.AttackRange);
_baseComp?.OnInit(stats.AttackSpeed, stats.AttackPropertyType);
_currentTarget = null;
_retargetTimer = 0f;
}
public void OnReset()
{
_currentTarget = null;
_retargetTimer = 0f;
_muzzleComp?.OnReset();
_bearingComp?.OnReset();
_baseComp?.OnReset();
}
public void SetAutoUpdate(bool autoUpdate)
{
_autoUpdate = autoUpdate;
}
public void SetTarget(Transform target)
{
_currentTarget = target;
}
public void ClearTarget()
{
_currentTarget = null;
}
public void OnUpdate(float deltaTime)
{
if (!HasCoreComponents())
{
return;
}
if (!IsTargetValid(_currentTarget) || !_bearingComp.IsTargetInRange(_currentTarget, _scanOrigin))
{
TryRetarget(deltaTime);
}
if (_currentTarget == null)
{
_baseComp.Tick(deltaTime);
return;
}
_baseComp.TryAttack(_bearingComp, _muzzleComp, _currentTarget, deltaTime);
}
private void TryRetarget(float deltaTime)
{
_retargetTimer -= Mathf.Max(0f, deltaTime);
if (_retargetTimer > 0f)
{
return;
}
_retargetTimer = _retargetInterval;
_currentTarget = FindNearestEnemyTarget();
}
private Transform FindNearestEnemyTarget()
{
Vector3 origin = _scanOrigin != null ? _scanOrigin.position : transform.position;
float maxRange = _bearingComp.AttackRange;
float maxRangeSquared = maxRange * maxRange;
EnemyEntity bestEnemy = null;
float bestDistanceSquared = float.MaxValue;
for (int i = 0; i < EnemyEntity.ActiveEnemies.Count; i++)
{
EnemyEntity enemy = EnemyEntity.ActiveEnemies[i];
if (enemy == null || !enemy.isActiveAndEnabled)
{
continue;
}
Transform enemyTransform = enemy.transform;
Vector3 delta = enemyTransform.position - origin;
float distanceSquared = delta.sqrMagnitude;
if (distanceSquared > maxRangeSquared || distanceSquared >= bestDistanceSquared)
{
continue;
}
bestDistanceSquared = distanceSquared;
bestEnemy = enemy;
}
return bestEnemy != null ? bestEnemy.transform : null;
}
private void ResolveComponents()
{
if (_muzzleComp == null)
{
_muzzleComp = GetComponent<ShooterMuzzleComp>();
}
if (_bearingComp == null)
{
_bearingComp = GetComponent<BasicBearingComp>();
}
if (_baseComp == null)
{
_baseComp = GetComponent<BasicBaseComp>();
}
}
private bool HasCoreComponents()
{
return _muzzleComp != null && _bearingComp != null && _baseComp != null;
}
private static bool IsTargetValid(Transform target)
{
return target != null && target.gameObject.activeInHierarchy;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b33f7d4136745b8bbf516ec80495b80
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
using GeometryTD.Definition;
namespace Components
{
public interface IDamageReceiver
{
void TakeDamage(int damage, AttackPropertyType attackPropertyType);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0b8c22c0144cd4e4c887c3cf49ba35a7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,121 @@
using UnityEngine;
using GeometryTD.Definition;
using GeometryTD.Entity.EntityData;
namespace Components
{
[DisallowMultipleComponent]
public class ShooterBullet : MonoBehaviour
{
[SerializeField] [Min(0.1f)] private float _defaultSpeed = 12f;
[SerializeField] [Min(0.02f)] private float _hitDistance = 0.15f;
[SerializeField] [Min(0.1f)] private float _maxLifetime = 3f;
private Transform _target;
private float _speed;
private int _damage;
private AttackPropertyType _attackPropertyType;
private float _lifetime;
private float _runtimeMaxLifetime;
private bool _isRunning;
private bool _despawnRequested;
public void OnShow(BulletData bulletData)
{
_target = bulletData != null ? bulletData.Target : null;
_damage = bulletData != null ? Mathf.Max(0, bulletData.Damage) : 0;
_attackPropertyType = bulletData != null ? bulletData.AttackPropertyType : AttackPropertyType.None;
_speed = bulletData != null && bulletData.Speed > 0f ? bulletData.Speed : _defaultSpeed;
_runtimeMaxLifetime = bulletData != null && bulletData.MaxLifetime > 0f ? bulletData.MaxLifetime : _maxLifetime;
_lifetime = 0f;
_despawnRequested = false;
_isRunning = _target != null;
if (!_isRunning)
{
_despawnRequested = true;
}
}
public void OnReset()
{
_target = null;
_speed = 0f;
_damage = 0;
_attackPropertyType = AttackPropertyType.None;
_lifetime = 0f;
_runtimeMaxLifetime = _maxLifetime;
_isRunning = false;
_despawnRequested = false;
}
public void Tick(float deltaTime)
{
if (!_isRunning)
{
return;
}
_lifetime += Mathf.Max(0f, deltaTime);
if (_lifetime >= _runtimeMaxLifetime || _target == null)
{
_isRunning = false;
_despawnRequested = true;
return;
}
Vector3 toTarget = _target.position - transform.position;
float hitDistanceSquared = _hitDistance * _hitDistance;
if (toTarget.sqrMagnitude <= hitDistanceSquared)
{
HitTarget();
return;
}
Vector3 forward = toTarget.normalized;
float moveDistance = _speed * Mathf.Max(0f, deltaTime);
if (moveDistance * moveDistance >= toTarget.sqrMagnitude)
{
transform.position = _target.position;
HitTarget();
return;
}
transform.position += forward * moveDistance;
transform.forward = forward;
}
public bool TryConsumeDespawnRequest()
{
if (!_despawnRequested)
{
return false;
}
_despawnRequested = false;
return true;
}
private void HitTarget()
{
if (_target == null)
{
_isRunning = false;
_despawnRequested = true;
return;
}
MonoBehaviour[] components = _target.GetComponentsInParent<MonoBehaviour>();
for (int i = 0; i < components.Length; i++)
{
if (components[i] is IDamageReceiver damageReceiver)
{
damageReceiver.TakeDamage(_damage, _attackPropertyType);
break;
}
}
_isRunning = false;
_despawnRequested = true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81f9627601c091545a9b92fc7ef41f04
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,66 @@
using GeometryTD.Definition;
using GeometryTD.Entity.EntityData;
using GeometryTD;
using UnityEngine;
namespace Components
{
[DisallowMultipleComponent]
public class ShooterMuzzleComp : MonoBehaviour
{
[SerializeField] [Min(1f)] private int _attackDamage = 10;
[SerializeField] private AttackMethodType _attackMethodType = AttackMethodType.NormalBullet;
[SerializeField] [Min(1)] private int _bulletTypeId = 501;
[SerializeField] private Transform _muzzlePoint;
[SerializeField] [Min(0.1f)] private float _bulletSpeed = 12f;
public int AttackDamage => _attackDamage;
public AttackMethodType AttackMethodType => _attackMethodType;
public void OnInit(int attackDamage, AttackMethodType attackMethodType = AttackMethodType.NormalBullet, int bulletTypeId = 501)
{
_attackDamage = Mathf.Max(1, attackDamage);
_attackMethodType = attackMethodType;
_bulletTypeId = Mathf.Max(1, bulletTypeId);
}
public void OnReset()
{
_attackDamage = 1;
_attackMethodType = AttackMethodType.None;
_bulletTypeId = 501;
}
public bool Attack(Transform target)
{
return Attack(target, AttackPropertyType.None);
}
public bool Attack(Transform target, AttackPropertyType attackPropertyType)
{
if (_attackMethodType != AttackMethodType.NormalBullet)
{
return false;
}
if (target == null)
{
return false;
}
Transform spawnPoint = _muzzlePoint != null ? _muzzlePoint : transform;
int bulletEntityId = GameEntry.Entity.GenerateSerialId();
BulletData bulletData = new BulletData(
bulletEntityId,
_bulletTypeId,
spawnPoint.position,
spawnPoint.rotation,
target,
_attackDamage,
_bulletSpeed,
attackPropertyType);
GameEntry.Entity.ShowBullet(bulletData);
return true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 20a2c131403122146a41148cb72fcd43
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -20,7 +20,7 @@ namespace GeometryTD.CustomComponent
}
private EntityComponent _entity;
private CombatInfoFormUseCase _useCase;
private CombatInfoFormUseCase _combatInfoFormUseCase;
private int _loadingMapEntityId;
private int _loadedMapEntityId;
private int? _loadingCombatInfoFormSerialId;
@@ -63,7 +63,7 @@ namespace GeometryTD.CustomComponent
{
return false;
}
return true;
}
@@ -113,7 +113,8 @@ namespace GeometryTD.CustomComponent
return EventHandleStatus.Ignored;
}
if (args.EntityLogicType != typeof(MapEntity) || args.Entity == null || args.Entity.Id != _loadingMapEntityId)
if (args.EntityLogicType != typeof(MapEntity) || args.Entity == null ||
args.Entity.Id != _loadingMapEntityId)
{
return EventHandleStatus.Ignored;
}
@@ -248,10 +249,10 @@ namespace GeometryTD.CustomComponent
private bool TryOpenCombatInfoForm(out string errorMessage)
{
errorMessage = null;
if (_useCase == null)
if (_combatInfoFormUseCase == null)
{
_useCase = new CombatInfoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatInfoForm, _useCase);
_combatInfoFormUseCase = new CombatInfoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatInfoForm, _combatInfoFormUseCase);
}
int? serialId = GameEntry.UIRouter.OpenUI(UIFormType.CombatInfoForm);
@@ -273,4 +274,4 @@ namespace GeometryTD.CustomComponent
_isCombatInfoFormReady = false;
}
}
}
}
@@ -27,11 +27,16 @@ namespace GeometryTD.CustomComponent
private bool _runtimeInitialized;
private int _currentCoin;
private int _currentGold;
public LevelThemeType CurrentThemeType { get; private set; }
public DRLevel CurrentLevel { get; private set; }
public int CurrentCoin => _currentCoin;
public int CurrentGold => _currentGold;
public bool CanEndCombat => _combatScheduler.CanEndCombat;
public int LastDefeatedEnemyCount { get; private set; }
public int LastGainedCoin { get; private set; }
public int LastGainedGold { get; private set; }
public int CurrentLevelPhaseCount
{
@@ -66,6 +71,10 @@ namespace GeometryTD.CustomComponent
CurrentThemeType = themeType;
CurrentLevel = null;
_currentCoin = 0;
_currentGold = 0;
LastDefeatedEnemyCount = 0;
LastGainedCoin = 0;
LastGainedGold = 0;
_levelsById.Clear();
_phasesByLevelId.Clear();
_spawnEntriesByPhaseId.Clear();
@@ -200,13 +209,21 @@ namespace GeometryTD.CustomComponent
CurrentLevel = selectedLevel;
_currentCoin = Mathf.Max(0, selectedLevel.StartCoin);
_currentGold = 0;
LastDefeatedEnemyCount = 0;
LastGainedCoin = 0;
LastGainedGold = 0;
_combatScheduler.Start(selectedLevel, phaseList, _selectedSpawnEntriesByPhaseId);
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create());
}
public void EndCombat()
{
LastDefeatedEnemyCount = _combatScheduler.DefeatedEnemyCount;
LastGainedCoin = _combatScheduler.GainedCoin;
LastGainedGold = _combatScheduler.GainedGold;
_currentCoin = 0;
_currentGold = 0;
GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create());
}
@@ -229,9 +246,54 @@ namespace GeometryTD.CustomComponent
{
CurrentLevel = null;
_currentCoin = 0;
_currentGold = 0;
LastDefeatedEnemyCount = 0;
LastGainedCoin = 0;
LastGainedGold = 0;
ShutdownCombatRuntime();
}
public void ApplyEnemyDropReward(int coin, int gold)
{
if (coin > 0)
{
_currentCoin += coin;
}
if (gold > 0)
{
_currentGold += gold;
}
}
public bool TryConsumeCoin(int coin)
{
int requiredCoin = Mathf.Max(0, coin);
if (requiredCoin <= 0)
{
return true;
}
if (_currentCoin < requiredCoin)
{
return false;
}
_currentCoin -= requiredCoin;
return true;
}
public void AddCoin(int coin)
{
int gainCoin = Mathf.Max(0, coin);
if (gainCoin <= 0)
{
return;
}
_currentCoin += gainCoin;
}
private void OnDestroy()
{
OnShutdown();
@@ -1,6 +1,9 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.Entity;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
@@ -12,8 +15,9 @@ namespace GeometryTD.CustomComponent
Idle = 0,
WaitingForLoading = 1,
RunningPhase = 2,
Completed = 3,
Failed = 4
WaitingForFinishReturn = 3,
Completed = 4,
Failed = 5
}
private readonly List<DRLevelPhase> _phaseBuffer = new();
@@ -25,8 +29,11 @@ namespace GeometryTD.CustomComponent
private EntityComponent _entity;
private DRLevel _currentLevel;
private CombatFinishFormUseCase _combatFinishFormUseCase;
private SchedulerState _state = SchedulerState.Idle;
private bool _initialized;
private int _gainedCoin;
private int _gainedGold;
public bool IsRunning => _state == SchedulerState.WaitingForLoading || _state == SchedulerState.RunningPhase;
public bool IsCompleted => _state == SchedulerState.Completed;
@@ -35,6 +42,9 @@ namespace GeometryTD.CustomComponent
public MapEntity CurrentMap => _loadSession.CurrentMap;
public int DisplayPhaseIndex => _phaseLoopRuntime.DisplayPhaseIndex;
public bool CanEndCombat => _phaseLoopRuntime.CanEndCombat;
public int DefeatedEnemyCount => _enemyManager.DefeatedEnemyCount;
public int GainedCoin => _gainedCoin;
public int GainedGold => _gainedGold;
public void OnInit()
{
@@ -50,6 +60,7 @@ namespace GeometryTD.CustomComponent
OnCloseUIFormComplete);
_enemyManager.OnInit(this);
_loadSession.OnInit(_entity);
EnsureCombatFinishFormUseCaseBound();
_initialized = true;
}
@@ -75,9 +86,13 @@ namespace GeometryTD.CustomComponent
return;
}
CleanupCombatEntities();
CleanupAllCombatEntities();
CloseCombatFinishForm();
_enemyManager.EndPhase();
_enemyManager.ResetCombatStats();
ResetRuntime();
_gainedCoin = 0;
_gainedGold = 0;
_currentLevel = level;
for (int i = 0; i < phases.Count; i++)
@@ -113,7 +128,8 @@ namespace GeometryTD.CustomComponent
}
_state = SchedulerState.WaitingForLoading;
Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id, _phaseLoopRuntime.PhaseCount);
Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id,
_phaseLoopRuntime.PhaseCount);
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
@@ -143,17 +159,20 @@ namespace GeometryTD.CustomComponent
return;
}
CleanupCombatEntities();
CleanupAllCombatEntities();
CloseCombatFinishForm();
_enemyManager.OnDestroy();
ResetRuntime();
_eventBridge.Unbind();
_combatFinishFormUseCase = null;
_entity = null;
_initialized = false;
}
public bool TryEndCombatByPlayer()
{
if (_state == SchedulerState.Completed || _state == SchedulerState.Failed || _state == SchedulerState.Idle)
if (_state != SchedulerState.RunningPhase)
{
return false;
}
@@ -163,7 +182,7 @@ namespace GeometryTD.CustomComponent
return false;
}
FinishCombat("Combat ended by player.");
EnterFinishFlow("Combat ended by player.");
return true;
}
@@ -185,7 +204,8 @@ namespace GeometryTD.CustomComponent
_phaseLoopRuntime.AdvancePhaseElapsed(elapseSeconds);
_enemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
if (!_phaseLoopRuntime.ShouldEndCurrentPhase(_enemyManager.IsPhaseSpawnCompleted, _enemyManager.AliveEnemyCount))
if (!_phaseLoopRuntime.ShouldEndCurrentPhase(_enemyManager.IsPhaseSpawnCompleted,
_enemyManager.AliveEnemyCount))
{
return;
}
@@ -209,7 +229,7 @@ namespace GeometryTD.CustomComponent
{
if (!_phaseLoopRuntime.TryEnterNextPhase(out DRLevelPhase nextPhase))
{
FinishCombat("Combat ended after loop completion.");
EnterFinishFlow("Combat ended after loop completion.");
return;
}
@@ -227,17 +247,21 @@ namespace GeometryTD.CustomComponent
spawnEntries != null ? spawnEntries.Count : 0);
}
private void FinishCombat(string reason)
private void EnterFinishFlow(string reason)
{
_state = SchedulerState.Completed;
CleanupCombatEntities();
int defeatedEnemyCount = _enemyManager.DefeatedEnemyCount;
// Step 1: stop runtime and clear enemy entities only.
_enemyManager.EndPhase();
_state = SchedulerState.WaitingForFinishReturn;
_enemyManager.CleanupTrackedEnemies();
Log.Info(
"CombatScheduler level completed. Level={0}. Reason={1}",
"CombatScheduler entered finish flow. Level={0}. Reason={1}",
_currentLevel != null ? _currentLevel.Id : 0,
reason);
GameEntry.CombatNode.EndCombat();
OpenCombatFinishForm(defeatedEnemyCount, _gainedGold);
}
private void ResetRuntime()
@@ -250,12 +274,57 @@ namespace GeometryTD.CustomComponent
_state = SchedulerState.Idle;
}
private void CleanupCombatEntities()
private void CleanupAllCombatEntities()
{
_loadSession.Cleanup();
_enemyManager.CleanupTrackedEnemies();
}
private void EnsureCombatFinishFormUseCaseBound()
{
_combatFinishFormUseCase ??= new CombatFinishFormUseCase(this);
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase);
}
private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold)
{
EnsureCombatFinishFormUseCaseBound();
_combatFinishFormUseCase.SetSummary(
defeatedEnemyCount,
gainedGold);
GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm);
}
public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold)
{
int coin = Mathf.Max(0, gainedCoin);
int gold = Mathf.Max(0, gainedGold);
_gainedCoin += coin;
_gainedGold += gold;
GameEntry.CombatNode?.ApplyEnemyDropReward(coin, gold);
}
private void CloseCombatFinishForm()
{
GameEntry.UIRouter.CloseUI(UIFormType.CombatFinishForm);
}
public bool OnCombatFinishReturnRequested()
{
if (_state != SchedulerState.WaitingForFinishReturn)
{
return false;
}
// Step 2: clear remaining map/UI resources and close finish form.
_loadSession.Cleanup();
CloseCombatFinishForm();
_state = SchedulerState.Completed;
GameEntry.CombatNode.EndCombat();
return true;
}
#region Event Handlers
private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args)
@@ -287,7 +356,8 @@ namespace GeometryTD.CustomComponent
if (status == CombatLoadSession.EventHandleStatus.Failed)
{
_state = SchedulerState.Failed;
Log.Error("CombatScheduler failed. LevelId={0}, {1}", _currentLevel != null ? _currentLevel.Id : 0, errorMessage);
Log.Error("CombatScheduler failed. LevelId={0}, {1}", _currentLevel != null ? _currentLevel.Id : 0,
errorMessage);
}
}
@@ -31,6 +31,7 @@ namespace GeometryTD.CustomComponent
private readonly List<SpawnEntryRuntime> _spawnRuntimes = new List<SpawnEntryRuntime>();
private readonly HashSet<int> _trackedEnemyEntityIds = new HashSet<int>();
private readonly List<int> _trackedEnemyIdBuffer = new List<int>();
private readonly Dictionary<int, DREnemy> _trackedEnemyConfigByEntityId = new Dictionary<int, DREnemy>();
private CombatScheduler _combatScheduler;
private EntityComponent _entity;
@@ -38,6 +39,7 @@ namespace GeometryTD.CustomComponent
private int _spawnEnemyMaxCount = 5000;
private int _currentEnemyCount;
private int _defeatedEnemyCount;
private int _nextSpawnerIndex;
private int _currentMapEntityId;
private bool _initialized;
@@ -47,6 +49,7 @@ namespace GeometryTD.CustomComponent
private bool _isPhaseRunning;
public int AliveEnemyCount => _currentEnemyCount;
public int DefeatedEnemyCount => _defeatedEnemyCount;
public bool IsPhaseSpawnCompleted { get; private set; } = true;
public bool IsPhaseRunning => _isPhaseRunning;
@@ -61,6 +64,7 @@ namespace GeometryTD.CustomComponent
_entity = GameEntry.Entity;
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
_currentEnemyCount = 0;
_defeatedEnemyCount = 0;
_nextSpawnerIndex = 0;
_currentMapEntityId = 0;
_enemyConfigMissingLogged = false;
@@ -70,6 +74,7 @@ namespace GeometryTD.CustomComponent
_spawnRuntimes.Clear();
_trackedEnemyEntityIds.Clear();
_trackedEnemyIdBuffer.Clear();
_trackedEnemyConfigByEntityId.Clear();
_phaseElapsed = 0f;
_isPhaseRunning = false;
IsPhaseSpawnCompleted = true;
@@ -149,13 +154,20 @@ namespace GeometryTD.CustomComponent
_pathBuffer.Clear();
_trackedEnemyEntityIds.Clear();
_trackedEnemyIdBuffer.Clear();
_trackedEnemyConfigByEntityId.Clear();
_currentEnemyCount = 0;
_defeatedEnemyCount = 0;
_currentMapEntityId = 0;
_nextSpawnerIndex = 0;
_combatScheduler = null;
_initialized = false;
}
public void ResetCombatStats()
{
_defeatedEnemyCount = 0;
}
public void CleanupTrackedEnemies()
{
if (_trackedEnemyEntityIds.Count <= 0)
@@ -171,6 +183,7 @@ namespace GeometryTD.CustomComponent
}
_trackedEnemyEntityIds.Clear();
_trackedEnemyConfigByEntityId.Clear();
_currentEnemyCount = 0;
if (_entity == null)
@@ -352,6 +365,7 @@ namespace GeometryTD.CustomComponent
int enemyEntityId = _entity.GenerateSerialId();
_trackedEnemyEntityIds.Add(enemyEntityId);
_trackedEnemyConfigByEntityId[enemyEntityId] = enemyConfig;
EnemyData enemyData = new EnemyData(
enemyEntityId,
enemyConfig.EntityId,
@@ -497,6 +511,7 @@ namespace GeometryTD.CustomComponent
}
_trackedEnemyEntityIds.Remove(ne.EntityId);
_trackedEnemyConfigByEntityId.Remove(ne.EntityId);
}
private void OnHideEntityComplete(object sender, GameEventArgs e)
@@ -508,10 +523,33 @@ namespace GeometryTD.CustomComponent
if (!_trackedEnemyEntityIds.Remove(ne.EntityId))
{
_trackedEnemyConfigByEntityId.Remove(ne.EntityId);
return;
}
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
bool wasKilled = EnemyEntity.TryConsumeKilledFlag(ne.EntityId);
if (_combatScheduler != null && _combatScheduler.IsRunning && wasKilled)
{
_defeatedEnemyCount++;
int droppedCoin = 0;
int droppedGold = 0;
if (_trackedEnemyConfigByEntityId.TryGetValue(ne.EntityId, out DREnemy enemyConfig) && enemyConfig != null)
{
droppedCoin = Mathf.Max(0, enemyConfig.DropCoin);
float dropRate = enemyConfig.DropPercent > 1f
? Mathf.Clamp01(enemyConfig.DropPercent * 0.01f)
: Mathf.Clamp01(enemyConfig.DropPercent);
if (enemyConfig.DropGold > 0 && dropRate > 0f && Random.value <= dropRate)
{
droppedGold = Mathf.Max(0, enemyConfig.DropGold);
}
}
_combatScheduler.OnEnemyDefeatedRewardResolved(droppedCoin, droppedGold);
}
_trackedEnemyConfigByEntityId.Remove(ne.EntityId);
}
}
}
}
+14 -2
View File
@@ -35,10 +35,20 @@ namespace GeometryTD.DataTable
public float Speed { get; private set; }
/// <summary>
/// 获取敌人掉落金币
/// 获取敌人掉落硬币
/// </summary>
public int DropCoin { get; private set; }
/// <summary>
/// 获取敌人掉落金币
/// </summary>
public int DropGold { get; private set; }
/// <summary>
/// 获取敌人掉落金币概率
/// </summary>
public float DropPercent { get; private set; }
public override bool ParseDataRow(string dataRowString, object userData)
{
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
@@ -56,8 +66,10 @@ namespace GeometryTD.DataTable
BaseDamage = int.Parse(columnStrings[index++]);
Speed = float.Parse(columnStrings[index++]);
DropCoin = int.Parse(columnStrings[index++]);
DropGold = int.Parse(columnStrings[index++]);
DropPercent = float.Parse(columnStrings[index++]);
return true;
}
}
}
}
@@ -46,6 +46,16 @@
/// 战斗信息界面。
/// </summary>
CombatInfoForm = 140,
/// <summary>
/// 战斗结算界面。
/// </summary>
CombatFinishForm = 141,
/// <summary>
/// 战斗选择界面
/// </summary>
CombatSelectForm = 142,
/// <summary>
/// 测试菜单。
@@ -0,0 +1,66 @@
using System;
using GeometryTD.Definition;
using UnityEngine;
namespace GeometryTD.Entity.EntityData
{
[Serializable]
public class BulletData : EntityDataBase
{
[SerializeField] private Transform _target = null;
[SerializeField] private int _damage = 0;
[SerializeField] private float _speed = 0f;
[SerializeField] private float _maxLifetime = 3f;
[SerializeField] private AttackPropertyType _attackPropertyType = AttackPropertyType.None;
public BulletData(
int entityId,
int typeId,
Vector3 position,
Quaternion rotation,
Transform target,
int damage,
float speed,
AttackPropertyType attackPropertyType = AttackPropertyType.None,
float maxLifetime = 3f) : base(entityId, typeId)
{
Position = position;
Rotation = rotation;
_target = target;
_damage = damage;
_speed = speed;
_attackPropertyType = attackPropertyType;
_maxLifetime = maxLifetime;
}
public Transform Target
{
get => _target;
set => _target = value;
}
public int Damage
{
get => _damage;
set => _damage = value;
}
public float Speed
{
get => _speed;
set => _speed = value;
}
public float MaxLifetime
{
get => _maxLifetime;
set => _maxLifetime = value;
}
public AttackPropertyType AttackPropertyType
{
get => _attackPropertyType;
set => _attackPropertyType = value;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f7248061a2a4f4f8ecf8d5db99f0a82
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,26 @@
using System;
using GeometryTD.Definition;
using UnityEngine;
namespace GeometryTD.Entity.EntityData
{
[Serializable]
public class DefenseTowerData : EntityDataBase
{
[SerializeField] private DefenseTowerStatsData _stats = new DefenseTowerStatsData();
public DefenseTowerData(int entityId, int typeId, Vector3 position, Quaternion rotation, DefenseTowerStatsData stats)
: base(entityId, typeId)
{
Position = position;
Rotation = rotation;
_stats = stats ?? new DefenseTowerStatsData();
}
public DefenseTowerStatsData Stats
{
get => _stats;
set => _stats = value ?? new DefenseTowerStatsData();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d46035f451d4e01a5a21ed5984ce42b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -41,16 +41,21 @@ namespace GeometryTD
entityComponent.AttachEntity(entityBase.Entity, ownerId, parentTransformPath, userData);
}
public static void ShowPlayer(this EntityComponent entityComponent, PlayerData data)
{
entityComponent.ShowEntity(typeof(Player), "Player", Constant.AssetPriority.PlayerAsset, data);
}
public static void ShowEnemy(this EntityComponent entityComponent, EnemyData data)
{
entityComponent.ShowEntity(typeof(EnemyEntity), "Enemy", Constant.AssetPriority.EnemyAsset, data);
}
public static void ShowDefenseTower(this EntityComponent entityComponent, DefenseTowerData data)
{
entityComponent.ShowEntity(typeof(DefenseTowerEntity), "Tower", Constant.AssetPriority.EnemyAsset, data);
}
public static void ShowBullet(this EntityComponent entityComponent, BulletData data)
{
entityComponent.ShowEntity(typeof(BulletEntity), "Bullet", Constant.AssetPriority.BulletAsset, data);
}
public static void ShowMap(this EntityComponent entityComponent, MapData data)
{
ShowMap(entityComponent, data, null);
@@ -0,0 +1,68 @@
using Components;
using GeometryTD.Entity.EntityData;
using UnityGameFramework.Runtime;
namespace GeometryTD.Entity
{
public class BulletEntity : EntityBase
{
private ShooterBullet _shooterBullet;
protected override void OnInit(object userData)
{
base.OnInit(userData);
_shooterBullet = GetComponent<ShooterBullet>();
if (_shooterBullet == null)
{
Log.Error("ShooterBullet component is missing on bullet entity '{0}'.", name);
}
}
protected override void OnShow(object userData)
{
base.OnShow(userData);
if (_shooterBullet == null)
{
GameEntry.Entity.HideEntity(Entity);
return;
}
if (userData is not BulletData bulletData)
{
Log.Warning("BulletData is invalid for bullet entity '{0}'.", Id);
GameEntry.Entity.HideEntity(Entity);
return;
}
_shooterBullet.OnShow(bulletData);
if (_shooterBullet.TryConsumeDespawnRequest())
{
GameEntry.Entity.HideEntity(Entity);
}
}
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
if (_shooterBullet == null)
{
return;
}
_shooterBullet.Tick(elapseSeconds);
if (_shooterBullet.TryConsumeDespawnRequest())
{
GameEntry.Entity.HideEntity(Entity);
}
}
protected override void OnHide(bool isShutdown, object userData)
{
_shooterBullet?.OnReset();
base.OnHide(isShutdown, userData);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e4912a44cc6c4f3d85f6d54868780f2f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,55 @@
using Components;
using GeometryTD.Entity.EntityData;
using UnityGameFramework.Runtime;
namespace GeometryTD.Entity
{
public class DefenseTowerEntity : EntityBase
{
private DefenseTowerController _towerController;
protected override void OnInit(object userData)
{
base.OnInit(userData);
_towerController = GetComponent<DefenseTowerController>();
if (_towerController == null)
{
Log.Error("DefenseTowerController is missing on tower entity '{0}'.", name);
}
}
protected override void OnShow(object userData)
{
base.OnShow(userData);
if (_towerController == null)
{
return;
}
if (userData is not DefenseTowerData towerData)
{
Log.Warning("DefenseTowerData is invalid for tower entity '{0}'.", Id);
_towerController.OnReset();
GameEntry.Entity.HideEntity(Entity);
return;
}
_towerController.SetAutoUpdate(false);
_towerController.OnInit(towerData.Stats);
}
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
_towerController?.OnUpdate(elapseSeconds);
}
protected override void OnHide(bool isShutdown, object userData)
{
_towerController?.OnReset();
base.OnHide(isShutdown, userData);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d87f56efd9024709a3baf8ef8a6f4fd3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,21 +1,33 @@
using Components;
using System.Collections.Generic;
using GeometryTD.Definition;
using GeometryTD.Entity.EntityData;
using UnityEngine;
namespace GeometryTD.Entity
{
public class EnemyEntity : EntityBase
public class EnemyEntity : EntityBase, IDamageReceiver
{
private const float WaypointReachDistance = 0.05f;
private static readonly List<EnemyEntity> _activeEnemies = new();
private static readonly HashSet<int> _killedEnemyEntityIds = new();
private Transform _target;
private float _speed;
private int _maxHealth;
private int _currentHealth;
private MovementComponent _movementComponent;
private readonly List<Vector3> _pathPoints = new List<Vector3>();
private readonly List<Vector3> _pathPoints = new();
private int _pathPointIndex;
private bool _isDespawnRequested;
public static IReadOnlyList<EnemyEntity> ActiveEnemies => _activeEnemies;
public static bool TryConsumeKilledFlag(int entityId)
{
return _killedEnemyEntityIds.Remove(entityId);
}
protected override void OnInit(object userData)
{
base.OnInit(userData);
@@ -31,10 +43,14 @@ namespace GeometryTD.Entity
_pathPoints.Clear();
_pathPointIndex = 0;
_isDespawnRequested = false;
_maxHealth = 1;
_currentHealth = 1;
if (userData is EnemyData enemyData)
{
_speed = enemyData.Speed;
_target = enemyData.Player;
_maxHealth = Mathf.Max(1, enemyData.MaxHealth);
_currentHealth = _maxHealth;
if (enemyData.HasPath)
{
IReadOnlyList<Vector3> pathPoints = enemyData.PathPoints;
@@ -47,6 +63,11 @@ namespace GeometryTD.Entity
_movementComponent.OnInit(_speed, CachedTransform);
_movementComponent.SetMove(true);
_killedEnemyEntityIds.Remove(Id);
if (!_activeEnemies.Contains(this))
{
_activeEnemies.Add(this);
}
}
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
@@ -66,10 +87,45 @@ namespace GeometryTD.Entity
_pathPoints.Clear();
_pathPointIndex = 0;
_isDespawnRequested = false;
_maxHealth = 0;
_currentHealth = 0;
_activeEnemies.Remove(this);
_killedEnemyEntityIds.Remove(Id);
base.OnHide(isShutdown, userData);
}
private void OnDestroy()
{
_activeEnemies.Remove(this);
if (Entity != null)
{
_killedEnemyEntityIds.Remove(Entity.Id);
}
}
public void TakeDamage(int damage, AttackPropertyType attackPropertyType)
{
_ = attackPropertyType;
if (_isDespawnRequested || damage <= 0 || _currentHealth <= 0)
{
return;
}
int previousHealth = _currentHealth;
_currentHealth = Mathf.Max(0, _currentHealth - damage);
if (_maxHealth > 0)
{
GameEntry.HPBar?.ShowHPBar(this, (float)previousHealth / _maxHealth, (float)_currentHealth / _maxHealth);
}
if (_currentHealth <= 0)
{
_killedEnemyEntityIds.Add(Id);
RequestDespawn();
}
}
private void UpdatePathMovement(float elapseSeconds, float realElapseSeconds)
{
if (_isDespawnRequested)
@@ -120,6 +176,11 @@ namespace GeometryTD.Entity
}
private void DespawnOnReachHouse()
{
RequestDespawn();
}
private void RequestDespawn()
{
if (_isDespawnRequested)
{
@@ -131,4 +192,4 @@ namespace GeometryTD.Entity
GameEntry.Entity.HideEntity(Entity);
}
}
}
}
@@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using GeometryTD;
using GeometryTD.Definition;
using GeometryTD.Entity.EntityData;
using GeometryTD.Pathfinding;
using GeometryTD.UI;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Tilemaps;
using UnityGameFramework.Runtime;
@@ -12,6 +16,7 @@ namespace GeometryTD.Entity
{
private const string PathTileName = "Path";
private const string FoundationTileName = "Foundation";
private const int DefaultTowerTypeId = 401;
private static readonly Spawner[] EmptySpawners = Array.Empty<Spawner>();
private readonly List<Vector3Int> _pathCells = new();
@@ -22,11 +27,25 @@ namespace GeometryTD.Entity
private readonly List<Vector3Int> _pathCellBuffer = new();
private readonly Dictionary<Spawner, Vector3Int> _spawnerPathStartByRef = new();
private readonly Dictionary<Spawner, List<Vector3Int>> _defaultPathCellsBySpawner = new();
private readonly Dictionary<Vector3Int, int> _towerEntityIdByFoundationCell = new();
private readonly Dictionary<int, Vector3Int> _foundationCellByTowerEntityId = new();
private readonly Dictionary<int, DefenseTowerStatsData> _towerStatsByEntityId = new();
private readonly List<int> _towerEntityIdBuffer = new();
[SerializeField] private bool _enableCombatSelectInput = true;
[SerializeField] private int _towerTypeId = DefaultTowerTypeId;
[SerializeField] private int[] _buildTowerCosts = { 80, 120, 160, 220 };
[SerializeField] private int _upgradeCost = 80;
[SerializeField] private int _destroyGain = 40;
private MapDataRefs _mapDataRefs;
private MapData _mapData;
private bool _hasHousePathCell;
private Vector3Int _housePathCell;
private CombatSelectFormUseCase _combatSelectFormUseCase;
private bool _hasSelectedFoundationCell;
private Vector3Int _selectedFoundationCell;
private int _selectedTowerEntityId;
public IReadOnlyList<Vector3Int> PathCells => _pathCells;
public IReadOnlyList<Vector3Int> FoundationCells => _foundationCells;
@@ -156,6 +175,8 @@ namespace GeometryTD.Entity
{
Log.Error("MapDataRefs is missing on map entity '{0}'.", name);
}
InitializeCombatSelectUseCase();
}
protected override void OnShow(object userData)
@@ -169,14 +190,24 @@ namespace GeometryTD.Entity
}
RefreshTiles();
ConfigureCombatSelectUseCase();
HideCombatSelectForm();
}
protected override void OnHide(bool isShutdown, object userData)
{
HideCombatSelectForm();
ClearPlacedTowers();
ClearRuntimeData();
base.OnHide(isShutdown, userData);
}
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
HandleCombatSelectInput();
}
private void RefreshTiles()
{
ClearRuntimeData();
@@ -295,6 +326,479 @@ namespace GeometryTD.Entity
_housePathCell = default;
_spawnerPathStartByRef.Clear();
_defaultPathCellsBySpawner.Clear();
_towerEntityIdByFoundationCell.Clear();
_foundationCellByTowerEntityId.Clear();
_towerStatsByEntityId.Clear();
_towerEntityIdBuffer.Clear();
ClearSelectedObject();
}
private void InitializeCombatSelectUseCase()
{
if (_combatSelectFormUseCase == null)
{
_combatSelectFormUseCase = new CombatSelectFormUseCase();
}
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatSelectForm, _combatSelectFormUseCase);
}
private void ConfigureCombatSelectUseCase()
{
if (_combatSelectFormUseCase == null)
{
return;
}
_combatSelectFormUseCase.SetCoinProvider(GetCurrentCoin);
for (int i = 0; i < 4; i++)
{
int buildIndex = i;
_combatSelectFormUseCase.SetBuildAction(
buildIndex,
() => TryBuildTower(buildIndex),
GetBuildTowerCost(buildIndex));
}
_combatSelectFormUseCase.SetUpgradeAction(
TryUpgradeTower,
Mathf.Max(0, _upgradeCost));
_combatSelectFormUseCase.SetDestroyAction(
TryDestroyTower,
Mathf.Max(0, _destroyGain));
}
private void HandleCombatSelectInput()
{
if (!_enableCombatSelectInput || !Input.GetMouseButtonDown(0))
{
return;
}
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
{
return;
}
if (!TryBuildCombatSelectUserData(out CombatSelectFormUserData userData))
{
userData = new CombatSelectFormUserData
{
ClickObjectType = CombatSelectClickObjectType.None
};
}
ApplySelectedObject(userData);
GameEntry.UIRouter.OpenUI(UIFormType.CombatSelectForm, userData);
}
private bool TryBuildCombatSelectUserData(out CombatSelectFormUserData userData)
{
userData = null;
if (Tilemap == null || !TryGetPointerWorldPosition(out Vector3 worldPosition, out Vector2 contentPosition))
{
return false;
}
Vector3Int clickedCell = Tilemap.WorldToCell(worldPosition);
CombatSelectClickObjectType clickObjectType = CombatSelectClickObjectType.None;
int towerEntityId = 0;
if (_towerEntityIdByFoundationCell.TryGetValue(clickedCell, out int occupiedTowerEntityId))
{
clickObjectType = CombatSelectClickObjectType.Tower;
towerEntityId = occupiedTowerEntityId;
}
else if (IsFoundationCell(clickedCell))
{
clickObjectType = CombatSelectClickObjectType.Foundation;
}
userData = new CombatSelectFormUserData
{
ClickObjectType = clickObjectType,
ContentPosition = contentPosition,
WorldPosition = worldPosition,
CellPosition = clickedCell,
TowerEntityId = towerEntityId,
UpgradeCost = Mathf.Max(0, _upgradeCost),
DestroyGain = Mathf.Max(0, _destroyGain)
};
return true;
}
private bool TryGetPointerWorldPosition(out Vector3 worldPosition, out Vector2 contentPosition)
{
worldPosition = Vector3.zero;
contentPosition = Vector2.zero;
Camera mainCamera = GameEntry.Scene != null ? GameEntry.Scene.MainCamera : Camera.main;
if (mainCamera == null)
{
return false;
}
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
float mapPlaneZ = Tilemap != null ? Tilemap.transform.position.z : CachedTransform.position.z;
Vector3 planeNormal = mainCamera.transform.forward.sqrMagnitude > Mathf.Epsilon
? -mainCamera.transform.forward
: Vector3.forward;
Plane mapPlane = new Plane(planeNormal, new Vector3(0f, 0f, mapPlaneZ));
if (!mapPlane.Raycast(ray, out float enterDistance))
{
return false;
}
worldPosition = ray.GetPoint(enterDistance);
contentPosition = BuildContentPosition(Input.mousePosition);
return true;
}
private static Vector2 BuildContentPosition(Vector3 pointerScreenPosition)
{
return new Vector2(
pointerScreenPosition.x - Screen.width * 0.5f,
pointerScreenPosition.y - Screen.height * 0.5f);
}
private void ApplySelectedObject(CombatSelectFormUserData userData)
{
if (userData == null)
{
ClearSelectedObject();
return;
}
switch (userData.ClickObjectType)
{
case CombatSelectClickObjectType.Foundation:
_hasSelectedFoundationCell = true;
_selectedFoundationCell = userData.CellPosition;
_selectedTowerEntityId = 0;
break;
case CombatSelectClickObjectType.Tower:
_hasSelectedFoundationCell = true;
_selectedFoundationCell = userData.CellPosition;
_selectedTowerEntityId = userData.TowerEntityId;
break;
default:
ClearSelectedObject();
break;
}
}
private bool TryBuildTower(int buildIndex)
{
if (!_hasSelectedFoundationCell || !IsFoundationCell(_selectedFoundationCell))
{
return false;
}
if (_towerEntityIdByFoundationCell.ContainsKey(_selectedFoundationCell))
{
return false;
}
int buildCost = GetBuildTowerCost(buildIndex);
if (!TryConsumeCoin(buildCost))
{
return false;
}
DefenseTowerStatsData towerStats = BuildTowerStats(buildIndex);
if (!TryShowTowerEntity(_selectedFoundationCell, towerStats, out int towerEntityId))
{
GameEntry.CombatNode?.AddCoin(buildCost);
return false;
}
_towerEntityIdByFoundationCell[_selectedFoundationCell] = towerEntityId;
_foundationCellByTowerEntityId[towerEntityId] = _selectedFoundationCell;
_towerStatsByEntityId[towerEntityId] = CloneTowerStats(towerStats);
_selectedTowerEntityId = towerEntityId;
return true;
}
private bool TryUpgradeTower()
{
if (!TryGetSelectedTower(out int towerEntityId, out Vector3Int foundationCell))
{
return false;
}
int upgradeCost = Mathf.Max(0, _upgradeCost);
if (!TryConsumeCoin(upgradeCost))
{
return false;
}
DefenseTowerStatsData oldStats = _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, out int newTowerEntityId))
{
if (TryShowTowerEntity(foundationCell, oldStats, out int fallbackTowerEntityId))
{
_towerEntityIdByFoundationCell[foundationCell] = fallbackTowerEntityId;
_foundationCellByTowerEntityId[fallbackTowerEntityId] = foundationCell;
_towerStatsByEntityId[fallbackTowerEntityId] = CloneTowerStats(oldStats);
_selectedTowerEntityId = fallbackTowerEntityId;
}
GameEntry.CombatNode?.AddCoin(upgradeCost);
return false;
}
_towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId;
_foundationCellByTowerEntityId[newTowerEntityId] = foundationCell;
_towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(upgradedStats);
_hasSelectedFoundationCell = true;
_selectedFoundationCell = foundationCell;
_selectedTowerEntityId = newTowerEntityId;
return true;
}
private bool TryDestroyTower()
{
if (!TryGetSelectedTower(out int towerEntityId, out Vector3Int foundationCell))
{
return false;
}
HideTowerEntity(towerEntityId);
_towerEntityIdByFoundationCell.Remove(foundationCell);
_foundationCellByTowerEntityId.Remove(towerEntityId);
_towerStatsByEntityId.Remove(towerEntityId);
GameEntry.CombatNode?.AddCoin(Mathf.Max(0, _destroyGain));
ClearSelectedObject();
return true;
}
private bool TryGetSelectedTower(out int towerEntityId, out Vector3Int foundationCell)
{
towerEntityId = 0;
foundationCell = default;
if (_selectedTowerEntityId == 0)
{
return false;
}
if (!_foundationCellByTowerEntityId.TryGetValue(_selectedTowerEntityId, out foundationCell))
{
return false;
}
towerEntityId = _selectedTowerEntityId;
return true;
}
private bool TryShowTowerEntity(Vector3Int foundationCell, DefenseTowerStatsData towerStats, out int towerEntityId)
{
towerEntityId = 0;
if (GameEntry.Entity == null)
{
return false;
}
int entityId = GameEntry.Entity.GenerateSerialId();
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);
GameEntry.Entity.ShowDefenseTower(towerData);
towerEntityId = entityId;
return true;
}
private static void HideTowerEntity(int towerEntityId)
{
if (towerEntityId == 0 || GameEntry.Entity == null)
{
return;
}
UnityGameFramework.Runtime.Entity towerEntity = GameEntry.Entity.GetEntity(towerEntityId);
if (towerEntity != null)
{
GameEntry.Entity.HideEntity(towerEntity);
}
}
private void ClearPlacedTowers()
{
_towerEntityIdBuffer.Clear();
foreach (KeyValuePair<int, Vector3Int> pair in _foundationCellByTowerEntityId)
{
_towerEntityIdBuffer.Add(pair.Key);
}
for (int i = 0; i < _towerEntityIdBuffer.Count; i++)
{
HideTowerEntity(_towerEntityIdBuffer[i]);
}
_towerEntityIdByFoundationCell.Clear();
_foundationCellByTowerEntityId.Clear();
_towerStatsByEntityId.Clear();
_towerEntityIdBuffer.Clear();
ClearSelectedObject();
}
private void HideCombatSelectForm()
{
_combatSelectFormUseCase?.Hide();
GameEntry.UIRouter.CloseUI(UIFormType.CombatSelectForm);
}
private void ClearSelectedObject()
{
_hasSelectedFoundationCell = false;
_selectedFoundationCell = default;
_selectedTowerEntityId = 0;
}
private int GetBuildTowerCost(int buildIndex)
{
if (_buildTowerCosts == null || buildIndex < 0 || buildIndex >= _buildTowerCosts.Length)
{
return 0;
}
return Mathf.Max(0, _buildTowerCosts[buildIndex]);
}
private static bool TryConsumeCoin(int cost)
{
int requiredCoin = Mathf.Max(0, cost);
if (requiredCoin <= 0)
{
return true;
}
if (GameEntry.CombatNode == null)
{
return false;
}
return GameEntry.CombatNode.TryConsumeCoin(requiredCoin);
}
private static int GetCurrentCoin()
{
return GameEntry.CombatNode != null ? Mathf.Max(0, GameEntry.CombatNode.CurrentCoin) : 0;
}
private static DefenseTowerStatsData BuildTowerStats(int buildIndex)
{
switch (buildIndex)
{
case 0:
return new DefenseTowerStatsData
{
AttackDamage = 10,
DamageRandomRate = 0f,
RotateSpeed = 200f,
AttackRange = 4.5f,
AttackSpeed = 1.5f,
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Physics,
Tags = Array.Empty<TagType>()
};
case 1:
return new DefenseTowerStatsData
{
AttackDamage = 13,
DamageRandomRate = 0f,
RotateSpeed = 160f,
AttackRange = 5f,
AttackSpeed = 1.2f,
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Fire,
Tags = Array.Empty<TagType>()
};
case 2:
return new DefenseTowerStatsData
{
AttackDamage = 17,
DamageRandomRate = 0f,
RotateSpeed = 140f,
AttackRange = 5.5f,
AttackSpeed = 0.95f,
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Ice,
Tags = Array.Empty<TagType>()
};
case 3:
return new DefenseTowerStatsData
{
AttackDamage = 22,
DamageRandomRate = 0f,
RotateSpeed = 120f,
AttackRange = 6f,
AttackSpeed = 0.75f,
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Poison,
Tags = Array.Empty<TagType>()
};
default:
return new DefenseTowerStatsData
{
AttackDamage = 10,
DamageRandomRate = 0f,
RotateSpeed = 180f,
AttackRange = 5f,
AttackSpeed = 1f,
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Physics,
Tags = Array.Empty<TagType>()
};
}
}
private static DefenseTowerStatsData CloneTowerStats(DefenseTowerStatsData source)
{
if (source == null)
{
return BuildTowerStats(0);
}
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,
AttackMethodType = source.AttackMethodType,
AttackPropertyType = source.AttackPropertyType,
Tags = copiedTags
};
}
private static void ApplyUpgradeToStats(DefenseTowerStatsData stats)
{
if (stats == null)
{
return;
}
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);
}
}
}
}
@@ -0,0 +1,21 @@
using GameFramework;
using GameFramework.Event;
namespace GeometryTD.CustomEvent
{
public class CombatFinishReturnEventArgs : GameEventArgs
{
public static int EventId => typeof(CombatFinishReturnEventArgs).GetHashCode();
public override int Id => EventId;
public static CombatFinishReturnEventArgs Create()
{
return ReferencePool.Acquire<CombatFinishReturnEventArgs>();
}
public override void Clear()
{
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8ff2a3f68ab24bf294b24907548c88de
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,31 @@
using GameFramework;
using GameFramework.Event;
using GeometryTD.UI;
namespace GeometryTD.CustomEvent
{
public class CombatSelectItemClickEventArgs : GameEventArgs
{
public static int EventId => typeof(CombatSelectItemClickEventArgs).GetHashCode();
public override int Id => EventId;
public CombatSelectActionType ActionType { get; private set; } = CombatSelectActionType.None;
public int ActionIndex { get; private set; } = -1;
public static CombatSelectItemClickEventArgs Create(CombatSelectActionType actionType, int actionIndex)
{
CombatSelectItemClickEventArgs args = ReferencePool.Acquire<CombatSelectItemClickEventArgs>();
args.ActionType = actionType;
args.ActionIndex = actionIndex;
return args;
}
public override void Clear()
{
ActionType = CombatSelectActionType.None;
ActionIndex = -1;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d8a9619fe7cdb054ea7eeac820f95ae4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -63,6 +63,7 @@ namespace GeometryTD.Procedure
GameEntry.CombatNode.OnShutdown();
GameEntry.Event.Unsubscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameEntry.Event.Unsubscribe(NodeCompleteEventArgs.EventId, OnNodeComplete);
_repoFormUseCase = null;
base.OnLeave(procedureOwner, isShutdown);
}
@@ -85,4 +86,4 @@ namespace GeometryTD.Procedure
GameEntry.UIRouter.OpenUI(UIFormType.MainForm);
}
}
}
}
@@ -0,0 +1,10 @@
namespace GeometryTD.UI
{
public class CombatFinishFormContext : UIContext
{
public string EnemyKilledText;
public string GoldGainedText;
public RepoItemContext[] RewardItems;
public bool CanReturn;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e8fd0de74a04422488e2e78615713bca
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,10 @@
namespace GeometryTD.UI
{
public enum CombatSelectActionType : byte
{
None = 0,
BuildTower = 1,
UpgradeTower = 2,
DestroyTower = 3
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2813faca2af4a574ea60171b6221c69c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,15 @@
using UnityEngine;
namespace GeometryTD.UI
{
public class CombatSelectFormContext : UIContext
{
public bool IsVisible;
public Vector2 ContentPosition;
public bool ShowBuildArea;
public bool ShowFuncArea;
public TowerSelectItemContext[] BuildItems;
public TowerSelectItemContext UpgradeItem;
public TowerSelectItemContext DestroyItem;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 10d666a1cb0d16a44a6fa5e92d6e8f3f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
using UnityEngine;
namespace GeometryTD.UI
{
public class TowerSelectItemContext : UIContext
{
public Sprite Icon;
public string PriceText;
public bool IsVisible;
public bool IsInteractable;
public CombatSelectActionType ActionType;
public int ActionIndex;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1a54121a8acc89c43b45617601960c33
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,189 @@
using System.Collections.Generic;
using GameFramework.Event;
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public class CombatFinishFormController : UIFormControllerCommonBase<CombatFinishFormContext, CombatFinishForm>
{
private CombatFinishFormUseCase _useCase;
protected override UIFormType UIFormTypeId => UIFormType.CombatFinishForm;
protected override void RefreshUI(CombatFinishForm form, CombatFinishFormContext context)
{
form.RefreshUI(context);
}
protected override void SubscribeCustomEvents()
{
GameEntry.Event.Subscribe(CombatFinishReturnEventArgs.EventId, OnCombatFinishReturnButtonClicked);
}
protected override void UnsubscribeCustomEvents()
{
GameEntry.Event.Unsubscribe(CombatFinishReturnEventArgs.EventId, OnCombatFinishReturnButtonClicked);
}
public override int? OpenUI(object userData = null)
{
if (userData is CombatFinishFormContext context)
{
return OpenUIInternal(context);
}
if (userData is CombatFinishFormRawData rawDataFromUserData)
{
return OpenUI(rawDataFromUserData);
}
if (userData != null)
{
Log.Warning("CombatFinishFormController.OpenUI() userData type is invalid.");
return null;
}
if (_useCase == null)
{
Log.Error("CombatFinishFormController.OpenUI() useCase is null.");
return null;
}
CombatFinishFormRawData rawData = _useCase.CreateInitialModel();
return OpenUI(rawData);
}
public int? OpenUI(CombatFinishFormRawData rawData)
{
CombatFinishFormContext context = BuildContext(rawData);
return OpenUIInternal(context);
}
public override void BindUseCase(IUIUseCase useCase)
{
if (!(useCase is CombatFinishFormUseCase combatFinishFormUseCase))
{
Log.Error("CombatFinishFormController.BindUseCase() useCase is invalid.");
return;
}
_useCase = combatFinishFormUseCase;
}
private static CombatFinishFormContext BuildContext(CombatFinishFormRawData rawData)
{
if (rawData == null)
{
return null;
}
return new CombatFinishFormContext
{
EnemyKilledText = rawData.DefeatedEnemyCount.ToString(),
GoldGainedText = rawData.GainedGold.ToString(),
RewardItems = BuildRewardItems(rawData.RewardInventory),
CanReturn = rawData.CanReturn
};
}
private static RepoItemContext[] BuildRewardItems(BackpackInventoryData inventory)
{
if (inventory == null)
{
return System.Array.Empty<RepoItemContext>();
}
List<RepoItemContext> itemContexts = new List<RepoItemContext>();
if (inventory.Towers != null)
{
for (int i = 0; i < inventory.Towers.Count; i++)
{
var tower = inventory.Towers[i];
if (tower == null)
{
continue;
}
itemContexts.Add(new RepoItemContext
{
Title = $"[Tower] {tower.Name} ({tower.Rarity})",
InstanceId = tower.InstanceId,
ComponentSlotType = TowerCompSlotType.None
});
}
}
if (inventory.MuzzleComponents != null)
{
for (int i = 0; i < inventory.MuzzleComponents.Count; i++)
{
var item = inventory.MuzzleComponents[i];
if (item == null)
{
continue;
}
itemContexts.Add(new RepoItemContext
{
Title = $"[Muzzle] {item.Name} ({item.Rarity})",
InstanceId = item.InstanceId,
ComponentSlotType = TowerCompSlotType.Muzzle
});
}
}
if (inventory.BearingComponents != null)
{
for (int i = 0; i < inventory.BearingComponents.Count; i++)
{
var item = inventory.BearingComponents[i];
if (item == null)
{
continue;
}
itemContexts.Add(new RepoItemContext
{
Title = $"[Bearing] {item.Name} ({item.Rarity})",
InstanceId = item.InstanceId,
ComponentSlotType = TowerCompSlotType.Bearing
});
}
}
if (inventory.BaseComponents != null)
{
for (int i = 0; i < inventory.BaseComponents.Count; i++)
{
var item = inventory.BaseComponents[i];
if (item == null)
{
continue;
}
itemContexts.Add(new RepoItemContext
{
Title = $"[Base] {item.Name} ({item.Rarity})",
InstanceId = item.InstanceId,
ComponentSlotType = TowerCompSlotType.Base
});
}
}
return itemContexts.ToArray();
}
private void OnCombatFinishReturnButtonClicked(object sender, GameEventArgs e)
{
if (!(sender is CombatFinishForm) || !(e is CombatFinishReturnEventArgs))
{
return;
}
_useCase?.TryReturnToMenu();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f9701c56f0541208d99c47b7e64e58c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,266 @@
using GameFramework.Event;
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public class CombatSelectFormController : UIFormControllerCommonBase<CombatSelectFormContext, CombatSelectForm>
{
private CombatSelectFormUseCase _useCase;
protected override UIFormType UIFormTypeId => UIFormType.CombatSelectForm;
protected override void RefreshUI(CombatSelectForm form, CombatSelectFormContext context)
{
form.RefreshUI(context);
}
protected override void SubscribeCustomEvents()
{
GameEntry.Event.Subscribe(CombatSelectItemClickEventArgs.EventId, OnSelectItemClicked);
}
protected override void UnsubscribeCustomEvents()
{
GameEntry.Event.Unsubscribe(CombatSelectItemClickEventArgs.EventId, OnSelectItemClicked);
}
public int? OpenUI(CombatSelectFormRawData rawData)
{
CombatSelectFormContext context = BuildContext(rawData);
if (context == null || !context.IsVisible)
{
CloseUI();
return null;
}
return OpenUIInternal(context);
}
public override int? OpenUI(object userData = null)
{
if (userData is CombatSelectFormUserData clickUserData)
{
return OpenUI(clickUserData);
}
if (userData is CombatSelectFormContext context)
{
if (context == null || !context.IsVisible)
{
CloseUI();
return null;
}
return OpenUIInternal(context);
}
if (userData is CombatSelectFormRawData rawDataFromUserData)
{
return OpenUI(rawDataFromUserData);
}
if (userData != null)
{
Log.Warning("CombatSelectFormController.OpenUI() userData type is invalid.");
return null;
}
if (_useCase == null)
{
Log.Error("CombatSelectFormController.OpenUI() useCase is null.");
return null;
}
CombatSelectFormRawData rawData = _useCase.CreateInitialModel();
return OpenUI(rawData);
}
public int? OpenUI(CombatSelectFormUserData userData)
{
if (_useCase == null)
{
Log.Error("CombatSelectFormController.OpenUI() useCase is null.");
return null;
}
if (userData == null)
{
_useCase.Hide();
CloseUI();
return null;
}
switch (userData.ClickObjectType)
{
case CombatSelectClickObjectType.Foundation:
_useCase.ShowForFoundation(userData.ContentPosition);
break;
case CombatSelectClickObjectType.Tower:
_useCase.SetUpgradePrice(userData.UpgradeCost);
_useCase.SetDestroyGain(userData.DestroyGain);
_useCase.ShowForTower(userData.ContentPosition);
break;
default:
_useCase.Hide();
CloseUI();
return null;
}
return OpenUI(_useCase.TryRefresh());
}
public override void BindUseCase(IUIUseCase useCase)
{
if (!(useCase is CombatSelectFormUseCase combatSelectFormUseCase))
{
Log.Error("CombatSelectFormController.BindUseCase() useCase is invalid.");
return;
}
_useCase = combatSelectFormUseCase;
}
public int? ShowForFoundation(Vector2 contentPosition)
{
if (_useCase == null)
{
Log.Error("CombatSelectFormController.ShowForFoundation() useCase is null.");
return null;
}
_useCase.ShowForFoundation(contentPosition);
return OpenUI(_useCase.TryRefresh());
}
public int? ShowForTower(Vector2 contentPosition, int upgradeCost, int destroyGain)
{
if (_useCase == null)
{
Log.Error("CombatSelectFormController.ShowForTower() useCase is null.");
return null;
}
_useCase.SetUpgradePrice(upgradeCost);
_useCase.SetDestroyGain(destroyGain);
_useCase.ShowForTower(contentPosition);
return OpenUI(_useCase.TryRefresh());
}
public void HideSelection()
{
_useCase?.Hide();
CloseUI();
}
public void RefreshFromUseCase()
{
if (_useCase == null)
{
return;
}
CombatSelectFormRawData rawData = _useCase.TryRefresh();
CombatSelectFormContext context = BuildContext(rawData);
if (context == null || !context.IsVisible)
{
CloseUI();
return;
}
SetContext(context);
RefreshCurrentUI();
}
private static CombatSelectFormContext BuildContext(CombatSelectFormRawData rawData)
{
if (rawData == null)
{
return null;
}
return new CombatSelectFormContext
{
IsVisible = rawData.IsVisible,
ContentPosition = rawData.ContentPosition,
ShowBuildArea = rawData.IsVisible && rawData.DisplayMode == CombatSelectDisplayMode.Build,
ShowFuncArea = rawData.IsVisible && rawData.DisplayMode == CombatSelectDisplayMode.Func,
BuildItems = BuildItemContexts(rawData.BuildItems),
UpgradeItem = BuildItemContext(rawData.UpgradeItem),
DestroyItem = BuildItemContext(rawData.DestroyItem)
};
}
private static TowerSelectItemContext[] BuildItemContexts(TowerSelectItemRawData[] itemRawData)
{
if (itemRawData == null || itemRawData.Length <= 0)
{
return System.Array.Empty<TowerSelectItemContext>();
}
TowerSelectItemContext[] contexts = new TowerSelectItemContext[itemRawData.Length];
for (int i = 0; i < itemRawData.Length; i++)
{
contexts[i] = BuildItemContext(itemRawData[i]);
}
return contexts;
}
private static TowerSelectItemContext BuildItemContext(TowerSelectItemRawData rawData)
{
if (rawData == null)
{
return null;
}
return new TowerSelectItemContext
{
Icon = rawData.Icon,
PriceText = BuildPriceText(rawData.Price, rawData.IsGain),
IsVisible = rawData.IsVisible,
IsInteractable = rawData.IsInteractable,
ActionType = rawData.ActionType,
ActionIndex = rawData.ActionIndex
};
}
private static string BuildPriceText(int price, bool isGain)
{
int positivePrice = Mathf.Max(0, price);
if (isGain)
{
return $"+{positivePrice}";
}
return positivePrice.ToString();
}
private void OnSelectItemClicked(object sender, GameEventArgs e)
{
if (!(e is CombatSelectItemClickEventArgs args))
{
return;
}
if (_useCase == null)
{
return;
}
if (Form != null && sender is Component component)
{
CombatSelectForm ownerForm = component.GetComponentInParent<CombatSelectForm>();
if (ownerForm != Form)
{
return;
}
}
_useCase.TryExecuteAction(args.ActionType, args.ActionIndex);
RefreshFromUseCase();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 03cc7fd540e32b34bad5b8786717725f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,12 @@
using GeometryTD.Definition;
namespace GeometryTD.UI
{
public class CombatFinishFormRawData
{
public int DefeatedEnemyCount;
public int GainedGold;
public BackpackInventoryData RewardInventory;
public bool CanReturn;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6ee396100a8f4c7f99e3978c7154c00b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
namespace GeometryTD.UI
{
public enum CombatSelectClickObjectType : byte
{
None = 0,
Foundation = 1,
Tower = 2
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d7dfced55a9048eea7df3b269e55e81e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
namespace GeometryTD.UI
{
public enum CombatSelectDisplayMode : byte
{
Hidden = 0,
Build = 1,
Func = 2
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cc38eaebc2c34d9428b1233aa57c653e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
using UnityEngine;
namespace GeometryTD.UI
{
public class CombatSelectFormRawData
{
public bool IsVisible;
public Vector2 ContentPosition;
public CombatSelectDisplayMode DisplayMode;
public TowerSelectItemRawData[] BuildItems;
public TowerSelectItemRawData UpgradeItem;
public TowerSelectItemRawData DestroyItem;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0958de13e46a34742acfa4352c7b6d4e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,15 @@
using UnityEngine;
namespace GeometryTD.UI
{
public class CombatSelectFormUserData
{
public CombatSelectClickObjectType ClickObjectType;
public Vector2 ContentPosition;
public Vector3 WorldPosition;
public Vector3Int CellPosition;
public int TowerEntityId;
public int UpgradeCost;
public int DestroyGain;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 53ae7da2d92244c893cea60fa4c05951
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,15 @@
using UnityEngine;
namespace GeometryTD.UI
{
public class TowerSelectItemRawData
{
public Sprite Icon;
public int Price;
public bool IsGain;
public bool IsVisible;
public bool IsInteractable;
public CombatSelectActionType ActionType;
public int ActionIndex;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 583bd0c79840d6649a4f340af2a8bf5b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,73 @@
using GeometryTD.CustomComponent;
using GeometryTD.Definition;
using UnityEngine;
namespace GeometryTD.UI
{
public class CombatFinishFormUseCase : IUIUseCase
{
private readonly RepoFormUseCase _repoFormUseCase = new RepoFormUseCase();
private CombatScheduler _combatScheduler;
private int _defeatedEnemyCount;
private int _gainedGold;
private BackpackInventoryData _rewardInventory;
private bool _isSummaryPrepared;
public CombatFinishFormRawData CreateInitialModel()
{
return BuildModel();
}
public CombatFinishFormRawData TryRefresh()
{
return BuildModel();
}
public void SetSummary(int defeatedEnemyCount, int gainedGold, BackpackInventoryData rewardInventory = null)
{
_defeatedEnemyCount = Mathf.Max(0, defeatedEnemyCount);
_gainedGold = Mathf.Max(0, gainedGold);
_rewardInventory = rewardInventory;
_isSummaryPrepared = true;
}
public CombatFinishFormUseCase(CombatScheduler combatScheduler)
{
this._combatScheduler = combatScheduler;
}
public bool TryReturnToMenu()
{
return _combatScheduler.OnCombatFinishReturnRequested();
}
private CombatFinishFormRawData BuildModel()
{
if (!_isSummaryPrepared)
{
_defeatedEnemyCount = GameEntry.CombatNode != null
? Mathf.Max(0, GameEntry.CombatNode.LastDefeatedEnemyCount)
: 0;
_gainedGold = GameEntry.CombatNode != null
? Mathf.Max(0, GameEntry.CombatNode.LastGainedGold)
: 0;
_rewardInventory = null;
}
BackpackInventoryData rewardInventory = _rewardInventory;
if (rewardInventory == null)
{
RepoFormRawData repoRawData = _repoFormUseCase.CreateInitialModel();
rewardInventory = repoRawData != null ? repoRawData.Inventory : null;
}
return new CombatFinishFormRawData
{
DefeatedEnemyCount = _defeatedEnemyCount,
GainedGold = _gainedGold,
RewardInventory = rewardInventory,
CanReturn = true
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9e5ee9e49e70480c9f98e6c77eb86f5f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,300 @@
using System;
using UnityEngine;
namespace GeometryTD.UI
{
public class CombatSelectFormUseCase : IUIUseCase
{
private const int BuildOptionCount = 4;
private readonly Option[] _buildOptions = new Option[BuildOptionCount];
private readonly Option _upgradeOption = new Option(CombatSelectActionType.UpgradeTower, 0, true);
private readonly Option _destroyOption = new Option(CombatSelectActionType.DestroyTower, 0, false);
private Func<int> _coinProvider;
private bool _isVisible;
private CombatSelectDisplayMode _displayMode = CombatSelectDisplayMode.Hidden;
private Vector2 _contentPosition;
public CombatSelectFormUseCase()
{
_coinProvider = DefaultCoinProvider;
for (int i = 0; i < BuildOptionCount; i++)
{
_buildOptions[i] = new Option(CombatSelectActionType.BuildTower, i, true);
}
}
public CombatSelectFormRawData CreateInitialModel()
{
return BuildModel();
}
public CombatSelectFormRawData TryRefresh()
{
return BuildModel();
}
public void SetCoinProvider(Func<int> coinProvider)
{
_coinProvider = coinProvider ?? DefaultCoinProvider;
}
public void SetBuildAction(int buildIndex, Func<bool> buildAction, int cost, Sprite icon = null)
{
if (buildIndex < 0 || buildIndex >= _buildOptions.Length)
{
return;
}
Option option = _buildOptions[buildIndex];
option.Action = buildAction;
option.Price = Mathf.Max(0, cost);
option.Icon = icon;
option.IsGain = false;
option.IsVisible = true;
}
public void SetBuildAction(int buildIndex, Action buildAction, int cost, Sprite icon = null)
{
if (buildAction == null)
{
SetBuildAction(buildIndex, (Func<bool>)null, cost, icon);
return;
}
SetBuildAction(
buildIndex,
() =>
{
buildAction.Invoke();
return true;
},
cost,
icon);
}
public void SetUpgradeAction(Func<bool> upgradeAction, int cost, Sprite icon = null)
{
_upgradeOption.Action = upgradeAction;
_upgradeOption.Price = Mathf.Max(0, cost);
_upgradeOption.Icon = icon;
_upgradeOption.IsGain = false;
_upgradeOption.IsVisible = true;
}
public void SetUpgradeAction(Action upgradeAction, int cost, Sprite icon = null)
{
if (upgradeAction == null)
{
SetUpgradeAction((Func<bool>)null, cost, icon);
return;
}
SetUpgradeAction(
() =>
{
upgradeAction.Invoke();
return true;
},
cost,
icon);
}
public void SetDestroyAction(Func<bool> destroyAction, int gain, Sprite icon = null)
{
_destroyOption.Action = destroyAction;
_destroyOption.Price = Mathf.Max(0, gain);
_destroyOption.Icon = icon;
_destroyOption.IsGain = true;
_destroyOption.IsVisible = true;
}
public void SetDestroyAction(Action destroyAction, int gain, Sprite icon = null)
{
if (destroyAction == null)
{
SetDestroyAction((Func<bool>)null, gain, icon);
return;
}
SetDestroyAction(
() =>
{
destroyAction.Invoke();
return true;
},
gain,
icon);
}
public void SetUpgradePrice(int cost)
{
_upgradeOption.Price = Mathf.Max(0, cost);
}
public void SetDestroyGain(int gain)
{
_destroyOption.Price = Mathf.Max(0, gain);
_destroyOption.IsGain = true;
}
public void ShowForFoundation(Vector2 contentPosition)
{
_contentPosition = contentPosition;
_displayMode = CombatSelectDisplayMode.Build;
_isVisible = true;
}
public void ShowForTower(Vector2 contentPosition)
{
_contentPosition = contentPosition;
_displayMode = CombatSelectDisplayMode.Func;
_isVisible = true;
}
public void Hide()
{
_isVisible = false;
_displayMode = CombatSelectDisplayMode.Hidden;
}
public bool TryExecuteAction(CombatSelectActionType actionType, int actionIndex)
{
if (!_isVisible)
{
return false;
}
Option option = GetOption(actionType, actionIndex);
if (option == null || !option.IsVisible)
{
return false;
}
int currentCoin = Mathf.Max(0, _coinProvider != null ? _coinProvider.Invoke() : 0);
if (!CanExecute(option, currentCoin))
{
return false;
}
if (option.Action == null)
{
return false;
}
bool result = option.Action.Invoke();
if (result)
{
Hide();
}
return result;
}
private CombatSelectFormRawData BuildModel()
{
int currentCoin = Mathf.Max(0, _coinProvider != null ? _coinProvider.Invoke() : 0);
TowerSelectItemRawData[] buildItems = new TowerSelectItemRawData[_buildOptions.Length];
for (int i = 0; i < _buildOptions.Length; i++)
{
buildItems[i] = BuildOptionRawData(_buildOptions[i], currentCoin);
}
return new CombatSelectFormRawData
{
IsVisible = _isVisible,
ContentPosition = _contentPosition,
DisplayMode = _displayMode,
BuildItems = buildItems,
UpgradeItem = BuildOptionRawData(_upgradeOption, currentCoin),
DestroyItem = BuildOptionRawData(_destroyOption, currentCoin)
};
}
private Option GetOption(CombatSelectActionType actionType, int actionIndex)
{
switch (actionType)
{
case CombatSelectActionType.BuildTower:
if (actionIndex < 0 || actionIndex >= _buildOptions.Length)
{
return null;
}
return _buildOptions[actionIndex];
case CombatSelectActionType.UpgradeTower:
return _upgradeOption;
case CombatSelectActionType.DestroyTower:
return _destroyOption;
default:
return null;
}
}
private static int DefaultCoinProvider()
{
if (GameEntry.CombatNode == null)
{
return 0;
}
return Mathf.Max(0, GameEntry.CombatNode.CurrentCoin);
}
private static bool CanExecute(Option option, int currentCoin)
{
if (option == null)
{
return false;
}
if (option.RequiresAffordable && currentCoin < option.Price)
{
return false;
}
return true;
}
private static TowerSelectItemRawData BuildOptionRawData(Option option, int currentCoin)
{
if (option == null)
{
return null;
}
return new TowerSelectItemRawData
{
Icon = option.Icon,
Price = option.Price,
IsGain = option.IsGain,
IsVisible = option.IsVisible,
IsInteractable = option.Action != null && CanExecute(option, currentCoin),
ActionType = option.ActionType,
ActionIndex = option.ActionIndex
};
}
private sealed class Option
{
public readonly CombatSelectActionType ActionType;
public readonly int ActionIndex;
public readonly bool RequiresAffordable;
public int Price;
public bool IsGain;
public bool IsVisible = true;
public Sprite Icon;
public Func<bool> Action;
public Option(CombatSelectActionType actionType, int actionIndex, bool requiresAffordable)
{
ActionType = actionType;
ActionIndex = actionIndex;
RequiresAffordable = requiresAffordable;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 040ca52500c562c4d9737d44a8940050
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,173 @@
using System.Collections.Generic;
using GameFramework.ObjectPool;
using GeometryTD.CustomEvent;
using GeometryTD.PoolObjectBase;
using TMPro;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public class CombatFinishForm : UGuiForm
{
[SerializeField] private TMP_Text _enemyKilledText;
[SerializeField] private TMP_Text _goldGainedText;
[SerializeField] private Transform _itemGainedParent;
[SerializeField] private RepoItem _itemTemplate;
[SerializeField] private int _instancePoolCapacity = 16;
private readonly List<RepoItem> _activeItems = new List<RepoItem>();
private IObjectPool<RepoItemObject> _itemPool;
private string _poolName;
private CombatFinishFormContext _context;
public void RefreshUI(CombatFinishFormContext context)
{
_context = context;
if (_enemyKilledText != null)
{
_enemyKilledText.text = context?.EnemyKilledText ?? "0";
}
if (_goldGainedText != null)
{
_goldGainedText.text = context?.GoldGainedText ?? "0";
}
RefreshRewardItems(context?.RewardItems);
}
public void OnReturnButtonClick()
{
if (_context != null && !_context.CanReturn)
{
return;
}
GameEntry.Event.Fire(this, CombatFinishReturnEventArgs.Create());
}
protected override void OnOpen(object userData)
{
base.OnOpen(userData);
if (userData is CombatFinishFormContext context)
{
RefreshUI(context);
return;
}
Log.Warning("CombatFinishForm requires CombatFinishFormContext as userData.");
}
protected override void OnClose(bool isShutdown, object userData)
{
_context = null;
if (_enemyKilledText != null)
{
_enemyKilledText.text = string.Empty;
}
if (_goldGainedText != null)
{
_goldGainedText.text = string.Empty;
}
ClearRewardItems();
base.OnClose(isShutdown, userData);
}
private void OnDestroy()
{
ClearRewardItems();
_itemPool?.ReleaseAllUnused();
}
private void EnsurePool()
{
if (_itemPool != null)
{
return;
}
_poolName = $"CombatFinishRepoItemPool_{GetInstanceID()}";
_itemPool = GameEntry.ObjectPool.CreateSingleSpawnObjectPool<RepoItemObject>(_poolName, _instancePoolCapacity);
}
private void RefreshRewardItems(RepoItemContext[] rewardItems)
{
ClearRewardItems();
if (rewardItems == null || rewardItems.Length <= 0)
{
return;
}
EnsurePool();
for (int i = 0; i < rewardItems.Length; i++)
{
RepoItem item = CreateOrSpawnItem();
if (item == null)
{
continue;
}
item.gameObject.SetActive(true);
item.OnInit(rewardItems[i]);
_activeItems.Add(item);
}
}
private void ClearRewardItems()
{
for (int i = _activeItems.Count - 1; i >= 0; i--)
{
RepoItem item = _activeItems[i];
if (item == null)
{
continue;
}
item.OnReset();
item.gameObject.SetActive(false);
_itemPool?.Unspawn(item);
}
_activeItems.Clear();
}
private RepoItem CreateOrSpawnItem()
{
if (_itemGainedParent == null)
{
Log.Warning("CombatFinishForm requires an item gained parent.");
return null;
}
if (_itemPool == null)
{
return null;
}
RepoItemObject itemObject = _itemPool.Spawn();
RepoItem item = itemObject != null ? (RepoItem)itemObject.Target : null;
if (item == null)
{
if (_itemTemplate == null)
{
Log.Warning("CombatFinishForm requires an item template.");
return null;
}
item = Instantiate(_itemTemplate);
_itemPool.Register(RepoItemObject.Create(item), true);
}
item.transform.SetParent(_itemGainedParent, false);
return item;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9679d912cb0ce9e4d95db41021d94463
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,60 @@
using UnityEngine;
namespace GeometryTD.UI
{
public class CombatSelectBuildArea : MonoBehaviour
{
[SerializeField] private TowerSelectItem[] _towerBuildItems;
public void OnInit(TowerSelectItemContext[] itemContexts)
{
if (_towerBuildItems == null)
{
return;
}
for (int i = 0; i < _towerBuildItems.Length; i++)
{
TowerSelectItem item = _towerBuildItems[i];
if (item == null)
{
continue;
}
TowerSelectItemContext itemContext =
itemContexts != null && i < itemContexts.Length ? itemContexts[i] : null;
bool isVisible = itemContext != null && itemContext.IsVisible;
item.gameObject.SetActive(isVisible);
if (isVisible)
{
item.OnInit(itemContext);
}
else
{
item.OnReset();
}
}
}
public void OnReset()
{
if (_towerBuildItems == null)
{
return;
}
for (int i = 0; i < _towerBuildItems.Length; i++)
{
TowerSelectItem item = _towerBuildItems[i];
if (item == null)
{
continue;
}
item.OnReset();
item.gameObject.SetActive(false);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 73470d4b91d03924b9233b58311e917c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,111 @@
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public class CombatSelectForm : UGuiForm
{
[SerializeField] private RectTransform _content;
[SerializeField] private CombatSelectBuildArea _buildArea;
[SerializeField] private CombatSelectFuncArea _funcArea;
private CombatSelectFormContext _context;
public void RefreshUI(CombatSelectFormContext context)
{
_context = context;
EnsureContentReference();
if (_content == null)
{
Log.Warning("CombatSelectForm content is missing.");
return;
}
bool isVisible = context != null && context.IsVisible;
_content.gameObject.SetActive(isVisible);
if (!isVisible)
{
_buildArea?.OnReset();
_funcArea?.OnReset();
return;
}
_content.anchoredPosition = context.ContentPosition;
if (_buildArea != null)
{
bool showBuildArea = context.ShowBuildArea;
_buildArea.gameObject.SetActive(showBuildArea);
if (showBuildArea)
{
_buildArea.OnInit(context.BuildItems);
}
else
{
_buildArea.OnReset();
}
}
if (_funcArea != null)
{
bool showFuncArea = context.ShowFuncArea;
_funcArea.gameObject.SetActive(showFuncArea);
if (showFuncArea)
{
_funcArea.OnInit(context.UpgradeItem, context.DestroyItem);
}
else
{
_funcArea.OnReset();
}
}
}
protected override void OnOpen(object userData)
{
base.OnOpen(userData);
if (userData is CombatSelectFormContext context)
{
RefreshUI(context);
return;
}
Log.Warning("CombatSelectForm requires CombatSelectFormContext as userData.");
RefreshUI(null);
}
protected override void OnClose(bool isShutdown, object userData)
{
_context = null;
_buildArea?.OnReset();
_funcArea?.OnReset();
if (_content != null)
{
_content.gameObject.SetActive(false);
}
base.OnClose(isShutdown, userData);
}
private void Awake()
{
EnsureContentReference();
}
private void EnsureContentReference()
{
if (_content != null)
{
return;
}
Transform contentTransform = transform.Find("Content");
_content = contentTransform as RectTransform;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e62b0f7fcf3ccfd4e88da34d80cc14ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,53 @@
using UnityEngine;
namespace GeometryTD.UI
{
public class CombatSelectFuncArea : MonoBehaviour
{
[SerializeField] private TowerSelectItem _upgradeItem;
[SerializeField] private TowerSelectItem _destroyItem;
public void OnInit(TowerSelectItemContext upgradeItemContext, TowerSelectItemContext destroyItemContext)
{
RefreshItem(_upgradeItem, upgradeItemContext);
RefreshItem(_destroyItem, destroyItemContext);
}
public void OnReset()
{
ResetItem(_upgradeItem);
ResetItem(_destroyItem);
}
private static void RefreshItem(TowerSelectItem item, TowerSelectItemContext context)
{
if (item == null)
{
return;
}
bool isVisible = context != null && context.IsVisible;
item.gameObject.SetActive(isVisible);
if (isVisible)
{
item.OnInit(context);
}
else
{
item.OnReset();
}
}
private static void ResetItem(TowerSelectItem item)
{
if (item == null)
{
return;
}
item.OnReset();
item.gameObject.SetActive(false);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 891304d1e879da34c9b989ca2fcd14a7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,96 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using GeometryTD.CustomEvent;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public class TowerSelectItem : MonoBehaviour
{
[SerializeField] private Image _icon;
[SerializeField] private TMP_Text _price;
[SerializeField] private CommonButton _button;
private TowerSelectItemContext _context;
private Sprite _defaultIcon;
private bool _hasCachedDefaultIcon;
public void OnInit(TowerSelectItemContext context)
{
CacheDefaultIcon();
_context = context;
if (_icon != null)
{
if (context != null && context.Icon != null)
{
_icon.sprite = context.Icon;
}
else if (_defaultIcon != null)
{
_icon.sprite = _defaultIcon;
}
_icon.enabled = _icon.sprite != null;
}
if (_price != null)
{
_price.text = context?.PriceText ?? string.Empty;
}
if (_button != null)
{
_button.Interactive = context != null && context.IsInteractable;
}
}
public void OnReset()
{
_context = null;
CacheDefaultIcon();
if (_icon != null)
{
_icon.sprite = _defaultIcon;
_icon.enabled = _icon.sprite != null;
}
if (_price != null)
{
_price.text = string.Empty;
}
if (_button != null)
{
_button.Interactive = false;
}
}
public void OnClick()
{
if (_context == null || !_context.IsInteractable)
{
return;
}
GameEntry.Event.Fire(
this,
CombatSelectItemClickEventArgs.Create(_context.ActionType, _context.ActionIndex));
}
private void CacheDefaultIcon()
{
if (_hasCachedDefaultIcon || _icon == null)
{
return;
}
_defaultIcon = _icon.sprite;
_hasCachedDefaultIcon = true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4da5ef45ee18db44ebe3c015dddb61d6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: