convert game playing..
This commit is contained in:
@@ -0,0 +1,828 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BumpCombat.Constants;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Progression;
|
||||
using BumpCombat.Enemies;
|
||||
using BumpCombat.Player;
|
||||
using BumpCombat.Spawning;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BumpCombat.Defense
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class DefensePrototypeController : MonoBehaviour
|
||||
{
|
||||
private const int LaneCount = 2;
|
||||
|
||||
private static Sprite whiteSprite;
|
||||
|
||||
private readonly Dictionary<EnemyController, DefenseRouteAgent> activeAgents = new();
|
||||
private readonly DefenseTower[] towers = new DefenseTower[LaneCount];
|
||||
private readonly bool[] padsBuilt = new bool[LaneCount];
|
||||
private readonly int[] suppliedPerLane = new int[LaneCount];
|
||||
private readonly DefenseWaveLedger[] portalLedgers = new DefenseWaveLedger[LaneCount];
|
||||
private readonly bool[] portalSupplyEnded = new bool[LaneCount];
|
||||
private readonly bool[] portalRewardVisible = new bool[LaneCount];
|
||||
private readonly bool[] portalRewardObjectCreated = new bool[LaneCount];
|
||||
private readonly GameObject[] portalRewardObjects = new GameObject[LaneCount];
|
||||
private readonly GameObject[] portalRewardLabels = new GameObject[LaneCount];
|
||||
private int baseHealth;
|
||||
private float nextSupplyTime;
|
||||
private int nextSupplyLane;
|
||||
private bool active;
|
||||
private bool playerDead;
|
||||
private bool failed;
|
||||
private int constructionCurrency;
|
||||
private int pendingPassiveRelicChoices;
|
||||
private float reviveRemaining;
|
||||
private GameObject worldRoot;
|
||||
private Text hudText;
|
||||
private RunManager runManager;
|
||||
private SpawnDirector spawnDirector;
|
||||
private PlayerHealth playerHealth;
|
||||
private PlayerController playerController;
|
||||
private DefenseConstants tuning;
|
||||
|
||||
public static DefensePrototypeController Instance { get; private set; }
|
||||
public static Sprite WhiteSprite
|
||||
{
|
||||
get
|
||||
{
|
||||
if (whiteSprite == null)
|
||||
{
|
||||
whiteSprite = Sprite.Create(
|
||||
Texture2D.whiteTexture,
|
||||
new Rect(0f, 0f, 1f, 1f),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
1f);
|
||||
whiteSprite.name = "Defense Prototype White Primitive";
|
||||
}
|
||||
|
||||
return whiteSprite;
|
||||
}
|
||||
}
|
||||
|
||||
public DefenseConstants Tuning => tuning ?? GameplayConstants.Current.Defense;
|
||||
public bool IsActive => active;
|
||||
public bool PlayerIsAlive => playerHealth != null && playerHealth.CurrentHealth > 0f;
|
||||
public bool CanOperateTowers => DefensePrototypeRules.CanOperateTowers(
|
||||
PlayerIsAlive,
|
||||
active && !failed && runManager != null
|
||||
&& !runManager.IsGameOver
|
||||
&& !runManager.IsRunCleared
|
||||
&& !runManager.IsPaused
|
||||
&& !runManager.IsSelectionOpen
|
||||
&& !runManager.IsTitleScreen);
|
||||
public int ConstructionCurrency => constructionCurrency;
|
||||
public int GetBaseHealth(int lane) => lane >= 0 && lane < LaneCount ? baseHealth : 0;
|
||||
public int GetPortalRemainingSupply(int lane)
|
||||
{
|
||||
if (tuning == null || lane < 0 || lane >= LaneCount)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Mathf.Max(
|
||||
0,
|
||||
tuning.PortalEnemyCount - (portalLedgers[lane]?.SuppliedCount ?? 0));
|
||||
}
|
||||
|
||||
public int RemainingPortalSupply
|
||||
{
|
||||
get
|
||||
{
|
||||
if (tuning == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int remaining = 0;
|
||||
for (int lane = 0; lane < LaneCount; lane++)
|
||||
{
|
||||
remaining += Mathf.Max(
|
||||
0,
|
||||
tuning.PortalEnemyCount - (portalLedgers[lane]?.SuppliedCount ?? 0));
|
||||
}
|
||||
|
||||
return remaining;
|
||||
}
|
||||
}
|
||||
public bool IsRewardVisible
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int lane = 0; lane < LaneCount; lane++)
|
||||
{
|
||||
if (portalRewardVisible[lane])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public float PlayerReviveRemaining => playerDead ? reviveRemaining : 0f;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
runManager = RunManager.Instance;
|
||||
if (runManager != null)
|
||||
{
|
||||
runManager.OnRunStarted += HandleRunStarted;
|
||||
runManager.OnGameOver += HandleRunEnded;
|
||||
runManager.OnRunCleared += HandleRunEnded;
|
||||
}
|
||||
|
||||
if (runManager != null
|
||||
&& runManager.IsDefensePrototypeRun
|
||||
&& !runManager.IsTitleScreen)
|
||||
{
|
||||
Activate();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (runManager != null)
|
||||
{
|
||||
runManager.OnRunStarted -= HandleRunStarted;
|
||||
runManager.OnGameOver -= HandleRunEnded;
|
||||
runManager.OnRunCleared -= HandleRunEnded;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<EnemyController, DefenseRouteAgent> pair in activeAgents)
|
||||
{
|
||||
if (pair.Key != null)
|
||||
{
|
||||
pair.Key.OnDied -= HandleEnemyDied;
|
||||
}
|
||||
}
|
||||
|
||||
if (Instance == this)
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!active || runManager == null || failed || runManager.IsGameOver || runManager.IsRunCleared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (runManager.IsPaused || runManager.IsSelectionOpen || runManager.IsTitleScreen)
|
||||
{
|
||||
UpdateHud();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdatePlayerRecovery();
|
||||
UpdatePortalSupply();
|
||||
TryOfferRewards();
|
||||
TryCollectRewards();
|
||||
UpdateHud();
|
||||
|
||||
if (Keyboard.current != null && CanOperateTowers)
|
||||
{
|
||||
if (Keyboard.current.digit1Key.wasPressedThisFrame
|
||||
|| Keyboard.current.numpad1Key.wasPressedThisFrame)
|
||||
{
|
||||
TryBuildNearestTower(DefenseTowerKind.Direct);
|
||||
}
|
||||
else if (Keyboard.current.digit2Key.wasPressedThisFrame
|
||||
|| Keyboard.current.numpad2Key.wasPressedThisFrame)
|
||||
{
|
||||
TryBuildNearestTower(DefenseTowerKind.Summon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Activate()
|
||||
{
|
||||
if (active || runManager == null || !runManager.IsDefensePrototypeRun)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tuning = GameplayConstants.Current.Defense;
|
||||
spawnDirector = FindAnyObjectByType<SpawnDirector>();
|
||||
playerHealth = FindAnyObjectByType<PlayerHealth>();
|
||||
playerController = FindAnyObjectByType<PlayerController>();
|
||||
if (playerController != null)
|
||||
{
|
||||
PassiveRelicController relics = playerController.GetComponent<PassiveRelicController>();
|
||||
if (relics == null)
|
||||
{
|
||||
relics = playerController.gameObject.AddComponent<PassiveRelicController>();
|
||||
}
|
||||
|
||||
relics.BeginDefenseRun();
|
||||
}
|
||||
ArenaBounds bounds = ArenaBounds.Resolve();
|
||||
bounds?.SetZoneBounds(tuning.ArenaCenter, tuning.ArenaPlayableHalfExtents);
|
||||
FindAnyObjectByType<ArenaCameraFollow>()?.ResetCenter(tuning.ArenaCenter);
|
||||
active = true;
|
||||
failed = false;
|
||||
playerDead = false;
|
||||
constructionCurrency = tuning.InitialConstructionCurrency;
|
||||
pendingPassiveRelicChoices = 0;
|
||||
baseHealth = tuning.BaseMaximumHealth;
|
||||
activeAgents.Clear();
|
||||
for (int lane = 0; lane < LaneCount; lane++)
|
||||
{
|
||||
suppliedPerLane[lane] = 0;
|
||||
portalLedgers[lane] = new DefenseWaveLedger(tuning.PortalEnemyCount);
|
||||
portalSupplyEnded[lane] = false;
|
||||
portalRewardVisible[lane] = false;
|
||||
portalRewardObjectCreated[lane] = false;
|
||||
portalRewardObjects[lane] = null;
|
||||
portalRewardLabels[lane] = null;
|
||||
padsBuilt[lane] = false;
|
||||
towers[lane] = null;
|
||||
}
|
||||
|
||||
nextSupplyLane = 0;
|
||||
nextSupplyTime = Time.time + 0.5f;
|
||||
CreateWorldVisuals();
|
||||
CreateHud();
|
||||
UpdateHud();
|
||||
}
|
||||
|
||||
public bool TryBuildTowerAtPad(int padIndex)
|
||||
{
|
||||
return TryBuildTowerAtPad(
|
||||
padIndex,
|
||||
padIndex == 0 ? DefenseTowerKind.Direct : DefenseTowerKind.Summon);
|
||||
}
|
||||
|
||||
public bool TryBuildTowerAtPad(int padIndex, DefenseTowerKind kind)
|
||||
{
|
||||
if (!CanOperateTowers
|
||||
|| padIndex < 0
|
||||
|| padIndex >= LaneCount
|
||||
|| padsBuilt[padIndex]
|
||||
|| playerController == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector2 position = tuning.GetBuildPadPosition(padIndex);
|
||||
if (((Vector2)playerController.transform.position - position).sqrMagnitude
|
||||
> tuning.BuildInteractionRadius * tuning.BuildInteractionRadius)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int cost = kind == DefenseTowerKind.Direct
|
||||
? tuning.DirectTowerCost
|
||||
: tuning.SummonTowerCost;
|
||||
if (constructionCurrency < cost)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
GameObject towerObject = new(
|
||||
kind == DefenseTowerKind.Direct ? "Direct Damage Tower" : "Summon Tower");
|
||||
towerObject.transform.SetParent(worldRoot.transform, false);
|
||||
towerObject.transform.position = position;
|
||||
DefenseTower tower = towerObject.AddComponent<DefenseTower>();
|
||||
tower.Configure(this, kind, padIndex, tuning);
|
||||
towers[padIndex] = tower;
|
||||
padsBuilt[padIndex] = true;
|
||||
constructionCurrency -= cost;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryBuildNearestTower(DefenseTowerKind kind)
|
||||
{
|
||||
if (playerController == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int nearestPad = -1;
|
||||
float nearestDistance = tuning.BuildInteractionRadius * tuning.BuildInteractionRadius;
|
||||
for (int pad = 0; pad < LaneCount; pad++)
|
||||
{
|
||||
if (padsBuilt[pad])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float distance = ((Vector2)playerController.transform.position
|
||||
- tuning.GetBuildPadPosition(pad)).sqrMagnitude;
|
||||
if (distance <= nearestDistance)
|
||||
{
|
||||
nearestDistance = distance;
|
||||
nearestPad = pad;
|
||||
}
|
||||
}
|
||||
|
||||
return nearestPad >= 0 && TryBuildTowerAtPad(nearestPad, kind);
|
||||
}
|
||||
|
||||
public bool TryAttackFromTower(DefenseTower tower)
|
||||
{
|
||||
if (!CanOperateTowers || tower == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DefenseRouteAgent target = FindNearestTarget(
|
||||
tower.transform.position,
|
||||
tuning.TowerRange,
|
||||
tower.Kind);
|
||||
return tower.TryAttack(target);
|
||||
}
|
||||
|
||||
public DefenseRouteAgent FindNearestTarget(
|
||||
Vector2 sourcePosition,
|
||||
float range,
|
||||
DefenseTowerKind sourceKind)
|
||||
{
|
||||
DefenseRouteAgent nearest = null;
|
||||
float nearestDistance = range * range;
|
||||
foreach (DefenseRouteAgent agent in activeAgents.Values)
|
||||
{
|
||||
if (agent == null
|
||||
|| agent.HasArrived
|
||||
|| agent.Enemy == null
|
||||
|| agent.Enemy.IsDead
|
||||
|| DefensePrototypeRules.IsTowerImmune(
|
||||
sourceKind,
|
||||
agent.ImmuneTowerKind))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float distance = ((Vector2)agent.transform.position - sourcePosition).sqrMagnitude;
|
||||
if (distance <= nearestDistance)
|
||||
{
|
||||
nearestDistance = distance;
|
||||
nearest = agent;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
public bool TryApplyTowerDamage(
|
||||
DefenseTower tower,
|
||||
DefenseRouteAgent target,
|
||||
float damage)
|
||||
{
|
||||
if (!CanOperateTowers || tower == null || target == null || playerHealth == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float distanceToPlayer = Vector2.Distance(
|
||||
playerHealth.transform.position,
|
||||
tower.transform.position);
|
||||
return target.TryTakeTowerDamage(
|
||||
tower.Kind,
|
||||
damage,
|
||||
distanceToPlayer,
|
||||
tuning.PlayerSightRadius,
|
||||
tuning.OutsideSightDamageEfficiency,
|
||||
tower.transform.position);
|
||||
}
|
||||
|
||||
public bool HandlePlayerDied()
|
||||
{
|
||||
if (!active || failed || runManager == null || runManager.IsGameOver || runManager.IsRunCleared)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
playerDead = true;
|
||||
reviveRemaining = tuning.PlayerReviveDelaySeconds;
|
||||
playerController?.SetTransitionLocked(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleRunStarted()
|
||||
{
|
||||
if (runManager != null && runManager.IsDefensePrototypeRun)
|
||||
{
|
||||
Activate();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRunEnded()
|
||||
{
|
||||
failed = true;
|
||||
playerController?.GetComponent<PassiveRelicController>()?.EndDefenseRun();
|
||||
}
|
||||
|
||||
private void UpdatePlayerRecovery()
|
||||
{
|
||||
if (!playerDead)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
reviveRemaining = Mathf.Max(0f, reviveRemaining - Time.deltaTime);
|
||||
if (reviveRemaining > 0f || !PlayerCanBeRevived())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playerDead = false;
|
||||
playerHealth?.ReviveForDefense(tuning.PlayerReviveInvulnerabilitySeconds);
|
||||
}
|
||||
|
||||
private bool PlayerCanBeRevived()
|
||||
{
|
||||
return !DefensePrototypeRules.IsBaseDestroyed(baseHealth);
|
||||
}
|
||||
|
||||
private void UpdatePortalSupply()
|
||||
{
|
||||
if (spawnDirector == null || Time.time < nextSupplyTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < LaneCount; attempt++)
|
||||
{
|
||||
int lane = (nextSupplyLane + attempt) % LaneCount;
|
||||
if (suppliedPerLane[lane] >= tuning.PortalEnemyCount)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnemyController enemy = spawnDirector.SpawnDefenseSkeleton(
|
||||
tuning.GetPortalPosition(lane));
|
||||
if (enemy == null)
|
||||
{
|
||||
nextSupplyTime = Time.time + tuning.PortalSpawnInterval;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2[] route =
|
||||
{
|
||||
tuning.GetPortalPosition(lane),
|
||||
tuning.GetWaypointPosition(lane, 0),
|
||||
tuning.GetWaypointPosition(lane, 1),
|
||||
tuning.GetBasePosition(lane),
|
||||
};
|
||||
DefenseRouteAgent agent = enemy.gameObject.AddComponent<DefenseRouteAgent>();
|
||||
agent.Configure(
|
||||
enemy,
|
||||
lane,
|
||||
route,
|
||||
enemy.MoveSpeed * tuning.RouteSpeedMultiplier,
|
||||
Mathf.Max(0.1f, enemy.AttackRange + 0.35f),
|
||||
tuning.PostAttackProgressSeconds,
|
||||
tuning.GetPortalImmuneTowerKind(lane),
|
||||
HandleEnemyArrived);
|
||||
enemy.OnDied += HandleEnemyDied;
|
||||
activeAgents.Add(enemy, agent);
|
||||
portalLedgers[lane].RegisterSupply(enemy.SpawnLifetimeIdentity);
|
||||
suppliedPerLane[lane]++;
|
||||
if (suppliedPerLane[lane] >= tuning.PortalEnemyCount)
|
||||
{
|
||||
portalSupplyEnded[lane] = true;
|
||||
portalLedgers[lane].MarkSupplyEnded();
|
||||
}
|
||||
|
||||
nextSupplyLane = (lane + 1) % LaneCount;
|
||||
nextSupplyTime = Time.time + tuning.PortalSpawnInterval;
|
||||
return;
|
||||
}
|
||||
|
||||
for (int lane = 0; lane < LaneCount; lane++)
|
||||
{
|
||||
if (!portalSupplyEnded[lane]
|
||||
&& suppliedPerLane[lane] >= tuning.PortalEnemyCount)
|
||||
{
|
||||
portalSupplyEnded[lane] = true;
|
||||
portalLedgers[lane].MarkSupplyEnded();
|
||||
}
|
||||
}
|
||||
}
|
||||
private void HandleEnemyDied(EnemyController enemy)
|
||||
{
|
||||
if (enemy == null || !activeAgents.TryGetValue(enemy, out DefenseRouteAgent agent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
enemy.OnDied -= HandleEnemyDied;
|
||||
activeAgents.Remove(enemy);
|
||||
int lane = Mathf.Clamp(agent.Lane, 0, LaneCount - 1);
|
||||
portalLedgers[lane]?.TryResolve(
|
||||
enemy.SpawnLifetimeIdentity,
|
||||
DefenseEnemyResolution.Death);
|
||||
}
|
||||
|
||||
private void HandleEnemyArrived(DefenseRouteAgent agent)
|
||||
{
|
||||
if (agent == null
|
||||
|| agent.Enemy == null
|
||||
|| !activeAgents.ContainsKey(agent.Enemy))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyController enemy = agent.Enemy;
|
||||
enemy.OnDied -= HandleEnemyDied;
|
||||
activeAgents.Remove(enemy);
|
||||
int lane = Mathf.Clamp(agent.Lane, 0, LaneCount - 1);
|
||||
portalLedgers[lane]?.TryResolve(
|
||||
enemy.SpawnLifetimeIdentity,
|
||||
DefenseEnemyResolution.Arrival);
|
||||
baseHealth = Mathf.Max(0, baseHealth - tuning.ArrivalBaseDamage);
|
||||
enemy.RetireFromRun(includeProtected: true, suppressFeedback: true);
|
||||
if (DefensePrototypeRules.IsBaseDestroyed(baseHealth))
|
||||
{
|
||||
failed = true;
|
||||
runManager.EndRun();
|
||||
}
|
||||
}
|
||||
|
||||
private void TryOfferRewards()
|
||||
{
|
||||
for (int lane = 0; lane < LaneCount; lane++)
|
||||
{
|
||||
DefenseWaveLedger ledger = portalLedgers[lane];
|
||||
if (!portalSupplyEnded[lane]
|
||||
|| ledger == null
|
||||
|| portalRewardVisible[lane]
|
||||
|| CountActiveAgentsForLane(lane) > 0
|
||||
|| !ledger.TryOfferReward())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
portalRewardVisible[lane] = true;
|
||||
if (!portalRewardObjectCreated[lane])
|
||||
{
|
||||
portalRewardObjectCreated[lane] = true;
|
||||
Vector2 rewardPosition = tuning.GetPortalPosition(lane) + Vector2.up * 1.2f;
|
||||
portalRewardObjects[lane] = CreateShape(
|
||||
$"Construction Reward {lane + 1}",
|
||||
rewardPosition,
|
||||
new Vector2(0.85f, 0.85f),
|
||||
new Color32(255, 224, 84, 255),
|
||||
7);
|
||||
portalRewardLabels[lane] = CreateWorldLabel(
|
||||
$"Reward Label {lane + 1}",
|
||||
rewardPosition + Vector2.up * 0.65f,
|
||||
$"+{tuning.PortalRewardCurrency} BUILD");
|
||||
}
|
||||
|
||||
portalRewardObjects[lane].SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryCollectRewards()
|
||||
{
|
||||
if (playerController == null || playerHealth == null || !PlayerIsAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 playerPosition = playerController.transform.position;
|
||||
for (int lane = 0; lane < LaneCount; lane++)
|
||||
{
|
||||
if (!portalRewardVisible[lane]
|
||||
|| portalRewardObjects[lane] == null
|
||||
|| Vector2.Distance(
|
||||
playerPosition,
|
||||
portalRewardObjects[lane].transform.position)
|
||||
> tuning.RewardPickupRadius
|
||||
|| !portalLedgers[lane].TryCollectReward())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
portalRewardVisible[lane] = false;
|
||||
portalRewardObjects[lane].SetActive(false);
|
||||
portalRewardLabels[lane]?.SetActive(false);
|
||||
constructionCurrency += tuning.PortalRewardCurrency;
|
||||
PassiveRelicController relics = playerController.GetComponent<PassiveRelicController>();
|
||||
ArtifactRewardController rewards = FindAnyObjectByType<ArtifactRewardController>();
|
||||
if (relics != null && relics.AvailableCount > 0 && rewards != null)
|
||||
{
|
||||
pendingPassiveRelicChoices++;
|
||||
rewards.QueuePassiveRelicReward();
|
||||
}
|
||||
else if (relics == null || relics.AvailableCount == 0 || rewards == null)
|
||||
{
|
||||
constructionCurrency += tuning.PortalRewardCurrency;
|
||||
}
|
||||
}
|
||||
|
||||
TryFinishClear();
|
||||
}
|
||||
|
||||
public void ResolvePassiveRelicChoice()
|
||||
{
|
||||
pendingPassiveRelicChoices = Mathf.Max(0, pendingPassiveRelicChoices - 1);
|
||||
TryFinishClear();
|
||||
}
|
||||
|
||||
private void TryFinishClear()
|
||||
{
|
||||
if (failed || activeAgents.Count > 0 || pendingPassiveRelicChoices > 0 || runManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int lane = 0; lane < LaneCount; lane++)
|
||||
{
|
||||
if (!portalSupplyEnded[lane]
|
||||
|| portalLedgers[lane] == null
|
||||
|| !portalLedgers[lane].IsRewardCollected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DefensePrototypeRules.IsBaseDestroyed(baseHealth)
|
||||
&& !runManager.IsGameOver
|
||||
&& !runManager.IsRunCleared)
|
||||
{
|
||||
runManager.EndRunAsClear();
|
||||
}
|
||||
}
|
||||
|
||||
private int CountActiveAgentsForLane(int lane)
|
||||
{
|
||||
int count = 0;
|
||||
foreach (DefenseRouteAgent agent in activeAgents.Values)
|
||||
{
|
||||
if (agent != null && agent.Lane == lane)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
private void CreateWorldVisuals()
|
||||
{
|
||||
if (worldRoot != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
worldRoot = new GameObject("Defense Prototype World");
|
||||
worldRoot.transform.SetParent(transform, false);
|
||||
for (int lane = 0; lane < LaneCount; lane++)
|
||||
{
|
||||
Vector2 portal = tuning.GetPortalPosition(lane);
|
||||
Vector2 basePosition = tuning.GetBasePosition(lane);
|
||||
CreateLaneLine(lane, portal, tuning.GetWaypointPosition(lane, 0));
|
||||
CreateLaneLine(lane, tuning.GetWaypointPosition(lane, 0), tuning.GetWaypointPosition(lane, 1));
|
||||
CreateLaneLine(lane, tuning.GetWaypointPosition(lane, 1), basePosition);
|
||||
CreateShape(
|
||||
$"Portal {lane + 1}",
|
||||
portal,
|
||||
new Vector2(0.85f, 1.25f),
|
||||
new Color32(179, 84, 220, 255),
|
||||
5);
|
||||
CreateWorldLabel($"Portal {lane + 1} Label", portal + Vector2.right * 1.6f + Vector2.up * 0.9f, $"P{lane + 1} PORTAL\nIMMUNE {tuning.GetPortalImmuneTowerKind(lane).ToString().ToUpperInvariant()}");
|
||||
if (lane == 0)
|
||||
{
|
||||
CreateShape(
|
||||
"Shared Base",
|
||||
basePosition,
|
||||
new Vector2(1.15f, 2.1f),
|
||||
new Color32(226, 78, 87, 255),
|
||||
5);
|
||||
CreateWorldLabel("Shared Base Label", basePosition + Vector2.up * 1.4f, "BASE");
|
||||
}
|
||||
}
|
||||
|
||||
for (int pad = 0; pad < LaneCount; pad++)
|
||||
{
|
||||
Vector2 position = tuning.GetBuildPadPosition(pad);
|
||||
CreateShape(
|
||||
$"Build Pad {pad + 1}",
|
||||
position,
|
||||
new Vector2(1.35f, 1.35f),
|
||||
new Color32(76, 104, 125, 255),
|
||||
3);
|
||||
CreateWorldLabel(
|
||||
$"Build Pad {pad + 1} Label",
|
||||
position + Vector2.down * 0.95f,
|
||||
"PAD " + (pad + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateHud()
|
||||
{
|
||||
Canvas canvas = FindAnyObjectByType<Canvas>();
|
||||
if (canvas == null || hudText != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject hudObject = new(
|
||||
"Defense Prototype HUD",
|
||||
typeof(RectTransform),
|
||||
typeof(CanvasRenderer),
|
||||
typeof(Text));
|
||||
hudObject.transform.SetParent(canvas.transform, false);
|
||||
RectTransform rect = hudObject.GetComponent<RectTransform>();
|
||||
rect.anchorMin = new Vector2(0f, 1f);
|
||||
rect.anchorMax = new Vector2(0f, 1f);
|
||||
rect.pivot = new Vector2(0f, 1f);
|
||||
rect.anchoredPosition = new Vector2(28f, -452f);
|
||||
rect.sizeDelta = new Vector2(900f, 110f);
|
||||
hudText = hudObject.GetComponent<Text>();
|
||||
hudText.font = BumpCombat.UI.PresentationUiStyle.GetFont();
|
||||
hudText.fontSize = 24;
|
||||
hudText.color = new Color32(236, 240, 246, 255);
|
||||
hudText.alignment = TextAnchor.UpperLeft;
|
||||
hudText.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
hudText.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
}
|
||||
|
||||
private void UpdateHud()
|
||||
{
|
||||
if (hudText == null || tuning == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string status = playerDead
|
||||
? $"PLAYER DOWN revive {Mathf.CeilToInt(reviveRemaining)}s"
|
||||
: IsRewardVisible ? "REWARD READY move to portal" : $"1 DIRECT {tuning.DirectTowerCost} / 2 SUMMON {tuning.SummonTowerCost} near a pad";
|
||||
hudText.text =
|
||||
$"DEFENSE TEST BASE {baseHealth}/{tuning.BaseMaximumHealth}\n"
|
||||
+ $"BUILD {constructionCurrency} P1 WAIT {GetPortalRemainingSupply(0)} LIVE {CountActiveAgentsForLane(0)} P2 WAIT {GetPortalRemainingSupply(1)} LIVE {CountActiveAgentsForLane(1)}\n"
|
||||
+ status;
|
||||
}
|
||||
|
||||
private void CreateLaneLine(int lane, Vector2 from, Vector2 to)
|
||||
{
|
||||
GameObject lineObject = new($"Lane {lane + 1} Segment");
|
||||
lineObject.transform.SetParent(worldRoot.transform, false);
|
||||
LineRenderer line = lineObject.AddComponent<LineRenderer>();
|
||||
line.useWorldSpace = true;
|
||||
line.positionCount = 2;
|
||||
line.SetPosition(0, from);
|
||||
line.SetPosition(1, to);
|
||||
line.startWidth = 0.08f;
|
||||
line.endWidth = 0.08f;
|
||||
line.startColor = new Color32(108, 151, 177, 210);
|
||||
line.endColor = new Color32(108, 151, 177, 210);
|
||||
line.material = new Material(Shader.Find("Sprites/Default"));
|
||||
line.sortingOrder = 1;
|
||||
}
|
||||
|
||||
private GameObject CreateShape(
|
||||
string name,
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
Color color,
|
||||
int sortingOrder)
|
||||
{
|
||||
GameObject shape = new(name);
|
||||
shape.transform.SetParent(worldRoot.transform, false);
|
||||
shape.transform.position = position;
|
||||
shape.transform.localScale = new Vector3(size.x, size.y, 1f);
|
||||
SpriteRenderer renderer = shape.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = WhiteSprite;
|
||||
renderer.color = color;
|
||||
renderer.sortingOrder = sortingOrder;
|
||||
return shape;
|
||||
}
|
||||
|
||||
private GameObject CreateWorldLabel(string name, Vector2 position, string text)
|
||||
{
|
||||
GameObject labelObject = new(name);
|
||||
labelObject.transform.SetParent(worldRoot.transform, false);
|
||||
labelObject.transform.position = position;
|
||||
TextMesh label = labelObject.AddComponent<TextMesh>();
|
||||
label.text = text;
|
||||
label.anchor = TextAnchor.MiddleCenter;
|
||||
label.alignment = TextAlignment.Center;
|
||||
label.characterSize = 0.08f;
|
||||
label.fontSize = 32;
|
||||
label.color = Color.white;
|
||||
label.GetComponent<MeshRenderer>().sortingOrder = 12;
|
||||
return labelObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a39d0992fba741809dfd6e95bc9cfd83
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,126 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Defense
|
||||
{
|
||||
public enum DefenseTowerKind
|
||||
{
|
||||
Direct,
|
||||
Summon,
|
||||
}
|
||||
|
||||
public enum DefenseEnemyResolution
|
||||
{
|
||||
Death,
|
||||
Arrival,
|
||||
}
|
||||
|
||||
public static class DefensePrototypeRules
|
||||
{
|
||||
public static float GetTowerEfficiency(
|
||||
float distanceToPlayer,
|
||||
float sightRadius,
|
||||
float insideEfficiency,
|
||||
float outsideEfficiency)
|
||||
{
|
||||
return distanceToPlayer <= sightRadius
|
||||
? insideEfficiency
|
||||
: outsideEfficiency;
|
||||
}
|
||||
|
||||
public static bool IsTowerImmune(
|
||||
DefenseTowerKind sourceKind,
|
||||
DefenseTowerKind immuneKind)
|
||||
{
|
||||
return sourceKind == immuneKind;
|
||||
}
|
||||
|
||||
public static bool CanOperateTowers(bool playerAlive, bool runActive)
|
||||
{
|
||||
return playerAlive && runActive;
|
||||
}
|
||||
|
||||
public static bool IsBaseDestroyed(float currentHealth)
|
||||
{
|
||||
return currentHealth <= 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DefenseWaveLedger
|
||||
{
|
||||
private readonly int expectedSupplyCount;
|
||||
private readonly System.Collections.Generic.HashSet<int> suppliedIds = new();
|
||||
private readonly System.Collections.Generic.HashSet<int> resolvedIds = new();
|
||||
private bool supplyEnded;
|
||||
private bool rewardOffered;
|
||||
private bool rewardCollected;
|
||||
|
||||
public DefenseWaveLedger(int expectedSupplyCount)
|
||||
{
|
||||
this.expectedSupplyCount = Mathf.Max(0, expectedSupplyCount);
|
||||
}
|
||||
|
||||
public int SuppliedCount => suppliedIds.Count;
|
||||
public int ResolvedCount => resolvedIds.Count;
|
||||
public int KillEventCount { get; private set; }
|
||||
public bool IsSupplyEnded => supplyEnded;
|
||||
public bool IsRewardOffered => rewardOffered;
|
||||
public bool IsRewardCollected => rewardCollected;
|
||||
|
||||
public bool RegisterSupply(int enemyId)
|
||||
{
|
||||
if (supplyEnded
|
||||
|| SuppliedCount >= expectedSupplyCount
|
||||
|| enemyId < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return suppliedIds.Add(enemyId);
|
||||
}
|
||||
|
||||
public void MarkSupplyEnded()
|
||||
{
|
||||
supplyEnded = true;
|
||||
}
|
||||
|
||||
public bool TryResolve(int enemyId, DefenseEnemyResolution resolution)
|
||||
{
|
||||
if (!suppliedIds.Contains(enemyId) || !resolvedIds.Add(enemyId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resolution == DefenseEnemyResolution.Death)
|
||||
{
|
||||
KillEventCount++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryOfferReward()
|
||||
{
|
||||
if (rewardOffered
|
||||
|| !supplyEnded
|
||||
|| SuppliedCount < expectedSupplyCount
|
||||
|| ResolvedCount < SuppliedCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
rewardOffered = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryCollectReward()
|
||||
{
|
||||
if (!rewardOffered || rewardCollected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
rewardCollected = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9be2c059f93b466e9735447cb1bd2146
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using BumpCombat.Enemies;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Defense
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class DefenseRouteAgent : MonoBehaviour
|
||||
{
|
||||
private EnemyController enemy;
|
||||
private Vector2[] waypoints = Array.Empty<Vector2>();
|
||||
private Action<DefenseRouteAgent> arrivalHandler;
|
||||
private Transform player;
|
||||
private BumpCombat.Player.PlayerHealth playerHealth;
|
||||
private float routeSpeed;
|
||||
private float playerEngagementRange;
|
||||
private float postAttackProgressSeconds;
|
||||
private float postAttackProgressRemaining;
|
||||
private bool wasAttackSequenceInProgress;
|
||||
private bool configured;
|
||||
private int waypointIndex;
|
||||
|
||||
public EnemyController Enemy => enemy;
|
||||
public int Lane { get; private set; }
|
||||
public int LifetimeId => enemy != null ? enemy.SpawnLifetimeIdentity : -1;
|
||||
public DefenseTowerKind ImmuneTowerKind { get; private set; }
|
||||
public Vector2 AnchorPosition => waypoints.Length > 0 ? waypoints[0] : transform.position;
|
||||
public bool HasArrived { get; private set; }
|
||||
|
||||
public void Configure(
|
||||
EnemyController configuredEnemy,
|
||||
int lane,
|
||||
Vector2[] route,
|
||||
float speed,
|
||||
float engagementRange,
|
||||
float postAttackProgress,
|
||||
DefenseTowerKind immuneTowerKind,
|
||||
Action<DefenseRouteAgent> onArrived)
|
||||
{
|
||||
enemy = configuredEnemy;
|
||||
Lane = lane;
|
||||
waypoints = route ?? Array.Empty<Vector2>();
|
||||
routeSpeed = Mathf.Max(0.1f, speed);
|
||||
playerEngagementRange = Mathf.Max(0.1f, engagementRange);
|
||||
postAttackProgressSeconds = Mathf.Max(0f, postAttackProgress);
|
||||
postAttackProgressRemaining = 0f;
|
||||
wasAttackSequenceInProgress = false;
|
||||
ImmuneTowerKind = immuneTowerKind;
|
||||
arrivalHandler = onArrived;
|
||||
waypointIndex = 0;
|
||||
configured = enemy != null && waypoints.Length > 0;
|
||||
HasArrived = false;
|
||||
player = FindAnyObjectByType<BumpCombat.Player.PlayerController>()?.transform;
|
||||
playerHealth = player != null
|
||||
? player.GetComponent<BumpCombat.Player.PlayerHealth>()
|
||||
: null;
|
||||
if (configured)
|
||||
{
|
||||
enemy.TryReposition(waypoints[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryDriveFixedUpdate(float deltaTime)
|
||||
{
|
||||
if (!configured || HasArrived || enemy == null || enemy.IsDead)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enemy.State == EnemyState.Spawn)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool attackSequenceActive = enemy.IsAttackSequenceInProgress;
|
||||
if (attackSequenceActive)
|
||||
{
|
||||
wasAttackSequenceInProgress = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wasAttackSequenceInProgress)
|
||||
{
|
||||
wasAttackSequenceInProgress = false;
|
||||
postAttackProgressRemaining = postAttackProgressSeconds;
|
||||
}
|
||||
|
||||
if (enemy.IsStunned)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
postAttackProgressRemaining = Mathf.Max(
|
||||
0f,
|
||||
postAttackProgressRemaining - Mathf.Max(0f, deltaTime));
|
||||
if (player == null)
|
||||
{
|
||||
player = FindAnyObjectByType<BumpCombat.Player.PlayerController>()?.transform;
|
||||
playerHealth = player != null
|
||||
? player.GetComponent<BumpCombat.Player.PlayerHealth>()
|
||||
: null;
|
||||
}
|
||||
|
||||
if (postAttackProgressRemaining <= 0f
|
||||
&& player != null
|
||||
&& playerHealth != null
|
||||
&& playerHealth.CurrentHealth > 0f
|
||||
&& (player.position - transform.position).sqrMagnitude
|
||||
<= playerEngagementRange * playerEngagementRange)
|
||||
{
|
||||
enemy.Alert();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (waypointIndex >= waypoints.Length)
|
||||
{
|
||||
CompleteArrival();
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector2 current = transform.position;
|
||||
Vector2 target = waypoints[waypointIndex];
|
||||
Vector2 next = Vector2.MoveTowards(
|
||||
current,
|
||||
target,
|
||||
routeSpeed * Mathf.Max(0f, deltaTime));
|
||||
if (!enemy.TryReposition(next))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector2 movementDirection = next - current;
|
||||
if (movementDirection.sqrMagnitude > 0.000001f)
|
||||
{
|
||||
enemy.SetFacingDirection(movementDirection);
|
||||
}
|
||||
Animator animator = enemy.GetComponent<Animator>();
|
||||
animator?.SetBool("IsMoving", (next - current).sqrMagnitude > 0.000001f);
|
||||
if ((target - next).sqrMagnitude <= 0.01f)
|
||||
{
|
||||
waypointIndex++;
|
||||
if (waypointIndex >= waypoints.Length)
|
||||
{
|
||||
CompleteArrival();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTakeTowerDamage(
|
||||
DefenseTowerKind sourceKind,
|
||||
float requestedDamage,
|
||||
float distanceToPlayer,
|
||||
float sightRadius,
|
||||
float outsideSightEfficiency,
|
||||
Vector2 sourcePosition)
|
||||
{
|
||||
if (!configured
|
||||
|| HasArrived
|
||||
|| enemy == null
|
||||
|| enemy.IsDead
|
||||
|| DefensePrototypeRules.IsTowerImmune(
|
||||
sourceKind,
|
||||
ImmuneTowerKind))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float efficiency = DefensePrototypeRules.GetTowerEfficiency(
|
||||
distanceToPlayer,
|
||||
sightRadius,
|
||||
1f,
|
||||
outsideSightEfficiency);
|
||||
Vector2 direction = ((Vector2)transform.position - sourcePosition).normalized;
|
||||
return enemy.TryTakeDamage(
|
||||
Mathf.Max(0f, requestedDamage) * efficiency,
|
||||
direction,
|
||||
0f,
|
||||
false,
|
||||
false,
|
||||
EnemyDamageSource.Tower);
|
||||
}
|
||||
|
||||
private void CompleteArrival()
|
||||
{
|
||||
if (HasArrived)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HasArrived = true;
|
||||
arrivalHandler?.Invoke(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be1e0aa426264604ac15478324ef43aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,79 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Defense
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class DefenseSummonUnit : MonoBehaviour
|
||||
{
|
||||
private DefenseTower owner;
|
||||
private DefensePrototypeController controller;
|
||||
private Vector2 anchorLocalPosition;
|
||||
private SpriteRenderer spriteRenderer;
|
||||
private float attackCooldown;
|
||||
|
||||
public void Configure(
|
||||
DefenseTower tower,
|
||||
DefensePrototypeController prototypeController,
|
||||
Vector2 localAnchor)
|
||||
{
|
||||
owner = tower;
|
||||
controller = prototypeController;
|
||||
anchorLocalPosition = localAnchor;
|
||||
spriteRenderer = gameObject.AddComponent<SpriteRenderer>();
|
||||
Sprite[] frames = Resources.LoadAll<Sprite>("Tutorial/Swordsman_Idle");
|
||||
if (frames != null && frames.Length > 0)
|
||||
{
|
||||
spriteRenderer.sprite = frames[0];
|
||||
}
|
||||
spriteRenderer.color = new Color32(255, 234, 137, 255);
|
||||
spriteRenderer.sortingOrder = 11;
|
||||
transform.localScale = new Vector3(0.9f, 0.9f, 1f);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (owner == null
|
||||
|| !owner.isActiveAndEnabled
|
||||
|| controller == null
|
||||
|| !controller.CanOperateTowers)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
attackCooldown = Mathf.Max(0f, attackCooldown - Time.deltaTime);
|
||||
DefenseRouteAgent target = controller.FindNearestTarget(
|
||||
transform.position,
|
||||
controller.Tuning.SummonAreaRadius,
|
||||
DefenseTowerKind.Summon);
|
||||
Vector2 anchor = owner.transform.TransformPoint(anchorLocalPosition);
|
||||
if (target == null)
|
||||
{
|
||||
transform.position = Vector2.MoveTowards(
|
||||
transform.position,
|
||||
anchor,
|
||||
controller.Tuning.SummonUnitMoveSpeed * Time.deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 targetPosition = target.transform.position;
|
||||
Vector2 fromAnchor = targetPosition - anchor;
|
||||
if (fromAnchor.magnitude > controller.Tuning.SummonAreaRadius)
|
||||
{
|
||||
targetPosition = anchor + fromAnchor.normalized * controller.Tuning.SummonAreaRadius;
|
||||
}
|
||||
|
||||
transform.position = Vector2.MoveTowards(
|
||||
transform.position,
|
||||
targetPosition,
|
||||
controller.Tuning.SummonUnitMoveSpeed * Time.deltaTime);
|
||||
if (attackCooldown <= 0f
|
||||
&& (target.transform.position - transform.position).sqrMagnitude <= controller.Tuning.SummonAttackRange * controller.Tuning.SummonAttackRange)
|
||||
{
|
||||
if (owner.TryAttack(target))
|
||||
{
|
||||
attackCooldown = controller.Tuning.SummonAttackInterval;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2c4cbe8f78e4e11898e78be45697a12
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,118 @@
|
||||
using BumpCombat.Constants;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Defense
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class DefenseTower : MonoBehaviour
|
||||
{
|
||||
private DefensePrototypeController owner;
|
||||
private DefenseConstants tuning;
|
||||
private float attackCooldown;
|
||||
private TextMesh label;
|
||||
|
||||
public DefenseTowerKind Kind { get; private set; }
|
||||
public int PadIndex { get; private set; }
|
||||
public bool IsOperational => isActiveAndEnabled
|
||||
&& owner != null
|
||||
&& owner.CanOperateTowers;
|
||||
|
||||
public void Configure(
|
||||
DefensePrototypeController controller,
|
||||
DefenseTowerKind kind,
|
||||
int padIndex,
|
||||
DefenseConstants constants)
|
||||
{
|
||||
owner = controller;
|
||||
Kind = kind;
|
||||
PadIndex = padIndex;
|
||||
tuning = constants;
|
||||
CreateVisuals();
|
||||
if (Kind == DefenseTowerKind.Summon)
|
||||
{
|
||||
for (int i = 0; i < tuning.SummonUnitCount; i++)
|
||||
{
|
||||
GameObject unitObject = new($"Summon Unit {i + 1}");
|
||||
unitObject.transform.SetParent(transform, false);
|
||||
unitObject.transform.localPosition = new Vector3(
|
||||
(i - (tuning.SummonUnitCount - 1) * 0.5f) * 0.42f,
|
||||
-0.5f,
|
||||
0f);
|
||||
DefenseSummonUnit unit = unitObject.AddComponent<DefenseSummonUnit>();
|
||||
unit.Configure(this, controller, unitObject.transform.localPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsOperational || Kind != DefenseTowerKind.Direct)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
attackCooldown -= Time.deltaTime;
|
||||
if (attackCooldown > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float interval = Kind == DefenseTowerKind.Direct
|
||||
? tuning.DirectTowerAttackInterval
|
||||
: tuning.SummonAttackInterval;
|
||||
if (owner.TryAttackFromTower(this))
|
||||
{
|
||||
attackCooldown = Mathf.Max(0.05f, interval);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateVisuals()
|
||||
{
|
||||
SpriteRenderer renderer = gameObject.GetComponent<SpriteRenderer>();
|
||||
if (renderer == null)
|
||||
{
|
||||
renderer = gameObject.AddComponent<SpriteRenderer>();
|
||||
}
|
||||
renderer.sprite = DefensePrototypeController.WhiteSprite;
|
||||
renderer.color = Kind == DefenseTowerKind.Direct
|
||||
? new Color32(65, 211, 240, 255)
|
||||
: new Color32(244, 154, 74, 255);
|
||||
renderer.sortingOrder = 8;
|
||||
transform.localScale = new Vector3(0.72f, 0.72f, 1f);
|
||||
|
||||
GameObject crown = new("Tower Crown");
|
||||
crown.transform.SetParent(transform, false);
|
||||
crown.transform.localPosition = new Vector3(0f, 0.42f, 0f);
|
||||
crown.transform.localScale = new Vector3(0.42f, 0.22f, 1f);
|
||||
SpriteRenderer crownRenderer = crown.AddComponent<SpriteRenderer>();
|
||||
crownRenderer.sprite = DefensePrototypeController.WhiteSprite;
|
||||
crownRenderer.color = Color.white;
|
||||
crownRenderer.sortingOrder = 9;
|
||||
|
||||
GameObject labelObject = new("Tower Label");
|
||||
labelObject.transform.SetParent(transform, false);
|
||||
labelObject.transform.localPosition = new Vector3(0f, 0.85f, 0f);
|
||||
label = labelObject.AddComponent<TextMesh>();
|
||||
label.text = Kind == DefenseTowerKind.Direct ? "DIRECT" : "SUMMON";
|
||||
label.anchor = TextAnchor.MiddleCenter;
|
||||
label.alignment = TextAlignment.Center;
|
||||
label.characterSize = 0.16f;
|
||||
label.fontSize = 32;
|
||||
label.color = Color.white;
|
||||
label.GetComponent<MeshRenderer>().sortingOrder = 10;
|
||||
}
|
||||
|
||||
public bool TryAttack(DefenseRouteAgent target)
|
||||
{
|
||||
if (target == null || owner == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float damage = Kind == DefenseTowerKind.Direct
|
||||
? tuning.DirectTowerDamage
|
||||
: tuning.SummonDamage;
|
||||
return owner.TryApplyTowerDamage(this, target, damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e82deace7a1a47e0b14c872019e51a01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user