diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..910fa09
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,8 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(Get-ChildItem -Path \"C:\\\\Users\\\\catos\\\\UnityTowerDefense\\\\Assets\\\\Scripts\" -Recurse -File -Filter \"*.cs\")",
+ "Bash(Select-Object -ExpandProperty FullName)"
+ ]
+ }
+}
diff --git a/Assets/DefaultNetworkPrefabs.asset b/Assets/DefaultNetworkPrefabs.asset
index 31969b6..82b70f9 100644
--- a/Assets/DefaultNetworkPrefabs.asset
+++ b/Assets/DefaultNetworkPrefabs.asset
@@ -39,3 +39,13 @@ MonoBehaviour:
SourcePrefabToOverride: {fileID: 0}
SourceHashToOverride: 0
OverridingTargetPrefab: {fileID: 0}
+ - Override: 0
+ Prefab: {fileID: 1455822126534880203, guid: 0854f339a1958d343a6cb16cd3f907ff, type: 3}
+ SourcePrefabToOverride: {fileID: 0}
+ SourceHashToOverride: 0
+ OverridingTargetPrefab: {fileID: 0}
+ - Override: 0
+ Prefab: {fileID: 2664719039363295382, guid: dc2e4a4108e03874a8b2dab88dcc8fba, type: 3}
+ SourcePrefabToOverride: {fileID: 0}
+ SourceHashToOverride: 0
+ OverridingTargetPrefab: {fileID: 0}
diff --git a/Assets/Resources.meta b/Assets/_Project/Data.meta
similarity index 100%
rename from Assets/Resources.meta
rename to Assets/_Project/Data.meta
diff --git a/Assets/Resources/TowerDefinitions.meta b/Assets/_Project/Data/TowerDefinitions.meta
similarity index 100%
rename from Assets/Resources/TowerDefinitions.meta
rename to Assets/_Project/Data/TowerDefinitions.meta
diff --git a/Assets/Resources/TowerDefinitions/BasicTower.asset b/Assets/_Project/Data/TowerDefinitions/BasicTower.asset
similarity index 66%
rename from Assets/Resources/TowerDefinitions/BasicTower.asset
rename to Assets/_Project/Data/TowerDefinitions/BasicTower.asset
index 02ff1a6..12cf1f5 100644
--- a/Assets/Resources/TowerDefinitions/BasicTower.asset
+++ b/Assets/_Project/Data/TowerDefinitions/BasicTower.asset
@@ -18,9 +18,19 @@ MonoBehaviour:
GoldCost: 25
BuildTime: 4
TowerPrefab: {fileID: 6482414459531823157, guid: 1511641f145758b469e64376d2a0d434, type: 3}
- Damage: 0
- Range: 0
- FireRate: 0
+ DamageType: 0
+ TargetPriority: 0
+ TargetType: 0
+ GroundedOnly: 0
+ Damage: 10
+ Range: 20
+ FireRate: 5
SplashRadius: 0
- SlowFactor: 1
- ProjectilePrefab: {fileID: 0}
+ ChainCount: 0
+ ChainRange: 0
+ SlowFactor: 0
+ DotDamagePerSecond: 0
+ EffectDuration: 0
+ ProjectilePrefab: {fileID: 2664719039363295382, guid: dc2e4a4108e03874a8b2dab88dcc8fba, type: 3}
+ ProjectileSpeed: 10
+ UpgradePaths: []
diff --git a/Assets/Resources/TowerDefinitions/BasicTower.asset.meta b/Assets/_Project/Data/TowerDefinitions/BasicTower.asset.meta
similarity index 100%
rename from Assets/Resources/TowerDefinitions/BasicTower.asset.meta
rename to Assets/_Project/Data/TowerDefinitions/BasicTower.asset.meta
diff --git a/Assets/_Project/Prefabs/Enemies/EnemyPlaceholder.prefab b/Assets/_Project/Prefabs/Enemies/EnemyPlaceholder.prefab
new file mode 100644
index 0000000..5cd3c2d
--- /dev/null
+++ b/Assets/_Project/Prefabs/Enemies/EnemyPlaceholder.prefab
@@ -0,0 +1,216 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &1455822126534880203
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 3374291962137961512}
+ - component: {fileID: 6900825049490657874}
+ - component: {fileID: 3838554347222657907}
+ - component: {fileID: 2827184357573667590}
+ - component: {fileID: 3180894108125880274}
+ - component: {fileID: 5830540397649648793}
+ - component: {fileID: 2892684246239657319}
+ - component: {fileID: 8213527798879671990}
+ m_Layer: 10
+ m_Name: EnemyPlaceholder
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &3374291962137961512
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1455822126534880203}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 19.2967, y: 0.5, z: 17.39775}
+ 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!33 &6900825049490657874
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1455822126534880203}
+ m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!23 &3838554347222657907
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1455822126534880203}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!65 &2827184357573667590
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1455822126534880203}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 1, y: 1, z: 1}
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!114 &3180894108125880274
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1455822126534880203}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
+ GlobalObjectIdHash: 4022812445
+ InScenePlacedSourceGlobalObjectIdHash: 0
+ DeferredDespawnTick: 0
+ Ownership: 1
+ AlwaysReplicateAsRoot: 0
+ SynchronizeTransform: 1
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 0
+ SpawnWithObservers: 1
+ DontDestroyWithOwner: 0
+ AutoObjectParentSync: 1
+ SyncOwnerTransformWhenParented: 1
+ AllowOwnerToParent: 0
+--- !u!114 &5830540397649648793
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1455822126534880203}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: e96cb6065543e43c4a752faaa1468eb1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Components.NetworkTransform
+ ShowTopMostFoldoutHeaderGroup: 1
+ NetworkTransformExpanded: 0
+ AutoOwnerAuthorityTickOffset: 1
+ PositionInterpolationType: 0
+ RotationInterpolationType: 0
+ ScaleInterpolationType: 0
+ PositionLerpSmoothing: 1
+ PositionMaxInterpolationTime: 0.1
+ RotationLerpSmoothing: 1
+ RotationMaxInterpolationTime: 0.1
+ ScaleLerpSmoothing: 1
+ ScaleMaxInterpolationTime: 0.1
+ AuthorityMode: 0
+ TickSyncChildren: 0
+ UseUnreliableDeltas: 0
+ SyncPositionX: 1
+ SyncPositionY: 1
+ SyncPositionZ: 1
+ SyncRotAngleX: 1
+ SyncRotAngleY: 1
+ SyncRotAngleZ: 1
+ SyncScaleX: 1
+ SyncScaleY: 1
+ SyncScaleZ: 1
+ PositionThreshold: 0.001
+ RotAngleThreshold: 0.01
+ ScaleThreshold: 0.01
+ UseQuaternionSynchronization: 0
+ UseQuaternionCompression: 0
+ UseHalfFloatPrecision: 0
+ InLocalSpace: 0
+ SwitchTransformSpaceWhenParented: 0
+ Interpolate: 1
+ SlerpPosition: 0
+--- !u!114 &2892684246239657319
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1455822126534880203}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 21dea26768768b8449a8924f638557be, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyHealth
+ ShowTopMostFoldoutHeaderGroup: 1
+ maxHp: 100
+--- !u!114 &8213527798879671990
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1455822126534880203}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 292d1b92cd49dc74f8dfd74cdbe4ece7, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyStatus
+ ShowTopMostFoldoutHeaderGroup: 1
diff --git a/Assets/_Project/Prefabs/Enemies/EnemyPlaceholder.prefab.meta b/Assets/_Project/Prefabs/Enemies/EnemyPlaceholder.prefab.meta
new file mode 100644
index 0000000..815a1e2
--- /dev/null
+++ b/Assets/_Project/Prefabs/Enemies/EnemyPlaceholder.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0854f339a1958d343a6cb16cd3f907ff
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/Projectiles/ProjectilePlaceholder.prefab b/Assets/_Project/Prefabs/Projectiles/ProjectilePlaceholder.prefab
new file mode 100644
index 0000000..1d664d8
--- /dev/null
+++ b/Assets/_Project/Prefabs/Projectiles/ProjectilePlaceholder.prefab
@@ -0,0 +1,203 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &2664719039363295382
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 5346505748924138806}
+ - component: {fileID: 2441523409650755752}
+ - component: {fileID: 3004458703758247920}
+ - component: {fileID: 7079697957481585344}
+ - component: {fileID: 6722677248442714376}
+ - component: {fileID: 3956702135205686426}
+ - component: {fileID: 6538425601827344639}
+ m_Layer: 0
+ m_Name: ProjectilePlaceholder
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &5346505748924138806
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2664719039363295382}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 0.2, y: 0.2, z: 0.2}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0}
+--- !u!33 &2441523409650755752
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2664719039363295382}
+ m_Mesh: {fileID: 10208, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!23 &3004458703758247920
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2664719039363295382}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!136 &7079697957481585344
+CapsuleCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2664719039363295382}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 2
+ m_Radius: 0.5
+ m_Height: 2
+ m_Direction: 1
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!114 &6722677248442714376
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2664719039363295382}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
+ GlobalObjectIdHash: 0
+ InScenePlacedSourceGlobalObjectIdHash: 0
+ DeferredDespawnTick: 0
+ Ownership: 1
+ AlwaysReplicateAsRoot: 0
+ SynchronizeTransform: 1
+ ActiveSceneSynchronization: 0
+ SceneMigrationSynchronization: 1
+ SpawnWithObservers: 1
+ DontDestroyWithOwner: 0
+ AutoObjectParentSync: 1
+ SyncOwnerTransformWhenParented: 1
+ AllowOwnerToParent: 0
+--- !u!114 &3956702135205686426
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2664719039363295382}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: e96cb6065543e43c4a752faaa1468eb1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Components.NetworkTransform
+ ShowTopMostFoldoutHeaderGroup: 1
+ NetworkTransformExpanded: 0
+ AutoOwnerAuthorityTickOffset: 1
+ PositionInterpolationType: 0
+ RotationInterpolationType: 0
+ ScaleInterpolationType: 0
+ PositionLerpSmoothing: 1
+ PositionMaxInterpolationTime: 0.1
+ RotationLerpSmoothing: 1
+ RotationMaxInterpolationTime: 0.1
+ ScaleLerpSmoothing: 1
+ ScaleMaxInterpolationTime: 0.1
+ AuthorityMode: 0
+ TickSyncChildren: 0
+ UseUnreliableDeltas: 0
+ SyncPositionX: 1
+ SyncPositionY: 1
+ SyncPositionZ: 1
+ SyncRotAngleX: 1
+ SyncRotAngleY: 1
+ SyncRotAngleZ: 1
+ SyncScaleX: 1
+ SyncScaleY: 1
+ SyncScaleZ: 1
+ PositionThreshold: 0.001
+ RotAngleThreshold: 0.01
+ ScaleThreshold: 0.01
+ UseQuaternionSynchronization: 0
+ UseQuaternionCompression: 0
+ UseHalfFloatPrecision: 0
+ InLocalSpace: 0
+ SwitchTransformSpaceWhenParented: 0
+ Interpolate: 1
+ SlerpPosition: 0
+--- !u!114 &6538425601827344639
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2664719039363295382}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a6854f71c9fdcda42b297c397f96c8be, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Combat.Projectile
+ ShowTopMostFoldoutHeaderGroup: 1
diff --git a/Assets/_Project/Prefabs/Projectiles/ProjectilePlaceholder.prefab.meta b/Assets/_Project/Prefabs/Projectiles/ProjectilePlaceholder.prefab.meta
new file mode 100644
index 0000000..24f2981
--- /dev/null
+++ b/Assets/_Project/Prefabs/Projectiles/ProjectilePlaceholder.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: dc2e4a4108e03874a8b2dab88dcc8fba
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/Towers/Tower_Basic.prefab b/Assets/_Project/Prefabs/Towers/Tower_Basic.prefab
index 511931b..42defa0 100644
--- a/Assets/_Project/Prefabs/Towers/Tower_Basic.prefab
+++ b/Assets/_Project/Prefabs/Towers/Tower_Basic.prefab
@@ -67,6 +67,8 @@ GameObject:
- component: {fileID: 5594214090440991794}
- component: {fileID: 7630870068340451557}
- component: {fileID: 9137031893466587143}
+ - component: {fileID: 805962841523123163}
+ - component: {fileID: 8853488620519990682}
m_Layer: 0
m_Name: Tower_Basic
m_TagString: Untagged
@@ -88,6 +90,7 @@ Transform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5753294230586596248}
+ - {fileID: 7547850274173754846}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &6869333096494165105
@@ -208,3 +211,94 @@ MonoBehaviour:
ShowTopMostFoldoutHeaderGroup: 1
tintedRenderers:
- {fileID: 4028055828417179692}
+--- !u!114 &805962841523123163
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6482414459531823157}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 7eb6cce6cd96b23478b7d2173cebf74d, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Combat.TowerCombat
+ ShowTopMostFoldoutHeaderGroup: 1
+ enemyLayerMask:
+ serializedVersion: 2
+ m_Bits: 1024
+--- !u!114 &8853488620519990682
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6482414459531823157}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: c0edb0c5206ca454bbd7c300c6cc7574, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Combat.TowerRangeIndicator
+ rangeProjector: {fileID: 8255517343120954594}
+ projectionDepth: 50
+--- !u!1 &7580197837852108944
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 7547850274173754846}
+ - component: {fileID: 8255517343120954594}
+ m_Layer: 0
+ m_Name: RangeIndicator
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &7547850274173754846
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7580197837852108944}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1283036264165444500}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &8255517343120954594
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7580197837852108944}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 0777d029ed3dffa4692f417d4aba19ca, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.DecalProjector
+ m_Material: {fileID: 2100000, guid: f99227cbde481ce47a2527e6bca709d2, type: 2}
+ m_DrawDistance: 1000
+ m_FadeScale: 0.9
+ m_StartAngleFade: 180
+ m_EndAngleFade: 180
+ m_UVScale: {x: 1, y: 1}
+ m_UVBias: {x: 0, y: 0}
+ m_RenderingLayerMask:
+ serializedVersion: 0
+ m_Bits: 1
+ m_ScaleMode: 0
+ m_Offset: {x: 0, y: 0, z: 0.5}
+ m_Size: {x: 1, y: 1, z: 1}
+ m_FadeFactor: 1
+ m_VisibleInScene: 1
+ version: 1
+ m_DecalLayerMask: 1
diff --git a/Assets/_Project/Scenes/Levels/Main.unity b/Assets/_Project/Scenes/Levels/Main.unity
index d4be64c..5e9d01f 100644
--- a/Assets/_Project/Scenes/Levels/Main.unity
+++ b/Assets/_Project/Scenes/Levels/Main.unity
@@ -1715,7 +1715,6 @@ GameObject:
m_Component:
- component: {fileID: 1597884409}
- component: {fileID: 1597884411}
- - component: {fileID: 1597884410}
m_Layer: 0
m_Name: TowerPlacementController
m_TagString: Untagged
@@ -1738,21 +1737,6 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!114 &1597884410
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1597884408}
- m_Enabled: 0
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: ea0e3a4681be19e4e9c359c1123bf68d, type: 3}
- m_Name:
- m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.TowerPlacementTestTrigger
- towerToPlace: {fileID: 11400000, guid: 0f693e29ca953e1439e10cb8f12e4b30, type: 2}
- towerTypeId: 1
- controller: {fileID: 1597884411}
--- !u!114 &1597884411
MonoBehaviour:
m_ObjectHideFlags: 0
diff --git a/Assets/_Project/Scripts/Combat.meta b/Assets/_Project/Scripts/Combat.meta
new file mode 100644
index 0000000..6aca6f9
--- /dev/null
+++ b/Assets/_Project/Scripts/Combat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 07a063da1d1c0b549b61d11d723e2930
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Combat/Projectile.cs b/Assets/_Project/Scripts/Combat/Projectile.cs
new file mode 100644
index 0000000..a645963
--- /dev/null
+++ b/Assets/_Project/Scripts/Combat/Projectile.cs
@@ -0,0 +1,177 @@
+// Assets/_Project/Scripts/Combat/Projectile.cs
+using Unity.Netcode;
+using UnityEngine;
+using TD.Core;
+using TD.Gameplay;
+
+namespace TD.Combat
+{
+ ///
+ /// A traveling projectile spawned by when a tower has
+ /// a non-null ProjectilePrefab configured.
+ ///
+ ///
+ /// Authority: Movement and hit detection run server-only.
+ /// NetworkTransform (required on the prefab) replicates the position to
+ /// clients so the projectile is visible on all peers.
+ ///
+ /// Initialization: Mirrors the TowerInstance.InitializeServer pattern —
+ /// is called by TowerCombat immediately after
+ /// Instantiate and before NetworkObject.Spawn(), which avoids writing
+ /// to NetworkVariables before spawn.
+ ///
+ /// Target loss: If the target dies or is destroyed before the projectile
+ /// arrives, the projectile despawns silently (no hit, no damage).
+ ///
+ /// Chain + Projectile: By design, TargetType.Chain is hitscan. If a designer
+ /// sets TargetType = Chain on a tower that has a ProjectilePrefab, the projectile
+ /// will hit the primary target only and ignore the chain. Log a warning to surface
+ /// the misconfiguration.
+ ///
+ /// Prefab requirements: Must have NetworkObject, NetworkTransform,
+ /// and this Projectile component at the root.
+ ///
+ [RequireComponent(typeof(NetworkObject))]
+ public class Projectile : NetworkBehaviour
+ {
+ // Hit threshold: squared distance at which the projectile considers itself
+ // to have reached the target. 0.09 = 0.3 world units; small enough to
+ // feel accurate, large enough to survive a high-speed frame where the
+ // projectile could skip past the target's transform in one step.
+ private const float HitThresholdSq = 0.09f;
+
+ // All fields are server-local. Set by InitializeServer before Spawn.
+ private EnemyHealth target;
+ private float damage;
+ private DamageType damageType;
+ private TargetType targetType;
+ private float splashRadius;
+ private float slowFactor;
+ private float dotDamagePerSecond;
+ private float effectDuration;
+ private float speed;
+ private LayerMask enemyLayerMask;
+ private bool initialized;
+
+ // Shared with TowerCombat's overlap calls. Both components run on the
+ // server main thread so there is no concurrent access.
+ private static readonly Collider[] s_overlapBuffer = new Collider[32];
+
+ // ----- Initialization (server-only, called before Spawn) -----------
+
+ ///
+ /// Stores all data this projectile needs to travel and apply damage.
+ /// Call this immediately after Instantiate and before
+ /// NetworkObject.Spawn().
+ ///
+ public void InitializeServer(
+ EnemyHealth target,
+ float damage,
+ DamageType damageType,
+ TargetType targetType,
+ float splashRadius,
+ float slowFactor,
+ float dotDamagePerSecond,
+ float effectDuration,
+ float speed,
+ LayerMask enemyLayerMask)
+ {
+ this.target = target;
+ this.damage = damage;
+ this.damageType = damageType;
+ this.targetType = targetType;
+ this.splashRadius = splashRadius;
+ this.slowFactor = slowFactor;
+ this.dotDamagePerSecond = dotDamagePerSecond;
+ this.effectDuration = effectDuration;
+ this.speed = speed;
+ this.enemyLayerMask = enemyLayerMask;
+ initialized = true;
+
+ if (targetType == TargetType.Chain)
+ Debug.LogWarning("[Projectile] TargetType.Chain is hitscan-only. " +
+ "This projectile will hit the primary target only. " +
+ "Consider using hitscan (null ProjectilePrefab) for chain towers.");
+ }
+
+ // ----- Server movement + hit detection -----------------------------
+
+ private void Update()
+ {
+ if (!IsServer || !initialized) return;
+
+ // Target gone — silently despawn, no damage applied.
+ if (target == null
+ || target.IsDead
+ || (target as UnityEngine.Object) == null)
+ {
+ NetworkObject.Despawn();
+ return;
+ }
+
+ Vector3 toTarget = target.transform.position - transform.position;
+
+ if (toTarget.sqrMagnitude <= HitThresholdSq)
+ {
+ ApplyHit();
+ NetworkObject.Despawn();
+ return;
+ }
+
+ // Rotate to face the target so the projectile mesh looks correct
+ // on all clients (NetworkTransform replicates both position and rotation).
+ transform.rotation = Quaternion.LookRotation(toTarget);
+ transform.position += toTarget.normalized * (speed * Time.deltaTime);
+ }
+
+ // ----- Hit application ---------------------------------------------
+
+ private void ApplyHit()
+ {
+ switch (targetType)
+ {
+ case TargetType.Single:
+ case TargetType.Chain: // chain falls back to single-target on projectiles
+ HitEnemy(target);
+ break;
+
+ case TargetType.Splash:
+ HitEnemy(target);
+ if (splashRadius > 0f)
+ {
+ int count = Physics.OverlapSphereNonAlloc(
+ transform.position, splashRadius,
+ s_overlapBuffer, enemyLayerMask);
+
+ for (int i = 0; i < count; i++)
+ {
+ var eh = s_overlapBuffer[i].GetComponent();
+ if (eh == null || eh.IsDead || (object)eh == (object)target) continue;
+ HitEnemy(eh);
+ }
+ }
+ break;
+ }
+ }
+
+ private void HitEnemy(EnemyHealth eh)
+ {
+ eh.TakeDamage(damage, damageType);
+
+ if (effectDuration > 0f)
+ {
+ float magnitude = damageType switch
+ {
+ DamageType.Cold => slowFactor,
+ DamageType.Fire => dotDamagePerSecond,
+ DamageType.Poison => dotDamagePerSecond,
+ _ => 0f,
+ };
+
+ if (magnitude > 0f)
+ eh.GetComponent()
+ ?.ApplyEffect(damageType, magnitude, effectDuration);
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Combat/Projectile.cs.meta b/Assets/_Project/Scripts/Combat/Projectile.cs.meta
new file mode 100644
index 0000000..61b0f3e
--- /dev/null
+++ b/Assets/_Project/Scripts/Combat/Projectile.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a6854f71c9fdcda42b297c397f96c8be
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Combat/TowerCombat.cs b/Assets/_Project/Scripts/Combat/TowerCombat.cs
new file mode 100644
index 0000000..8d5d6fb
--- /dev/null
+++ b/Assets/_Project/Scripts/Combat/TowerCombat.cs
@@ -0,0 +1,409 @@
+// Assets/_Project/Scripts/Combat/TowerCombat.cs
+using System.Collections.Generic;
+using Unity.Netcode;
+using UnityEngine;
+using TD.Core;
+using TD.Gameplay;
+using TD.Towers;
+
+namespace TD.Combat
+{
+ ///
+ /// Per-tower combat controller. Handles target acquisition, attack timing,
+ /// damage application, and projectile spawning.
+ ///
+ ///
+ /// Authority: All combat logic (targeting, damage, projectile spawn) runs
+ /// on the server only. Clients receive two signals for visual feedback:
+ ///
+ /// - — NetworkVariable clients can read to know
+ /// what the tower is aiming at (drives future rotation/lean visuals).
+ /// - — one-shot broadcast at each attack, carrying
+ /// the target world position for muzzle flash, tracer FX, etc.
+ ///
+ ///
+ /// Component coupling: Reads all stats from
+ /// on the same GameObject. Does not modify TowerInstance.
+ ///
+ /// Inspector setup required:
+ ///
+ /// - Assign to the "Enemy" physics layer.
+ ///
+ ///
+ [RequireComponent(typeof(TowerInstance))]
+ public class TowerCombat : NetworkBehaviour
+ {
+ [Tooltip("Physics layer(s) that enemies occupy. " +
+ "OverlapSphere uses this to find targets efficiently without " +
+ "touching non-enemy colliders.")]
+ [SerializeField] private LayerMask enemyLayerMask;
+
+ // Replicated so all peers know the current aim target.
+ // Visual consumers (barrel rotation, etc.) subscribe to OnValueChanged.
+ // Default (NetworkObjectId = 0) = no target.
+ private readonly NetworkVariable replicatedTarget =
+ new NetworkVariable(
+ default,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server);
+
+ // Server-local reference — cleared when the target dies or leaves range.
+ // Clients should use replicatedTarget.Value instead.
+ private EnemyHealth currentTarget;
+
+ // Attack cooldown decrements each server frame. When ≤ 0 and a target
+ // is in range, the tower fires and the timer resets to 1 / FireRate.
+ private float attackCooldown;
+
+ // Cached on OnNetworkSpawn — avoids GetComponent every Update.
+ private TowerInstance towerInstance;
+
+ // Shared OverlapSphere result buffer. 32 covers any realistic enemy
+ // density; size up if profiling reveals overflow.
+ private static readonly Collider[] s_overlapBuffer = new Collider[32];
+
+ // ----- Public API --------------------------------------------------
+
+ ///
+ /// Server-local reference to the current attack target.
+ /// Null on clients — use there.
+ ///
+ public EnemyHealth CurrentTarget => currentTarget;
+
+ ///
+ /// Fired locally on ALL peers when the tower acquires a new target.
+ /// Driven by .OnValueChanged, and also
+ /// fired in for late-joining clients so visual
+ /// consumers always initialise from the correct state.
+ ///
+ public event System.Action OnTargetAcquired;
+
+ /// Fired locally on ALL peers when the tower loses its target.
+ public event System.Action OnTargetLost;
+
+ ///
+ /// Fired locally on ALL peers each time the tower attacks.
+ /// Argument is the world-space position of the primary target at the moment
+ /// of fire — use it to aim muzzle flash, spawn a tracer, etc.
+ ///
+ public event System.Action OnFire;
+
+ // ----- NGO lifecycle -----------------------------------------------
+
+ public override void OnNetworkSpawn()
+ {
+ towerInstance = GetComponent();
+ replicatedTarget.OnValueChanged += HandleReplicatedTargetChanged;
+
+ // Late-joining clients receive the NV's current value in the initial
+ // sync message but won't get an OnValueChanged callback for it.
+ // Fire OnTargetAcquired here so visual consumers initialise correctly
+ // regardless of when this client connected.
+ if (replicatedTarget.Value.TryGet(out _))
+ OnTargetAcquired?.Invoke(replicatedTarget.Value);
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ replicatedTarget.OnValueChanged -= HandleReplicatedTargetChanged;
+
+ // Unsubscribe from the current target's death event to prevent
+ // a dangling delegate on the enemy after this tower despawns.
+ if (currentTarget != null)
+ currentTarget.OnDied -= HandleTargetDied;
+ }
+
+ // ----- Server update -----------------------------------------------
+
+ private void Update()
+ {
+ if (!IsServer) return;
+
+ TowerDefinition def = towerInstance?.Definition;
+ if (def == null || def.Range <= 0f || def.FireRate <= 0f) return;
+
+ // Only fire during active gameplay.
+ var ms = MatchState.Instance;
+ if (ms == null || ms.Phase != MatchPhase.Playing)
+ {
+ if (currentTarget != null) ClearTarget();
+ return;
+ }
+
+ ValidateTarget(def);
+ if (currentTarget == null)
+ AcquireTarget(def);
+ if (currentTarget != null)
+ TickAttack(def);
+ }
+
+ // ----- Target management -------------------------------------------
+
+ private void ValidateTarget(TowerDefinition def)
+ {
+ if (currentTarget == null) return;
+
+ bool dead = currentTarget.IsDead;
+ bool destroyed = (currentTarget as UnityEngine.Object) == null;
+ bool outOfRange = Vector3.Distance(
+ transform.position,
+ currentTarget.transform.position) > def.Range;
+
+ if (dead || destroyed || outOfRange)
+ ClearTarget();
+ }
+
+ private void AcquireTarget(TowerDefinition def)
+ {
+ int count = Physics.OverlapSphereNonAlloc(
+ transform.position, def.Range, s_overlapBuffer, enemyLayerMask);
+
+ EnemyHealth best = null;
+ float bestScore = 0f;
+
+ for (int i = 0; i < count; i++)
+ {
+ var eh = s_overlapBuffer[i].GetComponent();
+ if (eh == null || eh.IsDead) continue;
+ if (def.GroundedOnly && eh.IsFlying) continue;
+
+ float score = ScoreTarget(eh, def.TargetPriority);
+ bool isBetter = best == null
+ || (def.TargetPriority == TargetPriority.Strongest
+ ? score > bestScore // higher HP wins
+ : score < bestScore); // lower score wins for Closest/Weakest
+
+ if (isBetter) { best = eh; bestScore = score; }
+ }
+
+ if (best != null)
+ SetTarget(best);
+ }
+
+ // Returns a scalar used to rank candidates.
+ // Closest → sqrMagnitude (lower = better).
+ // Weakest → CurrentHp (lower = better).
+ // Strongest → CurrentHp (higher = better — caller inverts the comparison).
+ private float ScoreTarget(EnemyHealth eh, TargetPriority priority)
+ {
+ return priority switch
+ {
+ TargetPriority.Closest => (transform.position - eh.transform.position).sqrMagnitude,
+ TargetPriority.Weakest => eh.CurrentHp,
+ TargetPriority.Strongest => eh.CurrentHp,
+ _ => 0f,
+ };
+ }
+
+ private void SetTarget(EnemyHealth eh)
+ {
+ currentTarget = eh;
+ currentTarget.OnDied += HandleTargetDied;
+ replicatedTarget.Value = new NetworkObjectReference(eh.NetworkObject);
+ }
+
+ private void ClearTarget()
+ {
+ if (currentTarget != null)
+ currentTarget.OnDied -= HandleTargetDied;
+
+ currentTarget = null;
+ replicatedTarget.Value = default;
+ }
+
+ private void HandleTargetDied(EnemyHealth dead)
+ {
+ if ((object)dead == (object)currentTarget)
+ ClearTarget();
+ }
+
+ // ----- Attack tick -------------------------------------------------
+
+ private void TickAttack(TowerDefinition def)
+ {
+ attackCooldown -= Time.deltaTime;
+ if (attackCooldown > 0f) return;
+
+ attackCooldown = 1f / def.FireRate;
+ Fire(def);
+ }
+
+ private void Fire(TowerDefinition def)
+ {
+ Vector3 targetPos = currentTarget.transform.position;
+
+ if (def.ProjectilePrefab == null)
+ {
+ // Hitscan — apply damage this frame.
+ ApplyDamageToTarget(def, currentTarget, targetPos);
+ }
+ else
+ {
+ SpawnProjectile(def, currentTarget, targetPos);
+ }
+
+ FireClientRpc(targetPos);
+ }
+
+ // ----- Damage application ------------------------------------------
+
+ private void ApplyDamageToTarget(TowerDefinition def, EnemyHealth primary, Vector3 primaryPos)
+ {
+ switch (def.TargetType)
+ {
+ case TargetType.Single:
+ HitEnemy(def, primary);
+ break;
+
+ case TargetType.Splash:
+ HitEnemy(def, primary);
+ ApplySplash(def, primary, primaryPos);
+ break;
+
+ case TargetType.Chain:
+ ApplyChain(def, primary);
+ break;
+ }
+ }
+
+ private void ApplySplash(TowerDefinition def, EnemyHealth primary, Vector3 origin)
+ {
+ if (def.SplashRadius <= 0f) return;
+
+ int count = Physics.OverlapSphereNonAlloc(
+ origin, def.SplashRadius, s_overlapBuffer, enemyLayerMask);
+
+ for (int i = 0; i < count; i++)
+ {
+ var eh = s_overlapBuffer[i].GetComponent();
+ if (eh == null || eh.IsDead || (object)eh == (object)primary) continue;
+ HitEnemy(def, eh);
+ }
+ }
+
+ private void ApplyChain(TowerDefinition def, EnemyHealth primary)
+ {
+ // Chain hit positions are collected and sent to clients for the
+ // future lightning-arc visual. The list is per-fire, so allocation
+ // here is acceptable; optimise to a pool if chain towers become hot.
+ var hitPositions = new List { primary.transform.position };
+ var alreadyHit = new HashSet { primary };
+
+ HitEnemy(def, primary);
+
+ EnemyHealth current = primary;
+ for (int jump = 0; jump < def.ChainCount; jump++)
+ {
+ EnemyHealth next = null;
+ float bestSqr = float.MaxValue;
+
+ int count = Physics.OverlapSphereNonAlloc(
+ current.transform.position, def.ChainRange, s_overlapBuffer, enemyLayerMask);
+
+ for (int i = 0; i < count; i++)
+ {
+ var eh = s_overlapBuffer[i].GetComponent();
+ if (eh == null || eh.IsDead || alreadyHit.Contains(eh)) continue;
+
+ float sqr = (current.transform.position - eh.transform.position).sqrMagnitude;
+ if (sqr < bestSqr) { next = eh; bestSqr = sqr; }
+ }
+
+ if (next == null) break;
+
+ alreadyHit.Add(next);
+ hitPositions.Add(next.transform.position);
+ HitEnemy(def, next);
+ current = next;
+ }
+
+ ChainFiredClientRpc(hitPositions.ToArray());
+ }
+
+ private void HitEnemy(TowerDefinition def, EnemyHealth target)
+ {
+ target.TakeDamage(def.Damage, def.DamageType);
+ ApplyStatusEffect(def, target);
+ }
+
+ private void ApplyStatusEffect(TowerDefinition def, EnemyHealth target)
+ {
+ if (def.EffectDuration <= 0f) return;
+
+ float magnitude = def.DamageType switch
+ {
+ DamageType.Cold => def.SlowFactor,
+ DamageType.Fire => def.DotDamagePerSecond,
+ DamageType.Poison => def.DotDamagePerSecond,
+ _ => 0f,
+ };
+
+ if (magnitude <= 0f) return;
+
+ target.GetComponent()
+ ?.ApplyEffect(def.DamageType, magnitude, def.EffectDuration);
+ }
+
+ // ----- Projectile spawning -----------------------------------------
+
+ private void SpawnProjectile(TowerDefinition def, EnemyHealth target, Vector3 targetPos)
+ {
+ var go = Instantiate(def.ProjectilePrefab, transform.position, Quaternion.identity);
+
+ var proj = go.GetComponent();
+ if (proj == null)
+ {
+ Debug.LogError($"[TowerCombat] ProjectilePrefab '{def.ProjectilePrefab.name}' " +
+ $"has no Projectile component. The prefab must have Projectile, " +
+ $"NetworkObject, and NetworkTransform at its root.");
+ Destroy(go);
+ return;
+ }
+
+ proj.InitializeServer(
+ target,
+ def.Damage,
+ def.DamageType,
+ def.TargetType,
+ def.SplashRadius,
+ def.SlowFactor,
+ def.DotDamagePerSecond,
+ def.EffectDuration,
+ def.ProjectileSpeed,
+ enemyLayerMask);
+
+ go.GetComponent().Spawn();
+ }
+
+ // ----- ClientRpcs --------------------------------------------------
+
+ [ClientRpc]
+ private void FireClientRpc(Vector3 targetPos)
+ {
+ // Visual consumers subscribe to OnFire for muzzle flash, tracers, etc.
+ OnFire?.Invoke(targetPos);
+ }
+
+ [ClientRpc]
+ private void ChainFiredClientRpc(Vector3[] hitPositions)
+ {
+ // STUB: lightning-arc visual between hitPositions goes here.
+ // A future visual component will subscribe to an OnChainFired event
+ // and draw a line renderer through these world positions.
+ }
+
+ // ----- NV callback (fires on all peers) ----------------------------
+
+ private void HandleReplicatedTargetChanged(
+ NetworkObjectReference prev, NetworkObjectReference next)
+ {
+ bool hadTarget = prev.TryGet(out _);
+ bool hasTarget = next.TryGet(out _);
+
+ if (hasTarget)
+ OnTargetAcquired?.Invoke(next);
+ else if (hadTarget)
+ OnTargetLost?.Invoke();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Combat/TowerCombat.cs.meta b/Assets/_Project/Scripts/Combat/TowerCombat.cs.meta
new file mode 100644
index 0000000..c64476a
--- /dev/null
+++ b/Assets/_Project/Scripts/Combat/TowerCombat.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7eb6cce6cd96b23478b7d2173cebf74d
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs b/Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs
new file mode 100644
index 0000000..efb1191
--- /dev/null
+++ b/Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs
@@ -0,0 +1,135 @@
+// Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs
+using UnityEngine;
+using UnityEngine.Rendering.Universal;
+using TD.Gameplay;
+
+namespace TD.Combat
+{
+ ///
+ /// Displays a translucent decal circle representing this tower's attack range
+ /// when the local player selects the tower.
+ ///
+ ///
+ /// Visual only. No networking — selection is a local UI concept.
+ /// All clients independently show the range indicator for whatever tower
+ /// they have selected.
+ ///
+ /// Prefab setup:
+ ///
+ /// - Add this component to the tower prefab root (alongside TowerInstance).
+ /// - Add a child GameObject named "RangeIndicator".
+ /// - Add a DecalProjector to that child and assign it to
+ /// (or leave it unassigned — auto-found
+ /// via GetComponentInChildren in Start).
+ /// - Assign a translucent range-circle material to the DecalProjector.
+ ///
+ ///
+ /// Sizing: The projector diameter is set once in Start from
+ /// TowerDefinition.Range. Towers are static, so no per-frame resize is needed.
+ ///
+ /// Subscription timing: Follows the same deferred-subscribe pattern as
+ /// — retries until
+ /// is available, then stops polling.
+ ///
+ public class TowerRangeIndicator : MonoBehaviour
+ {
+ [Tooltip("DecalProjector used to render the range circle on the ground. " +
+ "Auto-found via GetComponentInChildren if left empty.")]
+ [SerializeField] private DecalProjector rangeProjector;
+
+ [Tooltip("Vertical extent of the decal projection volume in world units. " +
+ "Must be tall enough to project onto terrain at any height in your map.")]
+ [SerializeField] private float projectionDepth = 50f;
+
+ private TowerInstance towerInstance;
+ private bool subscribed;
+
+ // ----- Lifecycle ---------------------------------------------------
+
+ private void Start()
+ {
+ towerInstance = GetComponent();
+ if (towerInstance == null)
+ {
+ Debug.LogError("[TowerRangeIndicator] No TowerInstance found on this " +
+ "GameObject. TowerRangeIndicator must sit on the same " +
+ "prefab root as TowerInstance.");
+ enabled = false;
+ return;
+ }
+
+ if (rangeProjector == null)
+ rangeProjector = GetComponentInChildren();
+ if (rangeProjector == null)
+ {
+ Debug.LogError("[TowerRangeIndicator] No DecalProjector found. " +
+ "Add one as a child of this GameObject and assign it " +
+ "to the rangeProjector field.");
+ enabled = false;
+ return;
+ }
+
+ // TowerInstance resolves its Definition in OnNetworkSpawn, which runs
+ // before Start, so Definition is guaranteed available here.
+ float range = towerInstance.Definition != null ? towerInstance.Definition.Range : 0f;
+ float diameter = range * 2f;
+
+ rangeProjector.size = new Vector3(diameter, diameter, projectionDepth);
+ rangeProjector.pivot = Vector3.zero;
+
+ // DecalProjector projects along its local +Z axis; rotate 90° around X
+ // to project downward onto the ground plane.
+ rangeProjector.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
+ rangeProjector.transform.localPosition = Vector3.zero;
+
+ rangeProjector.enabled = false;
+
+ TrySubscribe();
+ }
+
+ private void OnDestroy()
+ {
+ Unsubscribe();
+ }
+
+ private void Update()
+ {
+ // Retry subscription each frame until SelectionState is ready.
+ // Once subscribed the branch short-circuits immediately.
+ if (!subscribed) TrySubscribe();
+ }
+
+ // ----- Subscription -----------------------------------------------
+
+ private void TrySubscribe()
+ {
+ if (subscribed) return;
+ var sel = SelectionState.Instance;
+ if (sel == null) return;
+
+ sel.OnSelectionChanged += HandleSelectionChanged;
+ subscribed = true;
+
+ // Catch whatever is already selected before we subscribed.
+ HandleSelectionChanged(sel.SelectedObject);
+ }
+
+ private void Unsubscribe()
+ {
+ if (!subscribed) return;
+ var sel = SelectionState.Instance;
+ if (sel != null) sel.OnSelectionChanged -= HandleSelectionChanged;
+ subscribed = false;
+ }
+
+ // ----- Selection handler ------------------------------------------
+
+ private void HandleSelectionChanged(ISelectable newSelection)
+ {
+ if (rangeProjector == null) return;
+
+ // Show only when THIS tower's TowerInstance is the selected object.
+ rangeProjector.enabled = (object)newSelection == (object)towerInstance;
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs.meta b/Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs.meta
new file mode 100644
index 0000000..035a7f0
--- /dev/null
+++ b/Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c0edb0c5206ca454bbd7c300c6cc7574
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Core/Enums.cs b/Assets/_Project/Scripts/Core/Enums.cs
index d84c1b4..80f9df8 100644
--- a/Assets/_Project/Scripts/Core/Enums.cs
+++ b/Assets/_Project/Scripts/Core/Enums.cs
@@ -1,5 +1,48 @@
namespace TD.Core
{
+ ///
+ /// How a tower's attack damage is typed. Determines which lingering effect is applied
+ /// on hit and how enemy resistances/weaknesses are calculated (Phase 1.5+).
+ ///
+ public enum DamageType : byte
+ {
+ Physical = 0,
+ Piercing = 1,
+ Cold = 2,
+ Fire = 3,
+ Poison = 4,
+ Holy = 5,
+ }
+
+ ///
+ /// How a tower selects which enemy in range to attack.
+ ///
+ public enum TargetPriority : byte
+ {
+ /// Nearest enemy to the tower's center.
+ Closest = 0,
+ /// Enemy with the lowest current HP.
+ Weakest = 1,
+ /// Enemy with the highest current HP.
+ Strongest = 2,
+ }
+
+ ///
+ /// How a tower's damage is distributed across enemies.
+ /// Orthogonal to : any target type
+ /// can be delivered by a projectile or hitscan.
+ ///
+ public enum TargetType : byte
+ {
+ /// Hits one enemy for full damage.
+ Single = 0,
+ /// Hits the primary target, then all enemies within SplashRadius.
+ Splash = 1,
+ /// Chains damage to up to ChainCount additional nearby enemies.
+ Chain = 2,
+ }
+
+
///
/// Identifies a player slot in a match. Backed by byte to keep grid arrays compact.
///
diff --git a/Assets/_Project/Scripts/Core/_testScript.cs b/Assets/_Project/Scripts/Core/_testScript.cs
deleted file mode 100644
index c945653..0000000
--- a/Assets/_Project/Scripts/Core/_testScript.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using UnityEngine;
-using TD.Core;
-
-public class GridCoordsTest : MonoBehaviour
-{
- void Start()
- {
- Debug.Log($"Tile (5,7) world center: {GridCoordinates.GridToWorld(new Vector2Int(5, 7))}");
- Debug.Log($"World (5.3, 0, 6.8) maps to tile: {GridCoordinates.WorldToGrid(new Vector3(5.3f, 0, 6.8f))}");
- Debug.Log($"2x2 footprint at (5,7) center: {GridCoordinates.GetFootprintCenterWorld(new Vector2Int(5, 7), new Vector2Int(2, 2))}");
- }
-}
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Core/_testScript.cs.meta b/Assets/_Project/Scripts/Core/_testScript.cs.meta
deleted file mode 100644
index 14d66fb..0000000
--- a/Assets/_Project/Scripts/Core/_testScript.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 9c2af3b2731bd314e8502ac9db3a8bc5
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/EnemyHealth.cs b/Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
new file mode 100644
index 0000000..05e38e4
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
@@ -0,0 +1,90 @@
+// Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
+using Unity.Netcode;
+using UnityEngine;
+using TD.Core;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Per-enemy HP component. Holds replicated HP and is the single point
+ /// through which all damage flows, so resistance lookups (Phase 1.5+) can
+ /// be added in one place without touching every damage source.
+ ///
+ ///
+ /// Lives on the enemy prefab root alongside and the
+ /// future EnemyMovement component (Phase 1.5/1.6). HP is server-written
+ /// and replicated to all clients so health bars can render on any peer.
+ ///
+ /// Death flow (server-only):
+ /// TakeDamage clamps HP to 0, fires , then calls
+ /// NetworkObject.Despawn. Subscribers must not touch the NetworkObject
+ /// after OnDied returns.
+ ///
+ [RequireComponent(typeof(NetworkObject))]
+ public class EnemyHealth : NetworkBehaviour
+ {
+ [SerializeField] private float maxHp = 100f;
+
+ private readonly NetworkVariable hp = new NetworkVariable(
+ 0f,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server);
+
+ // ----- Public state -----------------------------------------------
+
+ public float CurrentHp => hp.Value;
+ public float MaxHp => maxHp;
+ public bool IsDead => hp.Value <= 0f;
+
+ // Stub: set by EnemyMovement or spawner in Phase 1.5/1.6.
+ // TowerCombat reads this to honour the GroundedOnly tower flag.
+ public bool IsFlying => false;
+
+ // ----- Events -----------------------------------------------------
+
+ ///
+ /// Fired on the server immediately before the enemy NetworkObject is despawned.
+ /// subscribes to clear its target reference.
+ /// Do not access the NetworkObject after this event returns.
+ ///
+ public event System.Action OnDied;
+
+ // ----- NGO lifecycle ----------------------------------------------
+
+ public override void OnNetworkSpawn()
+ {
+ if (IsServer)
+ hp.Value = maxHp;
+ }
+
+ // ----- Server API -------------------------------------------------
+
+ ///
+ /// Applies to this enemy. Server-only; no-op on clients.
+ /// is recorded for future resistance/weakness lookups —
+ /// all damage is full-value until the resistance table is implemented (Phase 1.5+).
+ ///
+ public void TakeDamage(float damage, DamageType type)
+ {
+ if (!IsServer) return;
+ if (IsDead) return;
+
+ // STUB — resistance table slot:
+ // float modified = ResistanceTable.Apply(damage, type, this);
+ float modified = damage;
+
+ hp.Value = Mathf.Max(0f, hp.Value - modified);
+
+ if (hp.Value <= 0f)
+ HandleDeath();
+ }
+
+ // ----- Private ----------------------------------------------------
+
+ private void HandleDeath()
+ {
+ OnDied?.Invoke(this);
+ NetworkObject.Despawn();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Gameplay/EnemyHealth.cs.meta b/Assets/_Project/Scripts/Gameplay/EnemyHealth.cs.meta
new file mode 100644
index 0000000..e418834
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/EnemyHealth.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 21dea26768768b8449a8924f638557be
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/EnemyStatus.cs b/Assets/_Project/Scripts/Gameplay/EnemyStatus.cs
new file mode 100644
index 0000000..7827d70
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/EnemyStatus.cs
@@ -0,0 +1,160 @@
+// Assets/_Project/Scripts/Gameplay/EnemyStatus.cs
+using System.Collections.Generic;
+using Unity.Netcode;
+using UnityEngine;
+using TD.Core;
+
+namespace TD.Gameplay
+{
+ ///
+ /// A single active lingering effect on an enemy.
+ ///
+ ///
+ /// Magnitude semantics by source:
+ ///
+ /// - Cold — fraction of speed retained (0.5 = half speed)
+ /// - Fire — damage per second applied as a DoT tick
+ /// - Poison — damage per second applied as a DoT tick
+ /// - Others — unused (magnitude = 0)
+ ///
+ ///
+ public struct StatusEffect
+ {
+ public DamageType Source;
+ public float Magnitude;
+ public float RemainingDuration;
+ }
+
+ ///
+ /// Tracks and ticks lingering status effects (slow, burn, poison) on an enemy.
+ ///
+ ///
+ /// Authority: The active-effect list is server-local (not replicated).
+ /// Only the derived NetworkVariable is replicated,
+ /// so EnemyMovement (Phase 1.5/1.6) can scale speed on all peers without
+ /// re-broadcasting the full effect list.
+ ///
+ /// Stacking rule: A second hit of the same refreshes
+ /// the duration and magnitude rather than stacking. Cross-type interactions (e.g.
+ /// Cold + Fire) are not yet implemented; is the hook for
+ /// when that design is worked out.
+ ///
+ /// DoT damage is applied by calling each
+ /// tick so resistance lookups remain in one place.
+ ///
+ [RequireComponent(typeof(NetworkObject))]
+ public class EnemyStatus : NetworkBehaviour
+ {
+ // Replicated so EnemyMovement can read it on all clients without
+ // knowing anything about which effects are active.
+ private readonly NetworkVariable speedMultiplier = new NetworkVariable(
+ 1f,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server);
+
+ // Server-local — only the derived speedMultiplier NV crosses the wire.
+ private readonly List activeEffects = new List();
+
+ // Resolved once; used by Tick for DoT TakeDamage calls.
+ private EnemyHealth health;
+
+ // ----- NGO lifecycle -----------------------------------------------
+
+ public override void OnNetworkSpawn()
+ {
+ health = GetComponent();
+ }
+
+ // ----- Public API --------------------------------------------------
+
+ /// Current speed fraction (0–1). 1 = full speed, 0.5 = half speed, etc.
+ public float GetSpeedMultiplier() => speedMultiplier.Value;
+
+ /// True if an effect of the given type is currently active on this enemy.
+ public bool HasEffect(DamageType type)
+ {
+ for (int i = 0; i < activeEffects.Count; i++)
+ if (activeEffects[i].Source == type) return true;
+ return false;
+ }
+
+ ///
+ /// Applies or refreshes a lingering effect. Server-only; no-op on clients.
+ /// Re-hitting with the same damage type refreshes duration and magnitude.
+ ///
+ public void ApplyEffect(DamageType source, float magnitude, float duration)
+ {
+ if (!IsServer) return;
+
+ for (int i = 0; i < activeEffects.Count; i++)
+ {
+ if (activeEffects[i].Source != source) continue;
+
+ var e = activeEffects[i];
+ e.Magnitude = magnitude;
+ e.RemainingDuration = duration;
+ activeEffects[i] = e;
+ RecalculateSpeedMultiplier();
+ return;
+ }
+
+ activeEffects.Add(new StatusEffect
+ {
+ Source = source,
+ Magnitude = magnitude,
+ RemainingDuration = duration,
+ });
+ RecalculateSpeedMultiplier();
+ }
+
+ // ----- Server tick ------------------------------------------------
+
+ private void Update()
+ {
+ if (!IsServer || activeEffects.Count == 0) return;
+ TickEffects(Time.deltaTime);
+ }
+
+ private void TickEffects(float dt)
+ {
+ bool anyExpired = false;
+
+ for (int i = activeEffects.Count - 1; i >= 0; i--)
+ {
+ var e = activeEffects[i];
+
+ // Apply DoT for Fire and Poison.
+ if (e.Source == DamageType.Fire || e.Source == DamageType.Poison)
+ {
+ if (health != null && !health.IsDead)
+ health.TakeDamage(e.Magnitude * dt, e.Source);
+ }
+
+ e.RemainingDuration -= dt;
+ if (e.RemainingDuration <= 0f)
+ {
+ activeEffects.RemoveAt(i);
+ anyExpired = true;
+ }
+ else
+ {
+ activeEffects[i] = e;
+ }
+ }
+
+ if (anyExpired)
+ RecalculateSpeedMultiplier();
+ }
+
+ private void RecalculateSpeedMultiplier()
+ {
+ float mult = 1f;
+ for (int i = 0; i < activeEffects.Count; i++)
+ {
+ if (activeEffects[i].Source == DamageType.Cold)
+ mult = Mathf.Min(mult, activeEffects[i].Magnitude);
+ }
+ speedMultiplier.Value = mult;
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Gameplay/EnemyStatus.cs.meta b/Assets/_Project/Scripts/Gameplay/EnemyStatus.cs.meta
new file mode 100644
index 0000000..ad10ab2
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/EnemyStatus.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 292d1b92cd49dc74f8dfd74cdbe4ece7
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Towers/TowerDefinition.cs b/Assets/_Project/Scripts/Towers/TowerDefinition.cs
index 2634f72..a106ebf 100644
--- a/Assets/_Project/Scripts/Towers/TowerDefinition.cs
+++ b/Assets/_Project/Scripts/Towers/TowerDefinition.cs
@@ -1,4 +1,5 @@
using UnityEngine;
+using TD.Core;
namespace TD.Towers
{
@@ -68,29 +69,78 @@ namespace TD.Towers
public GameObject TowerPrefab;
// -------------------------------------------------------------------
- // Combat — STUBBED
+ // Combat
// -------------------------------------------------------------------
- [Header("Combat (Stubbed — not consumed until combat system is implemented)")]
- [Tooltip("STUBBED. Damage dealt per hit to a single target.")]
+ [Header("Combat — Targeting")]
+ [Tooltip("How the tower's damage is typed. Determines lingering effects and enemy " +
+ "resistance/weakness interactions.")]
+ public DamageType DamageType;
+
+ [Tooltip("Which enemy in range the tower prioritizes.")]
+ public TargetPriority TargetPriority;
+
+ [Tooltip("How damage is distributed: Single (one enemy), Splash (primary + radius), " +
+ "Chain (primary then N nearest neighbours).")]
+ public TargetType TargetType;
+
+ [Tooltip("When true, this tower can only target grounded enemies. " +
+ "Default false = targets both grounded and flying units.")]
+ public bool GroundedOnly;
+
+ [Header("Combat — Attack")]
+ [Tooltip("Damage dealt per hit to the primary target, before resistances.")]
public float Damage;
- [Tooltip("STUBBED. Attack range in world units. Enemies within this radius are targetable.")]
+ [Tooltip("Attack range in world units. Enemies within this radius are targetable. " +
+ "Displayed as a translucent decal circle when the tower is selected.")]
public float Range;
- [Tooltip("STUBBED. Attacks per second.")]
+ [Tooltip("Attacks per second.")]
public float FireRate;
- [Tooltip("STUBBED. Radius of splash damage around the impact point. 0 = single target.")]
+ [Header("Combat — Area")]
+ [Tooltip("Radius of splash damage around the primary impact point. " +
+ "Only used when TargetType = Splash.")]
public float SplashRadius;
- [Tooltip("STUBBED. Fraction by which enemy movement speed is multiplied on hit. " +
- "1.0 = no slow. 0.5 = 50% slow.")]
+ [Tooltip("Number of additional enemies damage chains to after the primary target. " +
+ "Only used when TargetType = Chain.")]
+ public int ChainCount;
+
+ [Tooltip("Maximum world-unit distance each chain jump can travel. " +
+ "Only used when TargetType = Chain.")]
+ public float ChainRange;
+
+ [Header("Combat — Effects")]
+ [Tooltip("For Cold towers: fraction of movement speed retained on hit. " +
+ "1.0 = no slow, 0.5 = half speed. Ignored for other damage types.")]
[Range(0f, 1f)]
public float SlowFactor = 1f;
- [Tooltip("STUBBED. Projectile prefab fired at targets. Null = hitscan (instant hit, no " +
- "projectile travel).")]
+ [Tooltip("For Fire / Poison towers: damage per second applied as a damage-over-time tick. " +
+ "Ignored for other damage types.")]
+ public float DotDamagePerSecond;
+
+ [Tooltip("Duration in seconds that any lingering effect (slow, burn, poison) persists " +
+ "after the hit. 0 = no lingering effect.")]
+ public float EffectDuration;
+
+ [Header("Combat — Projectile")]
+ [Tooltip("Prefab fired at targets. Must have a Projectile component, NetworkObject, " +
+ "and NetworkTransform at its root. Null = hitscan (instant hit, no travel).")]
public GameObject ProjectilePrefab;
+
+ [Tooltip("World-units per second the projectile travels. Ignored when ProjectilePrefab is null.")]
+ public float ProjectileSpeed = 10f;
+
+ // -------------------------------------------------------------------
+ // Upgrades
+ // -------------------------------------------------------------------
+
+ [Header("Upgrades")]
+ [Tooltip("Tower definitions this tower can upgrade into. Leave empty for no upgrades. " +
+ "Multiple entries create branching paths — the player chooses one.")]
+ public TowerDefinition[] UpgradePaths;
}
}
\ No newline at end of file
diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset
index a503376..627ee5e 100644
--- a/ProjectSettings/TagManager.asset
+++ b/ProjectSettings/TagManager.asset
@@ -15,7 +15,7 @@ TagManager:
- TerrainGeometry
- Selection
- BuildSite
- -
+ - Enemy
-
-
-