Adding major combat changes and features
This commit is contained in:
parent
abcefcd7f1
commit
42ee0bf65d
28 changed files with 1653 additions and 46 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
216
Assets/_Project/Prefabs/Enemies/EnemyPlaceholder.prefab
Normal file
216
Assets/_Project/Prefabs/Enemies/EnemyPlaceholder.prefab
Normal file
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0854f339a1958d343a6cb16cd3f907ff
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
203
Assets/_Project/Prefabs/Projectiles/ProjectilePlaceholder.prefab
Normal file
203
Assets/_Project/Prefabs/Projectiles/ProjectilePlaceholder.prefab
Normal file
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dc2e4a4108e03874a8b2dab88dcc8fba
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
8
Assets/_Project/Scripts/Combat.meta
Normal file
8
Assets/_Project/Scripts/Combat.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 07a063da1d1c0b549b61d11d723e2930
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
177
Assets/_Project/Scripts/Combat/Projectile.cs
Normal file
177
Assets/_Project/Scripts/Combat/Projectile.cs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
// Assets/_Project/Scripts/Combat/Projectile.cs
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
using TD.Gameplay;
|
||||
|
||||
namespace TD.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// A traveling projectile spawned by <see cref="TowerCombat"/> when a tower has
|
||||
/// a non-null <c>ProjectilePrefab</c> configured.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Authority:</b> Movement and hit detection run server-only.
|
||||
/// <c>NetworkTransform</c> (required on the prefab) replicates the position to
|
||||
/// clients so the projectile is visible on all peers.
|
||||
///
|
||||
/// <b>Initialization:</b> Mirrors the <c>TowerInstance.InitializeServer</c> pattern —
|
||||
/// <see cref="InitializeServer"/> is called by <c>TowerCombat</c> immediately after
|
||||
/// <c>Instantiate</c> and before <c>NetworkObject.Spawn()</c>, which avoids writing
|
||||
/// to NetworkVariables before spawn.
|
||||
///
|
||||
/// <b>Target loss:</b> If the target dies or is destroyed before the projectile
|
||||
/// arrives, the projectile despawns silently (no hit, no damage).
|
||||
///
|
||||
/// <b>Chain + Projectile:</b> 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.
|
||||
///
|
||||
/// <b>Prefab requirements:</b> Must have <c>NetworkObject</c>, <c>NetworkTransform</c>,
|
||||
/// and this <c>Projectile</c> component at the root.
|
||||
/// </remarks>
|
||||
[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) -----------
|
||||
|
||||
/// <summary>
|
||||
/// Stores all data this projectile needs to travel and apply damage.
|
||||
/// Call this immediately after <c>Instantiate</c> and before
|
||||
/// <c>NetworkObject.Spawn()</c>.
|
||||
/// </summary>
|
||||
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<EnemyHealth>();
|
||||
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<EnemyStatus>()
|
||||
?.ApplyEffect(damageType, magnitude, effectDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/Combat/Projectile.cs.meta
Normal file
2
Assets/_Project/Scripts/Combat/Projectile.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a6854f71c9fdcda42b297c397f96c8be
|
||||
409
Assets/_Project/Scripts/Combat/TowerCombat.cs
Normal file
409
Assets/_Project/Scripts/Combat/TowerCombat.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-tower combat controller. Handles target acquisition, attack timing,
|
||||
/// damage application, and projectile spawning.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Authority:</b> All combat logic (targeting, damage, projectile spawn) runs
|
||||
/// on the server only. Clients receive two signals for visual feedback:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="replicatedTarget"/> — NetworkVariable clients can read to know
|
||||
/// what the tower is aiming at (drives future rotation/lean visuals).</item>
|
||||
/// <item><see cref="FireClientRpc"/> — one-shot broadcast at each attack, carrying
|
||||
/// the target world position for muzzle flash, tracer FX, etc.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Component coupling:</b> Reads all stats from <see cref="TowerInstance.Definition"/>
|
||||
/// on the same GameObject. Does not modify TowerInstance.
|
||||
///
|
||||
/// <b>Inspector setup required:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Assign <see cref="enemyLayerMask"/> to the "Enemy" physics layer.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
[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<NetworkObjectReference> replicatedTarget =
|
||||
new NetworkVariable<NetworkObjectReference>(
|
||||
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 --------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Server-local reference to the current attack target.
|
||||
/// Null on clients — use <see cref="replicatedTarget"/> there.
|
||||
/// </summary>
|
||||
public EnemyHealth CurrentTarget => currentTarget;
|
||||
|
||||
/// <summary>
|
||||
/// Fired locally on ALL peers when the tower acquires a new target.
|
||||
/// Driven by <see cref="replicatedTarget"/>.<c>OnValueChanged</c>, and also
|
||||
/// fired in <see cref="OnNetworkSpawn"/> for late-joining clients so visual
|
||||
/// consumers always initialise from the correct state.
|
||||
/// </summary>
|
||||
public event System.Action<NetworkObjectReference> OnTargetAcquired;
|
||||
|
||||
/// <summary>Fired locally on ALL peers when the tower loses its target.</summary>
|
||||
public event System.Action OnTargetLost;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public event System.Action<Vector3> OnFire;
|
||||
|
||||
// ----- NGO lifecycle -----------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
towerInstance = GetComponent<TowerInstance>();
|
||||
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<EnemyHealth>();
|
||||
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<EnemyHealth>();
|
||||
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<Vector3> { primary.transform.position };
|
||||
var alreadyHit = new HashSet<EnemyHealth> { 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<EnemyHealth>();
|
||||
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<EnemyStatus>()
|
||||
?.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<Projectile>();
|
||||
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<NetworkObject>().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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/Combat/TowerCombat.cs.meta
Normal file
2
Assets/_Project/Scripts/Combat/TowerCombat.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7eb6cce6cd96b23478b7d2173cebf74d
|
||||
135
Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs
Normal file
135
Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using TD.Gameplay;
|
||||
|
||||
namespace TD.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays a translucent decal circle representing this tower's attack range
|
||||
/// when the local player selects the tower.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Visual only.</b> No networking — selection is a local UI concept.
|
||||
/// All clients independently show the range indicator for whatever tower
|
||||
/// they have selected.
|
||||
///
|
||||
/// <b>Prefab setup:</b>
|
||||
/// <list type="number">
|
||||
/// <item>Add this component to the tower prefab root (alongside TowerInstance).</item>
|
||||
/// <item>Add a child GameObject named "RangeIndicator".</item>
|
||||
/// <item>Add a <c>DecalProjector</c> to that child and assign it to
|
||||
/// <see cref="rangeProjector"/> (or leave it unassigned — auto-found
|
||||
/// via <c>GetComponentInChildren</c> in Start).</item>
|
||||
/// <item>Assign a translucent range-circle material to the DecalProjector.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Sizing:</b> The projector diameter is set once in <c>Start</c> from
|
||||
/// <c>TowerDefinition.Range</c>. Towers are static, so no per-frame resize is needed.
|
||||
///
|
||||
/// <b>Subscription timing:</b> Follows the same deferred-subscribe pattern as
|
||||
/// <see cref="SelectionVisualizer"/> — retries until <see cref="SelectionState"/>
|
||||
/// is available, then stops polling.
|
||||
/// </remarks>
|
||||
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<TowerInstance>();
|
||||
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<DecalProjector>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c0edb0c5206ca454bbd7c300c6cc7574
|
||||
|
|
@ -1,5 +1,48 @@
|
|||
namespace TD.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 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+).
|
||||
/// </summary>
|
||||
public enum DamageType : byte
|
||||
{
|
||||
Physical = 0,
|
||||
Piercing = 1,
|
||||
Cold = 2,
|
||||
Fire = 3,
|
||||
Poison = 4,
|
||||
Holy = 5,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How a tower selects which enemy in range to attack.
|
||||
/// </summary>
|
||||
public enum TargetPriority : byte
|
||||
{
|
||||
/// <summary>Nearest enemy to the tower's center.</summary>
|
||||
Closest = 0,
|
||||
/// <summary>Enemy with the lowest current HP.</summary>
|
||||
Weakest = 1,
|
||||
/// <summary>Enemy with the highest current HP.</summary>
|
||||
Strongest = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How a tower's damage is distributed across enemies.
|
||||
/// Orthogonal to <see cref="TowerDefinition.ProjectilePrefab"/>: any target type
|
||||
/// can be delivered by a projectile or hitscan.
|
||||
/// </summary>
|
||||
public enum TargetType : byte
|
||||
{
|
||||
/// <summary>Hits one enemy for full damage.</summary>
|
||||
Single = 0,
|
||||
/// <summary>Hits the primary target, then all enemies within SplashRadius.</summary>
|
||||
Splash = 1,
|
||||
/// <summary>Chains damage to up to ChainCount additional nearby enemies.</summary>
|
||||
Chain = 2,
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Identifies a player slot in a match. Backed by byte to keep grid arrays compact.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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))}");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9c2af3b2731bd314e8502ac9db3a8bc5
|
||||
90
Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
Normal file
90
Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Lives on the enemy prefab root alongside <see cref="EnemyStatus"/> and the
|
||||
/// future <c>EnemyMovement</c> component (Phase 1.5/1.6). HP is server-written
|
||||
/// and replicated to all clients so health bars can render on any peer.
|
||||
///
|
||||
/// <b>Death flow (server-only):</b>
|
||||
/// <c>TakeDamage</c> clamps HP to 0, fires <see cref="OnDied"/>, then calls
|
||||
/// <c>NetworkObject.Despawn</c>. Subscribers must not touch the NetworkObject
|
||||
/// after <c>OnDied</c> returns.
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public class EnemyHealth : NetworkBehaviour
|
||||
{
|
||||
[SerializeField] private float maxHp = 100f;
|
||||
|
||||
private readonly NetworkVariable<float> hp = new NetworkVariable<float>(
|
||||
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 -----------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Fired on the server immediately before the enemy NetworkObject is despawned.
|
||||
/// <see cref="TD.Combat.TowerCombat"/> subscribes to clear its target reference.
|
||||
/// Do not access the NetworkObject after this event returns.
|
||||
/// </summary>
|
||||
public event System.Action<EnemyHealth> OnDied;
|
||||
|
||||
// ----- NGO lifecycle ----------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (IsServer)
|
||||
hp.Value = maxHp;
|
||||
}
|
||||
|
||||
// ----- Server API -------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Applies <paramref name="damage"/> to this enemy. Server-only; no-op on clients.
|
||||
/// <paramref name="type"/> is recorded for future resistance/weakness lookups —
|
||||
/// all damage is full-value until the resistance table is implemented (Phase 1.5+).
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/Gameplay/EnemyHealth.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/EnemyHealth.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 21dea26768768b8449a8924f638557be
|
||||
160
Assets/_Project/Scripts/Gameplay/EnemyStatus.cs
Normal file
160
Assets/_Project/Scripts/Gameplay/EnemyStatus.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A single active lingering effect on an enemy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Magnitude semantics by source:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Cold — fraction of speed retained (0.5 = half speed)</item>
|
||||
/// <item>Fire — damage per second applied as a DoT tick</item>
|
||||
/// <item>Poison — damage per second applied as a DoT tick</item>
|
||||
/// <item>Others — unused (magnitude = 0)</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public struct StatusEffect
|
||||
{
|
||||
public DamageType Source;
|
||||
public float Magnitude;
|
||||
public float RemainingDuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks and ticks lingering status effects (slow, burn, poison) on an enemy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Authority:</b> The active-effect list is server-local (not replicated).
|
||||
/// Only the derived <see cref="speedMultiplier"/> NetworkVariable is replicated,
|
||||
/// so <c>EnemyMovement</c> (Phase 1.5/1.6) can scale speed on all peers without
|
||||
/// re-broadcasting the full effect list.
|
||||
///
|
||||
/// <b>Stacking rule:</b> A second hit of the same <see cref="DamageType"/> refreshes
|
||||
/// the duration and magnitude rather than stacking. Cross-type interactions (e.g.
|
||||
/// Cold + Fire) are not yet implemented; <see cref="HasEffect"/> is the hook for
|
||||
/// when that design is worked out.
|
||||
///
|
||||
/// <b>DoT damage</b> is applied by calling <see cref="EnemyHealth.TakeDamage"/> each
|
||||
/// tick so resistance lookups remain in one place.
|
||||
/// </remarks>
|
||||
[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<float> speedMultiplier = new NetworkVariable<float>(
|
||||
1f,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
// Server-local — only the derived speedMultiplier NV crosses the wire.
|
||||
private readonly List<StatusEffect> activeEffects = new List<StatusEffect>();
|
||||
|
||||
// Resolved once; used by Tick for DoT TakeDamage calls.
|
||||
private EnemyHealth health;
|
||||
|
||||
// ----- NGO lifecycle -----------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
health = GetComponent<EnemyHealth>();
|
||||
}
|
||||
|
||||
// ----- Public API --------------------------------------------------
|
||||
|
||||
/// <summary>Current speed fraction (0–1). 1 = full speed, 0.5 = half speed, etc.</summary>
|
||||
public float GetSpeedMultiplier() => speedMultiplier.Value;
|
||||
|
||||
/// <summary>True if an effect of the given type is currently active on this enemy.</summary>
|
||||
public bool HasEffect(DamageType type)
|
||||
{
|
||||
for (int i = 0; i < activeEffects.Count; i++)
|
||||
if (activeEffects[i].Source == type) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies or refreshes a lingering effect. Server-only; no-op on clients.
|
||||
/// Re-hitting with the same damage type refreshes duration and magnitude.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/Gameplay/EnemyStatus.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/EnemyStatus.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 292d1b92cd49dc74f8dfd74cdbe4ece7
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue