convert game playing..

This commit is contained in:
2026-09-23 12:53:52 +09:00
parent 37c7942b71
commit 5d11288a6f
136 changed files with 15856 additions and 299 deletions
@@ -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: