diff --git a/Assets/_Project/Definitions/BuilderSpells.meta b/Assets/_Project/Definitions/BuilderSpells.meta
deleted file mode 100644
index 15f3140..0000000
--- a/Assets/_Project/Definitions/BuilderSpells.meta
+++ /dev/null
@@ -1,8 +0,0 @@
-fileFormatVersion: 2
-guid: f6f56fe562e9b8c229442088bb6ce11c
-folderAsset: yes
-DefaultImporter:
- externalObjects: {}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Assets/_Project/Definitions/BuilderSpells/FireballSpell.asset b/Assets/_Project/Definitions/BuilderSpells/FireballSpell.asset
deleted file mode 100644
index f813362..0000000
--- a/Assets/_Project/Definitions/BuilderSpells/FireballSpell.asset
+++ /dev/null
@@ -1,24 +0,0 @@
-%YAML 1.1
-%TAG !u! tag:unity3d.com,2011:
---- !u!114 &11400000
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 0}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 22b36bfcd0ae4af4098d908138e874e1, type: 3}
- m_Name: FireballSpell
- m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderSpells.FireballSpellDefinition
- DisplayName:
- Description:
- Icon: {fileID: 0}
- Cooldown: 5
- TargetType: 0
- Radius: 3
- enemyLayerMask:
- serializedVersion: 2
- m_Bits: 128
- Damage: 50
diff --git a/Assets/_Project/Definitions/BuilderSpells/FireballSpell.asset.meta b/Assets/_Project/Definitions/BuilderSpells/FireballSpell.asset.meta
deleted file mode 100644
index 61581ca..0000000
--- a/Assets/_Project/Definitions/BuilderSpells/FireballSpell.asset.meta
+++ /dev/null
@@ -1,8 +0,0 @@
-fileFormatVersion: 2
-guid: 051c48f416e5366b59d49b0e062333ac
-NativeFormatImporter:
- externalObjects: {}
- mainObjectFileID: 11400000
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Assets/_Project/Definitions/BuilderSpells/SlowAreaSpell.asset b/Assets/_Project/Definitions/BuilderSpells/SlowAreaSpell.asset
deleted file mode 100644
index d45c506..0000000
--- a/Assets/_Project/Definitions/BuilderSpells/SlowAreaSpell.asset
+++ /dev/null
@@ -1,25 +0,0 @@
-%YAML 1.1
-%TAG !u! tag:unity3d.com,2011:
---- !u!114 &11400000
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 0}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 249c65067c3bac80693255e594d844c7, type: 3}
- m_Name: SlowAreaSpell
- m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderSpells.SlowAreaSpellDefinition
- DisplayName: Slow Area
- Description: Slows enemies in an area
- Icon: {fileID: 0}
- Cooldown: 5
- TargetType: 0
- Radius: 5
- enemyLayerMask:
- serializedVersion: 2
- m_Bits: 128
- SlowFactor: 0.5
- EffectDuration: 3
diff --git a/Assets/_Project/Definitions/BuilderSpells/SlowAreaSpell.asset.meta b/Assets/_Project/Definitions/BuilderSpells/SlowAreaSpell.asset.meta
deleted file mode 100644
index 1518d59..0000000
--- a/Assets/_Project/Definitions/BuilderSpells/SlowAreaSpell.asset.meta
+++ /dev/null
@@ -1,8 +0,0 @@
-fileFormatVersion: 2
-guid: d893f46b037536b4ba91307e184cecfe
-NativeFormatImporter:
- externalObjects: {}
- mainObjectFileID: 11400000
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_Fireball.asset b/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_Fireball.asset
deleted file mode 100644
index 5ec8c55..0000000
--- a/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_Fireball.asset
+++ /dev/null
@@ -1,19 +0,0 @@
-%YAML 1.1
-%TAG !u! tag:unity3d.com,2011:
---- !u!114 &11400000
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 0}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: a574e8aa74d8787f2b949300932af485, type: 3}
- m_Name: Draft_BuilderSpellOption_Fireball
- m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Draft.BuilderSpellDraftOption
- DisplayName: Fireball
- Description: Shoot a FIREBALL out of your COCK
- Icon: {fileID: 0}
- Weight: 1
- Kind: 0
diff --git a/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_Fireball.asset.meta b/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_Fireball.asset.meta
deleted file mode 100644
index f9e25b5..0000000
--- a/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_Fireball.asset.meta
+++ /dev/null
@@ -1,8 +0,0 @@
-fileFormatVersion: 2
-guid: 5f390de69ba43b2a6a0d89cf09580320
-NativeFormatImporter:
- externalObjects: {}
- mainObjectFileID: 11400000
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_SlowArea.asset b/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_SlowArea.asset
deleted file mode 100644
index 5dd615e..0000000
--- a/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_SlowArea.asset
+++ /dev/null
@@ -1,19 +0,0 @@
-%YAML 1.1
-%TAG !u! tag:unity3d.com,2011:
---- !u!114 &11400000
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 0}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: a574e8aa74d8787f2b949300932af485, type: 3}
- m_Name: Draft_BuilderSpellOption_SlowArea
- m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Draft.BuilderSpellDraftOption
- DisplayName: Slow Area
- Description: Slows enemies in the target area
- Icon: {fileID: 0}
- Weight: 1
- Kind: 1
diff --git a/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_SlowArea.asset.meta b/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_SlowArea.asset.meta
deleted file mode 100644
index 40f9145..0000000
--- a/Assets/_Project/Definitions/Draft/Draft_BuilderSpellOption_SlowArea.asset.meta
+++ /dev/null
@@ -1,8 +0,0 @@
-fileFormatVersion: 2
-guid: 29b35841f7e5db454903798cb5d83434
-NativeFormatImporter:
- externalObjects: {}
- mainObjectFileID: 11400000
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/Player/Player.prefab b/Assets/_Project/Prefabs/Player/Player.prefab
index 08614f3..33c1316 100644
--- a/Assets/_Project/Prefabs/Player/Player.prefab
+++ b/Assets/_Project/Prefabs/Player/Player.prefab
@@ -17,7 +17,6 @@ GameObject:
- component: {fileID: 2806524246861401799}
- component: {fileID: 2806524246861401801}
- component: {fileID: 5683786710272852339}
- - component: {fileID: 6856236869205671849}
m_Layer: 0
m_Name: Player
m_TagString: Untagged
@@ -160,16 +159,3 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderUpgradeManager
ShowTopMostFoldoutHeaderGroup: 1
---- !u!114 &6856236869205671849
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 3493329038866903420}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: dfa83208b02c50b40866bb1846240c4a, type: 3}
- m_Name:
- m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerSpellLoadout
- ShowTopMostFoldoutHeaderGroup: 1
diff --git a/Assets/_Project/Scenes/Levels/9Player.unity b/Assets/_Project/Scenes/Levels/9Player.unity
index 98a6d5e..3264d21 100644
--- a/Assets/_Project/Scenes/Levels/9Player.unity
+++ b/Assets/_Project/Scenes/Levels/9Player.unity
@@ -13345,53 +13345,6 @@ Transform:
m_Children: []
m_Father: {fileID: 1097152085}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!1 &914832724
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 914832726}
- - component: {fileID: 914832725}
- m_Layer: 0
- m_Name: BuilderSpellPool
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!114 &914832725
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 914832724}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 36a7be1b3824682b8a183fbcf8564652, type: 3}
- m_Name:
- m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderSpells.BuilderSpellPool
- spells:
- - {fileID: 11400000, guid: 051c48f416e5366b59d49b0e062333ac, type: 2}
- - {fileID: 11400000, guid: d893f46b037536b4ba91307e184cecfe, type: 2}
---- !u!4 &914832726
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 914832724}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: -18.90215, y: 6.17317, z: 0.5013}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 0}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &921503905
GameObject:
m_ObjectHideFlags: 0
@@ -13774,57 +13727,6 @@ BoxCollider:
serializedVersion: 3
m_Size: {x: 17, y: 1, z: 13}
m_Center: {x: 0, y: 0, z: 0}
---- !u!1 &993870443
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 993870445}
- - component: {fileID: 993870444}
- m_Layer: 0
- m_Name: BuilderSpellCastController
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!114 &993870444
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 993870443}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: cc99a2b3d3e8d8178a74e1d68a34553c, type: 3}
- m_Name:
- m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderSpellCastController
- groundLayerMask:
- serializedVersion: 2
- m_Bits: 128
- raycastMaxDistance: 500
- aimDecal: {fileID: 0}
- pointReticleDiameter: 1.5
- projectionDepth: 50
---- !u!4 &993870445
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 993870443}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: -18.90215, y: 6.17317, z: 0.5013}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 0}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &997315214
GameObject:
m_ObjectHideFlags: 0
@@ -28663,9 +28565,6 @@ MonoBehaviour:
- {fileID: 11400000, guid: 6b1d3f8a2c5e4097b8a1d0c3e6f9b240, type: 2}
- {fileID: 11400000, guid: f4a2b8d1c6e93f05a7b2d4c8e1f6a930, type: 2}
- {fileID: 11400000, guid: 65ff25c1c8a89f7df8e88f71968c1c98, type: 2}
- - {fileID: 11400000, guid: 5f390de69ba43b2a6a0d89cf09580320, type: 2}
- - {fileID: 11400000, guid: 65ff25c1c8a89f7df8e88f71968c1c98, type: 2}
- - {fileID: 11400000, guid: 29b35841f7e5db454903798cb5d83434, type: 2}
--- !u!4 &2139601601
Transform:
m_ObjectHideFlags: 0
@@ -29063,5 +28962,3 @@ SceneRoots:
- {fileID: 176580462}
- {fileID: 516125087}
- {fileID: 2079088113}
- - {fileID: 914832726}
- - {fileID: 993870445}
diff --git a/Assets/_Project/Scripts/Core/BuilderSpellKind.cs b/Assets/_Project/Scripts/Core/BuilderSpellKind.cs
deleted file mode 100644
index 58fcb5d..0000000
--- a/Assets/_Project/Scripts/Core/BuilderSpellKind.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace TD.Core
-{
- ///
- /// Identifies which builder spell a
- /// represents. Mirrors
- /// — a stable, enum-indexed identifier used by
- /// and
- /// instead of an asset reference.
- ///
- public enum BuilderSpellKind : byte
- {
- Fireball = 0,
- SlowArea = 1,
- }
-}
diff --git a/Assets/_Project/Scripts/Core/BuilderSpellKind.cs.meta b/Assets/_Project/Scripts/Core/BuilderSpellKind.cs.meta
deleted file mode 100644
index af62b64..0000000
--- a/Assets/_Project/Scripts/Core/BuilderSpellKind.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: a043765d6aa295a5397226e3e32d2c37
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Core/SpellTargetType.cs b/Assets/_Project/Scripts/Core/SpellTargetType.cs
deleted file mode 100644
index 7ebb6ff..0000000
--- a/Assets/_Project/Scripts/Core/SpellTargetType.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace TD.Core
-{
- ///
- /// How a is aimed.
- ///
- ///
- /// Deliberately not named TargetType — that name is already taken in this
- /// namespace by (Single/Splash/Chain/AllInRange, used by
- /// TowerDefinition/TowerCombat), and every combat-adjacent file already
- /// has using TD.Core;.
- ///
- public enum SpellTargetType : byte
- {
- /// Aimed at a single ground point. No radius preview while aiming, even
- /// if the spell has an internal splash radius.
- PointTarget = 0,
-
- /// Aimed at a ground point, affecting everything within
- /// . Previewed
- /// as a scaled radius decal while aiming.
- AreaOfEffect = 1,
- }
-}
diff --git a/Assets/_Project/Scripts/Core/SpellTargetType.cs.meta b/Assets/_Project/Scripts/Core/SpellTargetType.cs.meta
deleted file mode 100644
index 0722838..0000000
--- a/Assets/_Project/Scripts/Core/SpellTargetType.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 0ebeda6f965b64f7ca82ecd6f849142a
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderSpellCastController.cs b/Assets/_Project/Scripts/Gameplay/BuilderSpellCastController.cs
deleted file mode 100644
index 49160da..0000000
--- a/Assets/_Project/Scripts/Gameplay/BuilderSpellCastController.cs
+++ /dev/null
@@ -1,226 +0,0 @@
-// Assets/_Project/Scripts/Gameplay/BuilderSpellCastController.cs
-using UnityEngine;
-using UnityEngine.InputSystem;
-using UnityEngine.Rendering.Universal;
-using TD.Core;
-using TD.UI;
-using TD.Gameplay.BuilderSpells;
-
-namespace TD.Gameplay
-{
- ///
- /// Per-client controller for the builder-spell casting UX. Watches
- /// to enter aim mode, raycasts against a broad ground layer
- /// (must cover the enemy path, not just buildable tiles), previews the aim point, and
- /// dispatches the cast to via RPC on confirm.
- ///
- ///
- /// Plain MonoBehaviour. Aiming visuals are purely cosmetic and local, same
- /// rationale as . All server-authoritative
- /// resolution lives in .
- ///
- /// Point vs. area preview. shows a
- /// small fixed-size reticle, even if the spell has an internal splash radius.
- /// scales the same decal to the spell's
- /// , mirroring 's
- /// diameter-sizing idiom.
- ///
- /// Single-shot only. Unlike tower placement, there is no chained/shift-held
- /// casting — one keypress aims one cast.
- ///
- public class BuilderSpellCastController : MonoBehaviour
- {
- // ----- Inspector --------------------------------------------------
-
- [Tooltip("Physics layer(s) the aim raycast hits. Must cover the whole level's ground " +
- "(including over the enemy path), not just buildable tiles — set this to " +
- "'TerrainGeometry', not 'BuildablePlane'.")]
- [SerializeField] private LayerMask groundLayerMask;
-
- [Tooltip("Maximum raycast distance from the camera to the ground.")]
- [SerializeField] private float raycastMaxDistance = 500f;
-
- [Tooltip("Ground decal used for both the point reticle and the area-of-effect " +
- "radius preview. Auto-found in children if empty.")]
- [SerializeField] private DecalProjector aimDecal;
-
- [Tooltip("Fixed diameter of the reticle shown for PointTarget spells, regardless of " +
- "any internal splash radius the spell may use server-side.")]
- [SerializeField] private float pointReticleDiameter = 1.5f;
-
- [Tooltip("Vertical thickness of the decal projector's projection volume. Should " +
- "exceed the map's vertical range so it projects onto terrain at any height.")]
- [SerializeField] private float projectionDepth = 50f;
-
- // ----- Active-aim state ---------------------------------------------
-
- // -1 when not aiming.
- private int activeSlot = -1;
- private BuilderSpellDefinition activeDefinition;
-
- private bool lastHitValid;
- private Vector3 lastHitPoint;
-
- // ----- Lifecycle ----------------------------------------------------
-
- private void Awake()
- {
- if (aimDecal == null) aimDecal = GetComponentInChildren();
- if (aimDecal != null)
- {
- aimDecal.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
- aimDecal.enabled = false;
- }
- }
-
- private void OnDisable()
- {
- ExitAimMode();
- }
-
- private void Update()
- {
- if (!HUDController.IsTextInputActive)
- ScanHotkeys();
-
- if (activeSlot < 0) return; // idle — nothing to aim
-
- var keyboard = Keyboard.current;
- if (keyboard != null && keyboard.escapeKey.wasPressedThisFrame)
- {
- ExitAimMode();
- return;
- }
-
- var mouse = Mouse.current;
- if (mouse == null) return;
-
- if (mouse.rightButton.wasPressedThisFrame)
- {
- ExitAimMode();
- return;
- }
-
- if (TryGetGroundHit(mouse.position.ReadValue(), out Vector3 hitPoint))
- {
- lastHitValid = true;
- lastHitPoint = hitPoint;
- ShowAimDecal(hitPoint);
- }
- else
- {
- lastHitValid = false;
- HideAimDecal();
- }
-
- if (mouse.leftButton.wasPressedThisFrame && lastHitValid)
- {
- TrySubmitCast();
- }
- }
-
- // ----- Hotkey scan --------------------------------------------------
-
- private void ScanHotkeys()
- {
- if (activeSlot >= 0) return; // already aiming — hotkeys re-scanned only when idle
-
- var loadout = PlayerSpellLoadout.Local;
- if (loadout == null) return;
-
- var keyboard = Keyboard.current;
- if (keyboard == null) return;
-
- var layout = SpellHotkeys.Layout;
- int slotCount = Mathf.Min(loadout.SlotCount, layout.Length);
-
- for (int i = 0; i < slotCount; i++)
- {
- if (!keyboard[layout[i]].wasPressedThisFrame) continue;
- if (loadout.IsSlotOnCooldown(i)) continue;
-
- BeginAiming(i, loadout);
- break;
- }
- }
-
- private void BeginAiming(int slot, PlayerSpellLoadout loadout)
- {
- var kind = loadout.GetKind(slot);
- if (kind == null) return;
-
- var definition = BuilderSpellPool.Instance?.Get(kind.Value);
- if (definition == null)
- {
- Debug.LogWarning($"[BuilderSpellCastController] No BuilderSpellDefinition for {kind.Value}.");
- return;
- }
-
- activeSlot = slot;
- activeDefinition = definition;
- lastHitValid = false;
- }
-
- private void ExitAimMode()
- {
- activeSlot = -1;
- activeDefinition = null;
- lastHitValid = false;
- HideAimDecal();
- }
-
- // ----- Raycasting ---------------------------------------------------
-
- private bool TryGetGroundHit(Vector2 screenPos, out Vector3 hitPoint)
- {
- hitPoint = Vector3.zero;
-
- var cam = Camera.main;
- if (cam == null) return false;
-
- Ray ray = cam.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
- if (Physics.Raycast(ray, out RaycastHit hit, raycastMaxDistance, groundLayerMask))
- {
- hitPoint = hit.point;
- return true;
- }
- return false;
- }
-
- // ----- Aim decal -----------------------------------------------------
-
- private void ShowAimDecal(Vector3 point)
- {
- if (aimDecal == null) return;
-
- float diameter = activeDefinition.TargetType == SpellTargetType.AreaOfEffect
- ? activeDefinition.Radius * 2f
- : pointReticleDiameter;
-
- aimDecal.size = new Vector3(diameter, diameter, projectionDepth);
- aimDecal.pivot = Vector3.zero;
- aimDecal.transform.position = point;
- aimDecal.enabled = true;
- }
-
- private void HideAimDecal()
- {
- if (aimDecal != null) aimDecal.enabled = false;
- }
-
- // ----- Cast submission ------------------------------------------------
-
- private void TrySubmitCast()
- {
- var loadout = PlayerSpellLoadout.Local;
- if (loadout == null)
- {
- ExitAimMode();
- return;
- }
-
- loadout.RequestCastSpellRpc(activeSlot, lastHitPoint);
- ExitAimMode();
- }
- }
-}
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderSpellCastController.cs.meta b/Assets/_Project/Scripts/Gameplay/BuilderSpellCastController.cs.meta
deleted file mode 100644
index ffe509e..0000000
--- a/Assets/_Project/Scripts/Gameplay/BuilderSpellCastController.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: cc99a2b3d3e8d8178a74e1d68a34553c
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderSpells.meta b/Assets/_Project/Scripts/Gameplay/BuilderSpells.meta
deleted file mode 100644
index fb42554..0000000
--- a/Assets/_Project/Scripts/Gameplay/BuilderSpells.meta
+++ /dev/null
@@ -1,8 +0,0 @@
-fileFormatVersion: 2
-guid: 7f64edca739cf31bfbdf58e2403b4a74
-folderAsset: yes
-DefaultImporter:
- externalObjects: {}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellDefinition.cs b/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellDefinition.cs
deleted file mode 100644
index e1ca3b2..0000000
--- a/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellDefinition.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-// Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellDefinition.cs
-using UnityEngine;
-using TD.Core;
-
-namespace TD.Gameplay.BuilderSpells
-{
- ///
- /// Base class for one builder spell — a player-cast, key-triggered ability (e.g. "drop a
- /// fireball on a point", "slow enemies in a radius"). Granted via the draft () and tracked per-player by , which owns the hotkey slot and cooldown state.
- ///
- ///
- /// Contrast with . Effects
- /// are passive and queried by consumers; spells are active and resolve themselves.
- /// is that resolution — it IS the "apply the effect" method,
- /// replacing the effect system's IsValidFor-only gate pattern (grant-time validity
- /// still lives on ).
- ///
- /// One asset per kind. is fixed per subclass, same as
- /// . uses
- /// it to build a fixed-size, enum-indexed lookup table.
- ///
- /// Server-only resolution, client-only visual. runs on
- /// the server and applies damage/status via the same Physics.OverlapSphereNonAlloc +
- /// EnemyHealth/EnemyStatus pattern TowerCombat already uses.
- /// runs on every peer (including the server) after a successful
- /// cast, purely for presentation.
- ///
- public abstract class BuilderSpellDefinition : ScriptableObject
- {
- /// Which builder spell this asset's data belongs to.
- public abstract BuilderSpellKind Kind { get; }
-
- [Header("Presentation")]
- [Tooltip("Name shown on the draft card and the cast hotbar.")]
- public string DisplayName;
-
- [Tooltip("Short description shown on the draft card.")]
- [TextArea(2, 4)]
- public string Description;
-
- [Tooltip("Icon shown on the draft card and the cast hotbar.")]
- public Sprite Icon;
-
- [Header("Casting")]
- [Tooltip("Seconds before this spell can be cast again after a successful cast.")]
- [Min(0f)]
- public float Cooldown = 5f;
-
- [Tooltip("How the cast controller aims this spell. AreaOfEffect previews Radius as a " +
- "decal while aiming; PointTarget does not, even if Radius is used internally " +
- "(e.g. a splash radius).")]
- public SpellTargetType TargetType;
-
- [Tooltip("AreaOfEffect: the resolution radius AND the client-side preview size. " +
- "PointTarget: optional internal-only radius (e.g. splash) with no preview.")]
- [Min(0f)]
- public float Radius;
-
- [Tooltip("Physics layer(s) enemies occupy, queried by this spell's own OverlapSphere " +
- "call. Each spell asset authors its own mask — same convention as " +
- "TowerCombat/Projectile's per-instance enemyLayerMask.")]
- [SerializeField]
- protected LayerMask enemyLayerMask;
-
- // Shared scratch buffer for OverlapSphereNonAlloc queries. The server processes casts
- // sequentially (one RPC handler at a time), so a static buffer shared across all spell
- // assets is safe — mirrors TowerCombat.s_overlapBuffer, but sized larger (64 vs. 32):
- // OverlapSphereNonAlloc silently truncates past the buffer length with no way to
- // detect the truncation, and spells are more likely than a single tower's splash/chain
- // radius to catch a dense horde. TowerCombat's buffer is intentionally left at 32 —
- // out of scope here, flagged separately for the team to revisit.
- protected static readonly Collider[] s_overlapBuffer = new Collider[64];
-
- ///
- /// Server-only: resolve this spell's effect at for the
- /// casting player. Returns false if the cast could not be applied (e.g. hit nothing) —
- /// treats a false return as a no-op and does not start
- /// the cooldown.
- ///
- public abstract bool ServerCast(ulong clientId, Vector3 targetPoint);
-
- ///
- /// Runs on every peer (via 's ClientRpc) after a
- /// successful . Default no-op; override to spawn an impact VFX
- /// prefab and self-destroy it, the same idiom used elsewhere for one-off visuals.
- ///
- public virtual void ClientPlayVfx(Vector3 targetPoint) { }
- }
-}
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellDefinition.cs.meta b/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellDefinition.cs.meta
deleted file mode 100644
index 57deb3e..0000000
--- a/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellDefinition.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 9fca3191d98aa2652a9c5475d5c6b153
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellPool.cs b/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellPool.cs
deleted file mode 100644
index 2f44df6..0000000
--- a/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellPool.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-// Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellPool.cs
-using System;
-using UnityEngine;
-using TD.Core;
-
-namespace TD.Gameplay.BuilderSpells
-{
- ///
- /// Scene singleton holding every available this match,
- /// authored as a flat array in the inspector for convenience. Internally it builds a
- /// fixed-size table indexed by so lookups are a single array
- /// access, not a scan.
- ///
- ///
- /// Plain MonoBehaviour: identical on every peer (same assets), so there is nothing to sync.
- /// The server reads it to resolve casts; clients read it to resolve and to render the cast hotbar. Mirrors
- /// exactly.
- ///
- public class BuilderSpellPool : MonoBehaviour
- {
- public static BuilderSpellPool Instance { get; private set; }
-
- [Tooltip("Every BuilderSpellDefinition asset available this match. One entry per " +
- "BuilderSpellKind — order doesn't matter, Kind on the asset itself decides " +
- "its slot.")]
- [SerializeField] private BuilderSpellDefinition[] spells;
-
- // Fixed-size, enum-indexed lookup built once in Awake. Sized to the enum's entry count,
- // not authored count, so an out-of-range Kind is a compile-time impossibility rather
- // than a bounds check we'd otherwise need on every Get().
- private BuilderSpellDefinition[] byKind;
-
- private void Awake()
- {
- if (Instance != null && Instance != this)
- {
- Debug.LogError("[BuilderSpellPool] Multiple instances detected. Only one per scene.");
- return;
- }
- Instance = this;
-
- int kindCount = Enum.GetValues(typeof(BuilderSpellKind)).Length;
- byKind = new BuilderSpellDefinition[kindCount];
- if (spells == null) return;
-
- for (int i = 0; i < spells.Length; i++)
- {
- var def = spells[i];
- if (def == null) continue;
- byKind[(int)def.Kind] = def;
- }
- }
-
- private void OnDestroy()
- {
- if (Instance == this) Instance = null;
- }
-
- /// Returns the spell asset for , or null if none is
- /// authored in this pool.
- public BuilderSpellDefinition Get(BuilderSpellKind kind)
- {
- int i = (int)kind;
- return (byKind != null && i >= 0 && i < byKind.Length) ? byKind[i] : null;
- }
- }
-}
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellPool.cs.meta b/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellPool.cs.meta
deleted file mode 100644
index 52803c7..0000000
--- a/Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellPool.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 36a7be1b3824682b8a183fbcf8564652
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderSpells/FireballSpellDefinition.cs b/Assets/_Project/Scripts/Gameplay/BuilderSpells/FireballSpellDefinition.cs
deleted file mode 100644
index e7facd7..0000000
--- a/Assets/_Project/Scripts/Gameplay/BuilderSpells/FireballSpellDefinition.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-// Assets/_Project/Scripts/Gameplay/BuilderSpells/FireballSpellDefinition.cs
-using UnityEngine;
-using TD.Core;
-
-namespace TD.Gameplay.BuilderSpells
-{
- ///
- /// Builder spell #1 — drop a fireball on a point, dealing direct damage to whatever's
- /// there plus splash damage to nearby enemies.
- ///
- ///
- /// is
- /// — the cast controller shows only a fixed-size reticle, even though
- /// is still used internally here as the splash
- /// radius (0 = no splash, direct hit only).
- ///
- [CreateAssetMenu(fileName = "FireballSpell", menuName = "TD/Builder Spells/Fireball")]
- public class FireballSpellDefinition : BuilderSpellDefinition
- {
- public override BuilderSpellKind Kind => BuilderSpellKind.Fireball;
-
- [Header("Fireball")]
- [Tooltip("Damage dealt to every enemy within Radius of the target point.")]
- [Min(0f)]
- public float Damage = 50f;
-
- public override bool ServerCast(ulong clientId, Vector3 targetPoint)
- {
- PlayerSlot owner = PlayerMatchState.SlotForClient(clientId);
-
- int count = Physics.OverlapSphereNonAlloc(
- targetPoint, Mathf.Max(Radius, 0.01f), s_overlapBuffer, enemyLayerMask);
-
- // TEMP DEBUG — remove once cast resolution is confirmed working.
- Debug.Log($"[FireballSpellDefinition] ServerCast at {targetPoint}, radius " +
- $"{Mathf.Max(Radius, 0.01f):0.##}, found {count} colliders.");
-
- bool hitAny = false;
- for (int i = 0; i < count; i++)
- {
- var enemyHealth = s_overlapBuffer[i].GetComponent();
- if (enemyHealth == null || enemyHealth.IsDead) continue;
-
- enemyHealth.TakeDamage(Damage, DamageType.Fire, owner);
- hitAny = true;
- }
-
- return hitAny;
- }
- }
-}
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderSpells/FireballSpellDefinition.cs.meta b/Assets/_Project/Scripts/Gameplay/BuilderSpells/FireballSpellDefinition.cs.meta
deleted file mode 100644
index 0871524..0000000
--- a/Assets/_Project/Scripts/Gameplay/BuilderSpells/FireballSpellDefinition.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 22b36bfcd0ae4af4098d908138e874e1
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderSpells/SlowAreaSpellDefinition.cs b/Assets/_Project/Scripts/Gameplay/BuilderSpells/SlowAreaSpellDefinition.cs
deleted file mode 100644
index 4a640df..0000000
--- a/Assets/_Project/Scripts/Gameplay/BuilderSpells/SlowAreaSpellDefinition.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-// Assets/_Project/Scripts/Gameplay/BuilderSpells/SlowAreaSpellDefinition.cs
-using UnityEngine;
-using TD.Core;
-
-namespace TD.Gameplay.BuilderSpells
-{
- ///
- /// Builder spell #2 — slow every enemy within an area for a duration.
- ///
- ///
- /// is
- /// — the cast controller previews as a decal
- /// while aiming, and this spell resolves against that same radius (no separate field).
- /// Reuses the existing slow/DoT system () rather than
- /// introducing a new status mechanism.
- ///
- [CreateAssetMenu(fileName = "SlowAreaSpell", menuName = "TD/Builder Spells/Slow Area")]
- public class SlowAreaSpellDefinition : BuilderSpellDefinition
- {
- public override BuilderSpellKind Kind => BuilderSpellKind.SlowArea;
-
- [Header("Slow Area")]
- [Tooltip("Speed multiplier applied to affected enemies for EffectDuration " +
- "(e.g. 0.5 = half speed).")]
- [Range(0f, 1f)]
- public float SlowFactor = 0.5f;
-
- [Tooltip("Seconds the slow lasts. Re-casting on an already-slowed enemy refreshes it.")]
- [Min(0f)]
- public float EffectDuration = 3f;
-
- public override bool ServerCast(ulong clientId, Vector3 targetPoint)
- {
- PlayerSlot owner = PlayerMatchState.SlotForClient(clientId);
-
- int count = Physics.OverlapSphereNonAlloc(
- targetPoint, Mathf.Max(Radius, 0.01f), s_overlapBuffer, enemyLayerMask);
-
- // TEMP DEBUG — remove once cast resolution is confirmed working.
- Debug.Log($"[SlowAreaSpellDefinition] ServerCast at {targetPoint}, radius " +
- $"{Mathf.Max(Radius, 0.01f):0.##}, found {count} colliders.");
- for (int i = 0; i < count; i++)
- {
- var c = s_overlapBuffer[i];
- Debug.Log($" hit[{i}]: '{c.name}' layer={LayerMask.LayerToName(c.gameObject.layer)} " +
- $"hasEnemyHealth={c.GetComponent() != null}");
- }
-
- bool hitAny = false;
- for (int i = 0; i < count; i++)
- {
- var enemyHealth = s_overlapBuffer[i].GetComponent();
- if (enemyHealth == null || enemyHealth.IsDead) continue;
-
- var enemyStatus = s_overlapBuffer[i].GetComponent();
- if (enemyStatus == null) continue;
-
- enemyStatus.ApplyEffect(DamageType.Cold, SlowFactor, EffectDuration, owner);
- hitAny = true;
- }
-
- return hitAny;
- }
- }
-}
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderSpells/SlowAreaSpellDefinition.cs.meta b/Assets/_Project/Scripts/Gameplay/BuilderSpells/SlowAreaSpellDefinition.cs.meta
deleted file mode 100644
index 1ba03fa..0000000
--- a/Assets/_Project/Scripts/Gameplay/BuilderSpells/SlowAreaSpellDefinition.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 249c65067c3bac80693255e594d844c7
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/Draft/BuilderSpellDraftOption.cs b/Assets/_Project/Scripts/Gameplay/Draft/BuilderSpellDraftOption.cs
deleted file mode 100644
index 416fc03..0000000
--- a/Assets/_Project/Scripts/Gameplay/Draft/BuilderSpellDraftOption.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-// Assets/_Project/Scripts/Gameplay/Draft/BuilderSpellDraftOption.cs
-using UnityEngine;
-using TD.Core;
-using TD.Gameplay.BuilderSpells;
-
-namespace TD.Gameplay.Draft
-{
- ///
- /// Draft choice — "gain a builder spell". Grants the player a key-triggered active ability,
- /// adding it to their .
- ///
- ///
- /// Mirrors : carries the
- /// directly rather than an asset reference, since the enum value is the stable identifier
- /// and already key off of.
- /// also checks slot capacity — once
- /// is reached, no further spell options are
- /// offered.
- ///
- [CreateAssetMenu(fileName = "BuilderSpellOption", menuName = "TD/Draft/Builder Spell Option")]
- public class BuilderSpellDraftOption : DraftOption
- {
- [Header("Payload")]
- [Tooltip("The builder spell this option grants.")]
- public BuilderSpellKind Kind;
-
- public override bool IsValidFor(ulong clientId)
- {
- var loadout = PlayerSpellLoadout.GetForClient(clientId);
- if (loadout == null) return false;
- if (loadout.SlotCount >= PlayerSpellLoadout.MaxSpellSlots) return false;
- return !loadout.PlayerHasSpell(Kind);
- }
-
- public override bool ServerApply(ulong clientId)
- {
- var loadout = PlayerSpellLoadout.GetForClient(clientId);
- return loadout != null && loadout.ServerGrantSpell(Kind);
- }
- }
-}
diff --git a/Assets/_Project/Scripts/Gameplay/Draft/BuilderSpellDraftOption.cs.meta b/Assets/_Project/Scripts/Gameplay/Draft/BuilderSpellDraftOption.cs.meta
deleted file mode 100644
index a45da95..0000000
--- a/Assets/_Project/Scripts/Gameplay/Draft/BuilderSpellDraftOption.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: a574e8aa74d8787f2b949300932af485
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs b/Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs
deleted file mode 100644
index a6a1f68..0000000
--- a/Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs
+++ /dev/null
@@ -1,177 +0,0 @@
-// Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs
-using System.Collections.Generic;
-using Unity.Netcode;
-using UnityEngine;
-using TD.Core;
-using TD.Gameplay.BuilderSpells;
-
-namespace TD.Gameplay
-{
- ///
- /// Per-player set of granted builder spells and their cooldowns. Lives on the Player
- /// prefab alongside and .
- ///
- ///
- /// Slot == grant order == hotkey. is append-only; a
- /// slot's index in the list is both its identity and the index into
- /// . There is no player-configurable rebinding.
- ///
- /// Cast flow. The owning client calls with a
- /// slot index and an aimed world point. The server re-validates everything (slot exists,
- /// off cooldown, ) since client-side checks in
- /// are UX-only, resolves the
- /// via , and calls
- /// . A successful cast starts the cooldown
- /// and fires so every peer plays the same visual.
- ///
- public class PlayerSpellLoadout : NetworkBehaviour
- {
- /// Hard cap on granted spells, matching the number of hotkey slots available.
- public const int MaxSpellSlots = 4;
-
- // ----- Static registry (mirrors BuilderUpgradeManager) -----
-
- private static readonly Dictionary s_byClientId
- = new Dictionary();
-
- /// Returns the loadout owned by the given client, or null.
- public static PlayerSpellLoadout GetForClient(ulong clientId)
- {
- s_byClientId.TryGetValue(clientId, out var loadout);
- return loadout;
- }
-
- /// Convenience: the local client's own loadout.
- public static PlayerSpellLoadout Local
- {
- get
- {
- var nm = NetworkManager.Singleton;
- if (nm == null || !nm.IsClient) return null;
- return GetForClient(nm.LocalClientId);
- }
- }
-
- // ----- Networked state --------------------------------------------
-
- private readonly NetworkList spells = new NetworkList();
-
- /// Fired on every peer when a spell is granted or a cooldown changes.
- public event System.Action OnLoadoutChanged;
-
- // ----- NGO lifecycle ------------------------------------------------
-
- public override void OnNetworkSpawn()
- {
- s_byClientId[OwnerClientId] = this;
- spells.OnListChanged += HandleSpellsChanged;
- }
-
- public override void OnNetworkDespawn()
- {
- spells.OnListChanged -= HandleSpellsChanged;
-
- if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
- s_byClientId.Remove(OwnerClientId);
- }
-
- private void HandleSpellsChanged(NetworkListEvent change) => OnLoadoutChanged?.Invoke();
-
- // ----- Read API -----------------------------------------------------
-
- /// Number of spells currently granted.
- public int SlotCount => spells.Count;
-
- /// True if this player has already been granted the given spell.
- public bool PlayerHasSpell(BuilderSpellKind kind)
- {
- for (int i = 0; i < spells.Count; i++)
- {
- if (spells[i].Kind == kind) return true;
- }
- return false;
- }
-
- /// The spell kind occupying , or null if out of range.
- public BuilderSpellKind? GetKind(int slot)
- => (slot >= 0 && slot < spells.Count) ? spells[slot].Kind : (BuilderSpellKind?)null;
-
- /// True if is out of range, empty, or still cooling down.
- public bool IsSlotOnCooldown(int slot)
- {
- if (slot < 0 || slot >= spells.Count) return true;
-
- var nm = NetworkManager.Singleton;
- if (nm == null) return false;
-
- return spells[slot].CooldownEndServerTime > nm.ServerTime.Time;
- }
-
- /// Seconds remaining on 's cooldown, or 0 if ready/invalid.
- public float GetCooldownRemaining(int slot)
- {
- if (slot < 0 || slot >= spells.Count) return 0f;
-
- var nm = NetworkManager.Singleton;
- if (nm == null) return 0f;
-
- double remaining = spells[slot].CooldownEndServerTime - nm.ServerTime.Time;
- return remaining > 0d ? (float)remaining : 0f;
- }
-
- // ----- Server mutation -----------------------------------------------
-
- ///
- /// Server-only: grants the given spell (a draft pick) into the next free slot.
- /// No-op if already granted or slots are full. Returns true if actually granted.
- ///
- public bool ServerGrantSpell(BuilderSpellKind kind)
- {
- if (!IsServer) return false;
- if (spells.Count >= MaxSpellSlots) return false;
- if (PlayerHasSpell(kind)) return false;
-
- spells.Add(SpellSlot.CreateReady(kind));
- return true;
- }
-
- // ----- Cast RPC -----------------------------------------------------
-
- ///
- /// Owning-client entry point. Requests the server resolve a cast of the spell in
- /// at . All validation happens
- /// here on the server; the client only uses this to trigger an attempt.
- ///
- [Rpc(SendTo.Server, RequireOwnership = true)]
- public void RequestCastSpellRpc(int slot, Vector3 targetPoint)
- {
- if (MatchState.Instance == null || MatchState.Instance.Phase != MatchPhase.Playing)
- return;
-
- if (slot < 0 || slot >= spells.Count) return;
-
- var slotValue = spells[slot];
- if (slotValue.CooldownEndServerTime > NetworkManager.ServerTime.Time) return;
-
- var definition = BuilderSpellPool.Instance?.Get(slotValue.Kind);
- if (definition == null)
- {
- Debug.LogWarning($"[PlayerSpellLoadout] No BuilderSpellDefinition for {slotValue.Kind}.");
- return;
- }
-
- if (!definition.ServerCast(OwnerClientId, targetPoint)) return;
-
- slotValue.CooldownEndServerTime = NetworkManager.ServerTime.Time + definition.Cooldown;
- spells[slot] = slotValue;
-
- PlayVfxClientRpc(slotValue.Kind, targetPoint);
- }
-
- [ClientRpc]
- private void PlayVfxClientRpc(BuilderSpellKind kind, Vector3 targetPoint)
- {
- BuilderSpellPool.Instance?.Get(kind)?.ClientPlayVfx(targetPoint);
- }
- }
-}
diff --git a/Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs.meta b/Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs.meta
deleted file mode 100644
index 559df0e..0000000
--- a/Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: dfa83208b02c50b40866bb1846240c4a
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/SpellHotkeys.cs b/Assets/_Project/Scripts/Gameplay/SpellHotkeys.cs
deleted file mode 100644
index 2f162d1..0000000
--- a/Assets/_Project/Scripts/Gameplay/SpellHotkeys.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Assets/_Project/Scripts/Gameplay/SpellHotkeys.cs
-using UnityEngine.InputSystem;
-
-namespace TD.Gameplay
-{
- ///
- /// The fixed key bound to each spell hotkey slot (slot index == grant order ==
- /// list index). Shared by
- /// (reads input) and the HUD hotbar (renders key
- /// labels) so the two can never drift apart.
- ///
- ///
- /// 's HotkeyLayout already claims the entire
- /// Q/W/E/R/T/A/S/D/F/G/Z/X/C/V/B block for the build command grid, so spell casting uses
- /// the number row instead.
- ///
- public static class SpellHotkeys
- {
- public static readonly Key[] Layout =
- {
- Key.Digit1,
- Key.Digit2,
- Key.Digit3,
- Key.Digit4,
- };
-
- public static int MaxSpellSlots => Layout.Length;
- }
-}
diff --git a/Assets/_Project/Scripts/Gameplay/SpellHotkeys.cs.meta b/Assets/_Project/Scripts/Gameplay/SpellHotkeys.cs.meta
deleted file mode 100644
index 62da374..0000000
--- a/Assets/_Project/Scripts/Gameplay/SpellHotkeys.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 360482837f8c85d5abe29870dcf9cf1a
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/SpellSlot.cs b/Assets/_Project/Scripts/Gameplay/SpellSlot.cs
deleted file mode 100644
index c84e687..0000000
--- a/Assets/_Project/Scripts/Gameplay/SpellSlot.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-// Assets/_Project/Scripts/Gameplay/SpellSlot.cs
-using System;
-using Unity.Netcode;
-using TD.Core;
-
-namespace TD.Gameplay
-{
- ///
- /// One granted builder spell in a . Replicated as part of
- /// a ; the entry's index in that list IS the hotkey slot
- /// (grant order == key order), so no separate slot index field is needed.
- ///
- ///
- /// One struct, not parallel lists. is set once at grant time;
- /// is rewritten on every cast. NetworkList{T}'s
- /// indexer setter (spells[i] = newValue) short-circuits the write when
- /// Equals on the old value returns true (see for the
- /// documented case that first surfaced this). Comparing only would mean
- /// a cooldown update — same Kind, new CooldownEndServerTime — never actually replicates.
- /// therefore compares every field.
- ///
- [Serializable]
- public struct SpellSlot : INetworkSerializable, IEquatable
- {
- /// Which spell occupies this slot. Fixed for the slot's lifetime.
- public BuilderSpellKind Kind;
-
- ///
- /// NetworkManager.ServerTime.Time at which this slot is next ready to cast.
- /// 0 (or any value <= current server time) means ready now.
- ///
- public double CooldownEndServerTime;
-
- public static SpellSlot CreateReady(BuilderSpellKind kind)
- {
- return new SpellSlot { Kind = kind, CooldownEndServerTime = 0d };
- }
-
- // ----- INetworkSerializable ---------------------------------------
-
- public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
- {
- byte kindByte = (byte)Kind;
- serializer.SerializeValue(ref kindByte);
- Kind = (BuilderSpellKind)kindByte;
-
- serializer.SerializeValue(ref CooldownEndServerTime);
- }
-
- // ----- IEquatable -------------------------------------------------
- //
- // Full-field comparison — see remarks above. Without this, cooldown writes
- // (Kind unchanged, only CooldownEndServerTime updated) would be silently dropped
- // by NetworkList's indexer setter.
-
- public bool Equals(SpellSlot other) =>
- Kind == other.Kind && CooldownEndServerTime == other.CooldownEndServerTime;
-
- public override bool Equals(object obj) => obj is SpellSlot other && Equals(other);
-
- public override int GetHashCode() => (int)Kind;
- }
-}
diff --git a/Assets/_Project/Scripts/Gameplay/SpellSlot.cs.meta b/Assets/_Project/Scripts/Gameplay/SpellSlot.cs.meta
deleted file mode 100644
index d9c0bee..0000000
--- a/Assets/_Project/Scripts/Gameplay/SpellSlot.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 9c8fdabdb3c89204ab28f9e031678db4
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/UI/HUDController.cs b/Assets/_Project/Scripts/UI/HUDController.cs
index 504ac50..96f50ec 100644
--- a/Assets/_Project/Scripts/UI/HUDController.cs
+++ b/Assets/_Project/Scripts/UI/HUDController.cs
@@ -8,7 +8,6 @@ using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
using TD.Core;
using TD.Gameplay;
-using TD.Gameplay.BuilderSpells;
using TD.Gameplay.Draft;
using TD.Towers;
using TD.UI.Minimap;
@@ -116,27 +115,6 @@ namespace TD.UI
private bool draftSubscribed;
private PlayerDraft subscribedDraft;
- // Spell hotbar (bottom-ui Section 6). Frame is display:none while the local player
- // has no granted spells; rebuilt on grant (append-only, so this is rare). Cooldown
- // state changes continuously and has no change event, so it's polled every Update.
- private VisualElement spellHotbarFrame;
- private VisualElement spellHotbar;
- private bool spellLoadoutSubscribed;
- private PlayerSpellLoadout subscribedSpellLoadout;
- private readonly List spellSlotUis = new List();
-
- private readonly struct SpellSlotUi
- {
- public readonly VisualElement Cell;
- public readonly Label CooldownLabel;
-
- public SpellSlotUi(VisualElement cell, Label cooldownLabel)
- {
- Cell = cell;
- CooldownLabel = cooldownLabel;
- }
- }
-
// Chat panel (bottom-left, above portrait) — programmatic. The container
// holds both the scrollable feed and the input. Highlight + scroll
// interactivity are toggled on the container when typing.
@@ -316,23 +294,6 @@ namespace TD.UI
RebuildDraftCards();
}
- // Hook the local player's spell loadout so the hotbar rebuilds when a new spell is
- // granted. Retried each Update until the local PlayerSpellLoadout exists.
- private void TrySubscribeSpellLoadout()
- {
- var loadout = PlayerSpellLoadout.Local;
- if (loadout == null) return;
- loadout.OnLoadoutChanged += HandleSpellLoadoutChanged;
- subscribedSpellLoadout = loadout;
- spellLoadoutSubscribed = true;
- RebuildSpellHotbar();
- }
-
- private void HandleSpellLoadoutChanged()
- {
- RebuildSpellHotbar();
- }
-
// ----- Draft overlay ----------------------------------------------
// Non-modal draft UI: a card row floats at top-center while a draft is active, and
@@ -473,103 +434,6 @@ namespace TD.UI
draftPanel.style.display = (hasDraft || canShowBuy) ? DisplayStyle.Flex : DisplayStyle.None;
}
- // ----- Spell hotbar -------------------------------------------------
-
- // Rebuilds the hotbar cells from the local player's current loadout. Called on
- // OnLoadoutChanged (a new spell granted) — spells are append-only, so this is rare,
- // not a per-frame concern.
- private void RebuildSpellHotbar()
- {
- if (spellHotbar == null) return;
- spellHotbar.Clear();
- spellSlotUis.Clear();
-
- var loadout = PlayerSpellLoadout.Local;
- int slotCount = loadout?.SlotCount ?? 0;
-
- if (spellHotbarFrame != null)
- spellHotbarFrame.style.display = slotCount > 0 ? DisplayStyle.Flex : DisplayStyle.None;
-
- if (loadout == null) return;
-
- var layout = SpellHotkeys.Layout;
- for (int i = 0; i < slotCount; i++)
- {
- var kind = loadout.GetKind(i);
- if (kind == null) continue;
-
- var definition = BuilderSpellPool.Instance?.Get(kind.Value);
-
- var cell = new VisualElement();
- cell.AddToClassList("spell-slot");
-
- var icon = new VisualElement();
- icon.AddToClassList("spell-slot-icon");
- icon.pickingMode = PickingMode.Ignore;
- if (definition?.Icon != null)
- icon.style.backgroundImage = new StyleBackground(definition.Icon);
- cell.Add(icon);
-
- if (i < layout.Length)
- {
- var hkLabel = new Label(SpellKeyToDisplay(layout[i]));
- hkLabel.AddToClassList("spell-slot-hotkey");
- hkLabel.pickingMode = PickingMode.Ignore;
- cell.Add(hkLabel);
- }
-
- var cooldownLabel = new Label("");
- cooldownLabel.AddToClassList("spell-slot-cooldown-label");
- cooldownLabel.pickingMode = PickingMode.Ignore;
- cell.Add(cooldownLabel);
-
- cell.RegisterCallback(_ => ShowSpellTooltip(definition));
- cell.RegisterCallback(_ => ClearTooltip());
-
- spellHotbar.Add(cell);
- spellSlotUis.Add(new SpellSlotUi(cell, cooldownLabel));
- }
- }
-
- // Per-frame: dims each slot while on cooldown and shows the remaining whole seconds.
- // Cooldown has no change event (it advances continuously with server time), so this
- // has to be polled — cheap at hotbar scale (at most MaxSpellSlots cells).
- private void UpdateSpellCooldowns()
- {
- if (spellSlotUis.Count == 0) return;
-
- var loadout = PlayerSpellLoadout.Local;
- if (loadout == null) return;
-
- for (int i = 0; i < spellSlotUis.Count; i++)
- {
- var ui = spellSlotUis[i];
- bool onCooldown = loadout.IsSlotOnCooldown(i);
- ui.Cell.EnableInClassList("on-cooldown", onCooldown);
- ui.CooldownLabel.text = onCooldown
- ? $"{Mathf.CeilToInt(loadout.GetCooldownRemaining(i))}"
- : "";
- }
- }
-
- // Renders a spell hotkey as its display glyph. SpellHotkeys.Layout uses Key.DigitN,
- // whose ToString() is "DigitN" — strip the prefix so the badge shows "1", not "Digit1".
- private static string SpellKeyToDisplay(Key key)
- {
- string s = key.ToString();
- return s.StartsWith("Digit") ? s.Substring("Digit".Length) : s;
- }
-
- // Lightweight tooltip for spell slots — reuses the tooltip box (title + desc + cooldown).
- private void ShowSpellTooltip(BuilderSpellDefinition def)
- {
- if (ttTitle == null || def == null) return;
- ttTitle.text = def.DisplayName;
- ttDesc.text = def.Description ?? "";
- ttStats.text = $"Cooldown: {def.Cooldown:0.#}s";
- ttCost.text = "";
- }
-
private void InitializeUI()
{
var doc = GetComponent();
@@ -617,8 +481,6 @@ namespace TD.UI
ttStats = Require