diff --git a/Assets/_Project/Prefabs/Towers/Tower Construction Sets.meta b/Assets/_Project/Prefabs/Towers/Tower Construction Sets.meta new file mode 100644 index 0000000..e7fd49e --- /dev/null +++ b/Assets/_Project/Prefabs/Towers/Tower Construction Sets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3f1ef90255e065249a86a838844f7be2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Prefabs/Towers/BuildSiteVisual.prefab b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/BuildSiteVisual.prefab similarity index 99% rename from Assets/_Project/Prefabs/Towers/BuildSiteVisual.prefab rename to Assets/_Project/Prefabs/Towers/Tower Construction Sets/BuildSiteVisual.prefab index 66abbc6..6b3815f 100644 --- a/Assets/_Project/Prefabs/Towers/BuildSiteVisual.prefab +++ b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/BuildSiteVisual.prefab @@ -298,6 +298,7 @@ MonoBehaviour: queuedMaterial: {fileID: 2100000, guid: f8d951a6841d3f74098bb31255379774, type: 2} constructingMaterial: {fileID: 2100000, guid: 88f1dd7b174716645953857b38fb6948, type: 2} pausedMaterial: {fileID: 0} + defaultPhaseSet: {fileID: 11400000, guid: ed951755c09d6d0479b4fd924b6915da, type: 2} stageCount: 4 queuedYScale: 0.15 --- !u!65 &6259437393614329336 diff --git a/Assets/_Project/Prefabs/Towers/BuildSiteVisual.prefab.meta b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/BuildSiteVisual.prefab.meta similarity index 100% rename from Assets/_Project/Prefabs/Towers/BuildSiteVisual.prefab.meta rename to Assets/_Project/Prefabs/Towers/Tower Construction Sets/BuildSiteVisual.prefab.meta diff --git a/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet.meta b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet.meta new file mode 100644 index 0000000..e070754 --- /dev/null +++ b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 82e418e7a5a804747a9a4865648b7793 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstructionSet.asset b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstructionSet.asset new file mode 100644 index 0000000..2272a43 --- /dev/null +++ b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstructionSet.asset @@ -0,0 +1,18 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c8f2a4e1b9d6437052a1e8c4f7b3d069, type: 3} + m_Name: DefaultConstructionSet + m_EditorClassIdentifier: Assembly-CSharp::TD.Towers.ConstructionPhaseSet + phases: + - {fileID: 562891720401665892, guid: e7be86c29087e99439507b508425b612, type: 3} + - {fileID: 3284400382278582069, guid: c6dabc822d0afb746adc08fb8602c2b4, type: 3} + - {fileID: 2161157853692362223, guid: d00c71c0cfc7e8744b9f8c1d48c2d256, type: 3} diff --git a/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstructionSet.asset.meta b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstructionSet.asset.meta new file mode 100644 index 0000000..3832c3f --- /dev/null +++ b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstructionSet.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ed951755c09d6d0479b4fd924b6915da +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_1.prefab b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_1.prefab new file mode 100644 index 0000000..70ca06b --- /dev/null +++ b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_1.prefab @@ -0,0 +1,114 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &562891720401665892 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5433206400477731590} + - component: {fileID: 3041202948021905482} + - component: {fileID: 2318454595080774531} + - component: {fileID: 2971339723339766249} + m_Layer: 0 + m_Name: DefaultConstruction_Phase_1 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &5433206400477731590 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 562891720401665892} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 0.1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &3041202948021905482 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 562891720401665892} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &2318454595080774531 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 562891720401665892} + 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 &2971339723339766249 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 562891720401665892} + 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} diff --git a/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_1.prefab.meta b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_1.prefab.meta new file mode 100644 index 0000000..684c909 --- /dev/null +++ b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_1.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e7be86c29087e99439507b508425b612 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_2.prefab b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_2.prefab new file mode 100644 index 0000000..43ed50f --- /dev/null +++ b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_2.prefab @@ -0,0 +1,114 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &3284400382278582069 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3820746952356246079} + - component: {fileID: 3350984935134180217} + - component: {fileID: 6336690892943026752} + - component: {fileID: 2003287843732108245} + m_Layer: 0 + m_Name: DefaultConstruction_Phase_2 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &3820746952356246079 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3284400382278582069} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1.5, y: 0.5, z: 1.5} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &3350984935134180217 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3284400382278582069} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &6336690892943026752 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3284400382278582069} + 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 &2003287843732108245 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3284400382278582069} + 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} diff --git a/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_2.prefab.meta b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_2.prefab.meta new file mode 100644 index 0000000..bb668c4 --- /dev/null +++ b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_2.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c6dabc822d0afb746adc08fb8602c2b4 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_3.prefab b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_3.prefab new file mode 100644 index 0000000..0c380ed --- /dev/null +++ b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_3.prefab @@ -0,0 +1,114 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &2161157853692362223 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6591719589271492770} + - component: {fileID: 3693505870455731207} + - component: {fileID: 2366594505425509977} + - component: {fileID: 5474707419433840976} + m_Layer: 0 + m_Name: DefaultConstruction_Phase_3 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &6591719589271492770 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2161157853692362223} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1.75, y: 1, z: 1.75} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &3693505870455731207 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2161157853692362223} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &2366594505425509977 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2161157853692362223} + 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 &5474707419433840976 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2161157853692362223} + 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} diff --git a/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_3.prefab.meta b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_3.prefab.meta new file mode 100644 index 0000000..dc59d87 --- /dev/null +++ b/Assets/_Project/Prefabs/Towers/Tower Construction Sets/DefaultConstructionSet/DefaultConstruction_Phase_3.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d00c71c0cfc7e8744b9f8c1d48c2d256 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs b/Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs index b0e6e55..3ba4d76 100644 --- a/Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs +++ b/Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs @@ -1,5 +1,4 @@ // Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs -using Unity.Collections; using Unity.Netcode; using UnityEngine; using TD.Core; @@ -63,27 +62,25 @@ namespace TD.Gameplay "Suggested: muted/grey-tinted variant of the constructing material.")] [SerializeField] private Material pausedMaterial; - [Header("Construction stages")] - [Tooltip("Number of discrete growth stages while constructing. 4 matches the " + - "design doc (1/4 → 2/4 → 3/4 → 4/4 height).")] + [Header("Construction phases")] + [Tooltip("Project-default construction-stage visuals used when the tower being built " + + "doesn't define its own ConstructionPhases. When this (or the tower's set) " + + "has phases, the build site swaps between those phase prefabs as it builds. " + + "Leave empty to fall back to the legacy cube Y-scale growth below.")] + [SerializeField] private ConstructionPhaseSet defaultPhaseSet; + + [Header("Construction stages (cube fallback)")] + [Tooltip("FALLBACK ONLY (no phase set assigned): number of discrete growth stages " + + "while constructing. 4 = 1/4 → 2/4 → 3/4 → 4/4 height.")] [SerializeField] private int stageCount = 4; - [Tooltip("Y-scale applied to scaleTarget when Stage == Queued. Visually " + - "distinct from any constructing height so the queued ghost reads as " + - "'intent, not progress'.")] + [Tooltip("FALLBACK ONLY: Y-scale applied to scaleTarget when Stage == Queued. " + + "Visually distinct from any constructing height so the queued ghost reads " + + "as 'intent, not progress'.")] [SerializeField] private float queuedYScale = 0.15f; // ----- Networked state -------------------------------------------- - // Replicated definition name so clients can resolve the source TowerDefinition - // (matches TowerInstance's pattern). Used for footprint size only — the visual - // prefab is the same regardless of tower type for now. - private readonly NetworkVariable definitionName = - new NetworkVariable( - default, - readPerm: NetworkVariableReadPermission.Everyone, - writePerm: NetworkVariableWritePermission.Server); - // Replicated owner slot for color tinting. Mirrors TowerInstance. private readonly NetworkVariable ownerSlot = new NetworkVariable( @@ -232,7 +229,6 @@ namespace TD.Gameplay // ----- Pre-spawn init data (server) ------------------------------- - private string pendingDefName; private PlayerSlot pendingOwner = PlayerSlot.None; private float pendingBuildTime; private Vector2Int pendingAnchor; @@ -240,6 +236,17 @@ namespace TD.Gameplay private int pendingGoldSpent; private bool hasPendingInit; + // ----- Construction-phase runtime (local, all peers) -------------- + + // Resolved on spawn from the tower's ConstructionPhases or the defaultPhaseSet. + // When non-null with PhaseCount > 0, the build site swaps between these + // instantiated phase prefabs instead of Y-scaling the fallback cube. + private ConstructionPhaseSet effectivePhaseSet; + private GameObject[] phaseInstances; // one instantiated child per phase + private MeshRenderer[][] phaseRenderers; // cached renderers per phase, for tinting + private int activePhaseIndex = -1; + private bool usePhases; + // ----- Lifecycle -------------------------------------------------- /// @@ -261,7 +268,6 @@ namespace TD.Gameplay return; } - pendingDefName = def != null ? def.name : string.Empty; pendingOwner = owner; pendingBuildTime = def != null ? def.BuildTime : 0f; pendingAnchor = anchorTile; @@ -281,7 +287,6 @@ namespace TD.Gameplay // message so clients see correct values on their first OnNetworkSpawn. if (IsServer && hasPendingInit) { - definitionName.Value = new FixedString64Bytes(pendingDefName ?? string.Empty); ownerSlot.Value = pendingOwner; buildTime.Value = pendingBuildTime; anchor.Value = pendingAnchor; @@ -290,6 +295,10 @@ namespace TD.Gameplay hasPendingInit = false; } + // Resolve and instantiate the construction-phase visuals (or fall back to the + // cube). Depends on towerTypeId, which is replicated by now on every peer. + ResolvePhaseSet(); + // Subscribe to value changes so visual updates are reactive. currentStage.OnValueChanged += HandleStageChanged; @@ -344,14 +353,25 @@ namespace TD.Gameplay private void Update() { - // While constructing, smoothly interpolate Y-scale through the stages - // based on server time. This runs on every peer (server + clients) so - // visuals stay synchronized regardless of who's looking. - // Paused stage does NOT update — Y-scale is frozen at the pause point. + // While constructing, advance the visual based on server time. Runs on every + // peer so visuals stay synchronized. Paused does NOT update — the visual is + // frozen at the pause point. if (currentStage.Value != BuildStage.Constructing) return; - float yScale = ComputeConstructingYScale(); - ApplyYScale(yScale); + if (usePhases) + { + int index = PhaseIndexFromProgress(); + if (index != activePhaseIndex) + { + ShowPhase(index); + // Renderers changed with the swap — re-apply material + owner tint. + ApplyStageMaterialAndTint(BuildStage.Constructing); + } + return; + } + + // Fallback: smoothly grow the cube's Y-scale through the stages. + ApplyYScale(ComputeConstructingYScale()); } // ----- Server API ------------------------------------------------- @@ -498,33 +518,117 @@ namespace TD.Gameplay } private void ApplyStageVisual(BuildStage stage) + { + if (usePhases) + { + // Queued shows the first phase; constructing/paused show the phase + // matching current progress. ShowPhase repoints tintedRenderers at the + // active phase instance, so material + tint are applied AFTER it. + int index = stage == BuildStage.Queued ? 0 : PhaseIndexFromProgress(); + ShowPhase(index); + ApplyStageMaterialAndTint(stage); + return; + } + + // Fallback: legacy cube Y-scale growth (no phase set wired). + ApplyStageMaterialAndTint(stage); + switch (stage) + { + case BuildStage.Queued: ApplyYScale(queuedYScale); break; + case BuildStage.Constructing: ApplyYScale(ComputeConstructingYScale()); break; + // ComputePausedYScale uses accumulatedConstructionTime alone when + // constructionStartServerTime is -1 (the pause sentinel). + case BuildStage.Paused: ApplyYScale(ComputePausedYScale()); break; + } + } + + // Applies the stage's material + owner tint to whatever renderers are currently + // active — the cube fallback, or the active phase instance's renderers. Paused + // falls back to the constructing material if no paused material is assigned. + private void ApplyStageMaterialAndTint(BuildStage stage) { switch (stage) { case BuildStage.Queued: - SwapMaterial(queuedMaterial); - ApplyYScale(queuedYScale); + SwapMaterial(queuedMaterial); // green ghost — no owner tint break; - case BuildStage.Constructing: SwapMaterial(constructingMaterial); ApplyOwnerTint(); - ApplyYScale(ComputeConstructingYScale()); break; - case BuildStage.Paused: - // Use paused material if assigned; fall back to constructing - // material if not (still readable, just less distinct). SwapMaterial(pausedMaterial != null ? pausedMaterial : constructingMaterial); ApplyOwnerTint(); - // Freeze Y-scale at whatever the accumulated progress represents. - // ComputeConstructingYScale uses accumulatedConstructionTime alone - // when constructionStartServerTime is -1 (the pause sentinel). - ApplyYScale(ComputePausedYScale()); break; } } + // ----- Construction phases ---------------------------------------- + + // Resolves the effective phase set (tower's own, else the project default) and + // instantiates one inactive child per phase. Sets usePhases=false (cube fallback) + // if neither set has any phases. Runs on every peer in OnNetworkSpawn. + private void ResolvePhaseSet() + { + var def = TowerPlacementManager.GetDefinition(towerTypeId.Value); + effectivePhaseSet = + (def != null && def.ConstructionPhases != null && def.ConstructionPhases.PhaseCount > 0) + ? def.ConstructionPhases + : defaultPhaseSet; + + int n = effectivePhaseSet != null ? effectivePhaseSet.PhaseCount : 0; + if (n <= 0) + { + usePhases = false; + return; + } + + usePhases = true; + phaseInstances = new GameObject[n]; + phaseRenderers = new MeshRenderer[n][]; + + // Hide the legacy fallback cube — the instantiated phase prefabs replace it. + if (scaleTarget != null) scaleTarget.gameObject.SetActive(false); + + for (int i = 0; i < n; i++) + { + var prefab = effectivePhaseSet.GetPhase(i); + if (prefab == null) continue; + + var go = Instantiate(prefab, transform); + go.transform.localPosition = Vector3.zero; + go.transform.localRotation = Quaternion.identity; + go.SetActive(false); + + phaseInstances[i] = go; + phaseRenderers[i] = go.GetComponentsInChildren(true); + } + } + + // Activates only phase and points tintedRenderers at its + // renderers so the existing material/tint helpers drive the active phase. + private void ShowPhase(int index) + { + if (phaseInstances == null) return; + index = Mathf.Clamp(index, 0, phaseInstances.Length - 1); + + for (int i = 0; i < phaseInstances.Length; i++) + if (phaseInstances[i] != null) + phaseInstances[i].SetActive(i == index); + + activePhaseIndex = index; + tintedRenderers = phaseRenderers[index]; + } + + // Maps current normalized progress to a phase index (0..PhaseCount-1). + private int PhaseIndexFromProgress() + { + int n = effectivePhaseSet != null ? effectivePhaseSet.PhaseCount : 1; + if (n <= 1) return 0; + float p = ComputeProgressNormalized(); + return Mathf.Clamp(Mathf.FloorToInt(p * n), 0, n - 1); + } + // Stage index 0..stageCount-1 based on elapsed server time PLUS any accumulated // time from previous Constructing runs (resume support). // Returned Y-scale is (stageIndex + 1) / stageCount, so stage 0 = 1/4, diff --git a/Assets/_Project/Scripts/Gameplay/RaceDefinition.cs b/Assets/_Project/Scripts/Gameplay/RaceDefinition.cs index 83aec06..b809684 100644 --- a/Assets/_Project/Scripts/Gameplay/RaceDefinition.cs +++ b/Assets/_Project/Scripts/Gameplay/RaceDefinition.cs @@ -68,8 +68,8 @@ namespace TD.Gameplay "the prefab based on the player's RaceSelection.")] public GameObject BuilderPrefab; - [Tooltip("STUB (Phase 1.8): tower roster available to this race. TowerRegistry will " + - "filter to this list when the active player belongs to this race.")] + [Tooltip("STUB: tower roster available to this race/builder. The draft pool or " + + "starting deck will eventually filter to this list for this race.")] public TowerDefinition[] Towers; } } diff --git a/Assets/_Project/Scripts/Gameplay/TowerInstance.cs b/Assets/_Project/Scripts/Gameplay/TowerInstance.cs index b99c0c2..724776e 100644 --- a/Assets/_Project/Scripts/Gameplay/TowerInstance.cs +++ b/Assets/_Project/Scripts/Gameplay/TowerInstance.cs @@ -1,5 +1,4 @@ // Assets/_Project/Scripts/Gameplay/TowerInstance.cs -using Unity.Collections; using Unity.Netcode; using UnityEngine; using TD.Core; @@ -32,11 +31,11 @@ namespace TD.Gameplay /// are idempotent writes, so double-stamping is safe. /// /// Definition reference replication. TowerDefinition assets live in the - /// project on all clients. We replicate the asset by name via a - /// holding a FixedString64Bytes, then look - /// up the asset locally. This avoids serializing the full ScriptableObject over the - /// network. The lookup uses a singleton that must be - /// present in the scene. (Temporary: will be driven by RaceDefinition in Path E.) + /// project on all clients. We replicate the tower's TowerTypeId (its index in + /// 's catalog) via a , + /// then resolve the full asset locally with . + /// This avoids serializing the ScriptableObject over the network and reuses the same + /// identifier placement and the deck already use — a single source of truth. /// /// Combat. No combat logic here yet. Combat fields live stubbed on /// ; they will be consumed by a future @@ -58,12 +57,13 @@ namespace TD.Gameplay // ----- Networked state ------------------------------------------------ - // The name of the TowerDefinition asset for this tower. Replicated so all - // clients can look up the full definition locally without sending the whole - // ScriptableObject over the wire. - private readonly NetworkVariable definitionName = - new NetworkVariable( - default, + // The TowerTypeId (index into TowerPlacementManager's catalog) for this tower. + // Replicated so every client resolves the full definition locally via the catalog — + // the same identifier placement and the deck already use. 0 = unset/invalid (the + // reserved catalog index). Replaces the old replicate-by-name + TowerRegistry path. + private readonly NetworkVariable definitionTypeId = + new NetworkVariable( + 0, readPerm: NetworkVariableReadPermission.Everyone, writePerm: NetworkVariableWritePermission.Server); @@ -95,8 +95,8 @@ namespace TD.Gameplay // ----- Local resolved state ------------------------------------------- - // Resolved on every client in OnNetworkSpawn from definitionName. - // Null if the lookup fails (missing TowerRegistry or unknown name). + // Resolved on every client in OnNetworkSpawn from definitionTypeId via the catalog. + // Null if the lookup fails (TypeId not in the catalog on this peer). private TowerDefinition resolvedDefinition; // ----- Pre-spawn initialization data ---------------------------------- @@ -192,7 +192,7 @@ namespace TD.Gameplay hasPendingInit = true; // Cache the resolved definition on the server immediately — clients - // will resolve via the registry lookup once definitionName arrives. + // resolve from the replicated TowerTypeId via the catalog once it arrives. resolvedDefinition = def; } @@ -206,7 +206,17 @@ namespace TD.Gameplay // every client sees correct values on its very first OnNetworkSpawn. if (IsServer && hasPendingInit) { - definitionName.Value = new FixedString64Bytes(pendingDefinition.name); + // Resolve the catalog index for this definition and replicate that. + int typeId = 0; + var pm = TowerPlacementManager.Instance; + if (pm == null || !pm.TryGetTypeId(pendingDefinition, out typeId)) + { + Debug.LogError($"[TowerInstance] Could not resolve a TowerTypeId for " + + $"'{pendingDefinition?.name}'. Ensure it is in the " + + $"TowerPlacementManager catalog (towerDefinitions)."); + } + + definitionTypeId.Value = typeId; anchorTile.Value = pendingAnchor; ownerSlot.Value = pendingOwner; @@ -331,27 +341,13 @@ namespace TD.Gameplay // Already resolved (server path via InitializeServer). if (resolvedDefinition != null) return; - string defName = definitionName.Value.ToString(); - if (string.IsNullOrEmpty(defName)) - { - Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " + - $"definitionName is empty. Cannot resolve TowerDefinition."); - return; - } - - var registry = TowerRegistry.Instance; - if (registry == null) - { - Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " + - $"No TowerRegistry found in scene. Cannot resolve '{defName}'."); - return; - } - - resolvedDefinition = registry.Get(defName); + int typeId = definitionTypeId.Value; + resolvedDefinition = TowerPlacementManager.GetDefinition(typeId); if (resolvedDefinition == null) { Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " + - $"TowerRegistry does not contain a definition named '{defName}'."); + $"no TowerDefinition for TypeId {typeId}. Is the " + + $"TowerPlacementManager catalog populated on this peer?"); } } diff --git a/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs b/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs deleted file mode 100644 index 389df11..0000000 --- a/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Assets/_Project/Scripts/Gameplay/TowerRegistry.cs -using System.Collections.Generic; -using UnityEngine; -using TD.Towers; - -namespace TD.Gameplay -{ - /// - /// Scene singleton that holds every available in the - /// current match and lets any code look one up by asset name. - /// - /// - /// Why this exists. replicates a tower's - /// definition by name (a FixedString64Bytes over the network), then resolves - /// the full ScriptableObject locally on every client. TowerRegistry is the lookup - /// table that makes that resolution possible without hard-coding asset paths. - /// - /// Registration. Drag every asset into the - /// Definitions list on this component in the inspector. Assets can live anywhere - /// in the project — no special folder required. - /// - /// Path E upgrade path. In Path E the registry will filter to only the - /// definitions belonging to the active match's RaceDefinition rosters. For now - /// all assigned assets are registered. - /// - /// Plain MonoBehaviour. Not a NetworkBehaviour — the registry is - /// identical on every peer (same assets, same names), so there is nothing to sync. - /// - public class TowerRegistry : MonoBehaviour - { - // ----- Singleton -------------------------------------------------- - - /// - /// The active TowerRegistry. Null before Awake or after the scene unloads. - /// Always null-check before use. - /// - public static TowerRegistry Instance { get; private set; } - - // ----- Inspector -------------------------------------------------- - - [Tooltip("All TowerDefinition assets available in this match. " + - "Drag assets here from Assets/_Project/Data/TowerDefinitions/ " + - "(or wherever they live). Asset name is used as the registry key.")] - [SerializeField] private TowerDefinition[] definitions; - - // ----- Internal lookup table -------------------------------------- - - // Keyed by TowerDefinition.name (the asset name, not DisplayName). - private readonly Dictionary byName - = new Dictionary(); - - // ----- Lifecycle -------------------------------------------------- - - private void Awake() - { - if (Instance != null && Instance != this) - { - Debug.LogError("[TowerRegistry] Multiple instances detected. " + - "Only one TowerRegistry should exist per scene."); - return; - } - Instance = this; - - BuildLookupTable(); - } - - private void OnDestroy() - { - if (Instance == this) Instance = null; - } - - // ----- Public API ------------------------------------------------- - - /// - /// Returns the whose asset name equals - /// , or null if no match is found. - /// - public TowerDefinition Get(string assetName) - { - byName.TryGetValue(assetName, out var def); - return def; - } - - /// - /// Returns all registered tower definitions. Enumerates the internal - /// dictionary values — do not modify the returned collection. - /// - public IEnumerable All => byName.Values; - - // ----- Private ---------------------------------------------------- - - private void BuildLookupTable() - { - byName.Clear(); - - if (definitions == null || definitions.Length == 0) - { - Debug.LogWarning("[TowerRegistry] No TowerDefinition assets assigned. " + - "Drag assets into the Definitions list on the TowerRegistry component."); - return; - } - - foreach (var def in definitions) - { - if (def == null) continue; - - if (byName.ContainsKey(def.name)) - { - Debug.LogWarning($"[TowerRegistry] Duplicate asset name '{def.name}'. " + - $"Only the first entry will be used. Rename one of the assets."); - continue; - } - - byName[def.name] = def; - } - - Debug.Log($"[TowerRegistry] Registered {byName.Count} tower definition(s)."); - } - } -} diff --git a/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs.meta b/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs.meta deleted file mode 100644 index 6a7a87c..0000000 --- a/Assets/_Project/Scripts/Gameplay/TowerRegistry.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a9dc0fbbe4422bc479ab8db7658c082b \ No newline at end of file diff --git a/Assets/_Project/Scripts/Towers/ConstructionPhaseSet.cs b/Assets/_Project/Scripts/Towers/ConstructionPhaseSet.cs new file mode 100644 index 0000000..46c7c61 --- /dev/null +++ b/Assets/_Project/Scripts/Towers/ConstructionPhaseSet.cs @@ -0,0 +1,32 @@ +// Assets/_Project/Scripts/Towers/ConstructionPhaseSet.cs +using UnityEngine; + +namespace TD.Towers +{ + /// + /// An ordered set of construction-stage visuals a build site transitions between as a + /// tower is built — typically 3 assets (early → mid → late). A tower references its own + /// set via ; build sites fall back to a + /// project-default set when a tower doesn't define one. + /// + /// + /// The entries are shape prefabs only. BuildSiteVisual applies the + /// queued (green) / constructing / paused materials and the owner tint on top, so the + /// phase assets don't need their own build-state materials. Keep them simple + /// (single material) — the build site swaps the active phase's renderer material. + /// + [CreateAssetMenu(fileName = "ConstructionPhaseSet", menuName = "TD/Construction Phase Set")] + public class ConstructionPhaseSet : ScriptableObject + { + [Tooltip("Ordered construction-stage prefabs. Shown in sequence as the build " + + "progresses — e.g. 3 entries = early / mid / late thirds of the build.")] + [SerializeField] private GameObject[] phases; + + /// Number of construction phases in this set. + public int PhaseCount => phases != null ? phases.Length : 0; + + /// The phase prefab at , or null if out of range. + public GameObject GetPhase(int index) + => (phases != null && index >= 0 && index < phases.Length) ? phases[index] : null; + } +} diff --git a/Assets/_Project/Scripts/Towers/ConstructionPhaseSet.cs.meta b/Assets/_Project/Scripts/Towers/ConstructionPhaseSet.cs.meta new file mode 100644 index 0000000..4556cad --- /dev/null +++ b/Assets/_Project/Scripts/Towers/ConstructionPhaseSet.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c8f2a4e1b9d6437052a1e8c4f7b3d069 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Scripts/Towers/TowerDefinition.cs b/Assets/_Project/Scripts/Towers/TowerDefinition.cs index 8d80828..24232ad 100644 --- a/Assets/_Project/Scripts/Towers/TowerDefinition.cs +++ b/Assets/_Project/Scripts/Towers/TowerDefinition.cs @@ -68,6 +68,11 @@ namespace TD.Towers "replace with a real mesh when art is available.")] public GameObject TowerPrefab; + [Tooltip("Optional per-tower construction-stage visuals shown while this tower is " + + "being built. Leave empty to use the project-default set assigned on the " + + "BuildSiteVisual prefab.")] + public ConstructionPhaseSet ConstructionPhases; + [Tooltip("Icon shown on this tower's tile in the HUD build menu. Auto-generated from " + "the TowerPrefab by the 'TD/Generate Tower Icons' menu item, or assign a " + "Sprite manually. Null falls back to the grey placeholder icon.")] diff --git a/Project_Context.md b/Project_Context.md index 7242383..b9552bb 100644 --- a/Project_Context.md +++ b/Project_Context.md @@ -90,7 +90,7 @@ Unity **6.4 (6000.4.4f1)**, URP, IL2CPP, .NET Standard 2.1, Linear color space, - **Visuals are placeholder** throughout (primitive/cone towers, sourced enemy models). See roadmap art TODOs. - **Enemies lack idle animation support** — needs adding across all enemy prefabs. - **Tower footprint visuals don't fill their 2×2 space** — the cone/mesh sits centered and reads smaller than the tile area it occupies; should visually reflect the full footprint. -- **Two redundant tower lists:** `TowerPlacementManager.towerDefinitions[]` (by-index catalog) and `TowerRegistry.definitions[]` (by-name, for instance resolution). Candidate for consolidation. +- **Tower catalog is now a single source of truth:** `TowerPlacementManager.towerDefinitions[]` (by-index, = `TowerTypeId`). `TowerInstance` replicates that index and resolves via `TowerPlacementManager.GetDefinition`. *(The old by-name `TowerRegistry` was removed 2026-06-24 — add new towers only to `towerDefinitions` + `DraftPool`.)* - **Catalog index 0 is a reserved sentinel** (valid `TowerTypeId`s start at 1) — easy to forget when wiring the catalog in the inspector. - **Catalog-index identifiers aren't session-stable** — blocks cross-match persistence until a stable ID is added. - **Paint system frozen**; **Race vs Builder** naming unresolved; the gold **"Buy Roll available any time"** rule is provisional and may change.