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