feature/tesla-coil-tower #8

Merged
matt merged 5 commits from feature/tesla-coil-tower into main 2026-06-25 15:57:55 -07:00
16 changed files with 594 additions and 36 deletions
Showing only changes of commit 7b93137216 - Show all commits

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3f1ef90255e065249a86a838844f7be2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -298,6 +298,7 @@ MonoBehaviour:
queuedMaterial: {fileID: 2100000, guid: f8d951a6841d3f74098bb31255379774, type: 2} queuedMaterial: {fileID: 2100000, guid: f8d951a6841d3f74098bb31255379774, type: 2}
constructingMaterial: {fileID: 2100000, guid: 88f1dd7b174716645953857b38fb6948, type: 2} constructingMaterial: {fileID: 2100000, guid: 88f1dd7b174716645953857b38fb6948, type: 2}
pausedMaterial: {fileID: 0} pausedMaterial: {fileID: 0}
defaultPhaseSet: {fileID: 11400000, guid: ed951755c09d6d0479b4fd924b6915da, type: 2}
stageCount: 4 stageCount: 4
queuedYScale: 0.15 queuedYScale: 0.15
--- !u!65 &6259437393614329336 --- !u!65 &6259437393614329336

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 82e418e7a5a804747a9a4865648b7793
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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}

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ed951755c09d6d0479b4fd924b6915da
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e7be86c29087e99439507b508425b612
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c6dabc822d0afb746adc08fb8602c2b4
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d00c71c0cfc7e8744b9f8c1d48c2d256
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,5 +1,4 @@
// Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs // Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs
using Unity.Collections;
using Unity.Netcode; using Unity.Netcode;
using UnityEngine; using UnityEngine;
using TD.Core; using TD.Core;
@ -63,27 +62,25 @@ namespace TD.Gameplay
"Suggested: muted/grey-tinted variant of the constructing material.")] "Suggested: muted/grey-tinted variant of the constructing material.")]
[SerializeField] private Material pausedMaterial; [SerializeField] private Material pausedMaterial;
[Header("Construction stages")] [Header("Construction phases")]
[Tooltip("Number of discrete growth stages while constructing. 4 matches the " + [Tooltip("Project-default construction-stage visuals used when the tower being built " +
"design doc (1/4 → 2/4 → 3/4 → 4/4 height).")] "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; [SerializeField] private int stageCount = 4;
[Tooltip("Y-scale applied to scaleTarget when Stage == Queued. Visually " + [Tooltip("FALLBACK ONLY: Y-scale applied to scaleTarget when Stage == Queued. " +
"distinct from any constructing height so the queued ghost reads as " + "Visually distinct from any constructing height so the queued ghost reads " +
"'intent, not progress'.")] "as 'intent, not progress'.")]
[SerializeField] private float queuedYScale = 0.15f; [SerializeField] private float queuedYScale = 0.15f;
// ----- Networked state -------------------------------------------- // ----- 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<FixedString64Bytes> definitionName =
new NetworkVariable<FixedString64Bytes>(
default,
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Server);
// Replicated owner slot for color tinting. Mirrors TowerInstance. // Replicated owner slot for color tinting. Mirrors TowerInstance.
private readonly NetworkVariable<PlayerSlot> ownerSlot = private readonly NetworkVariable<PlayerSlot> ownerSlot =
new NetworkVariable<PlayerSlot>( new NetworkVariable<PlayerSlot>(
@ -232,7 +229,6 @@ namespace TD.Gameplay
// ----- Pre-spawn init data (server) ------------------------------- // ----- Pre-spawn init data (server) -------------------------------
private string pendingDefName;
private PlayerSlot pendingOwner = PlayerSlot.None; private PlayerSlot pendingOwner = PlayerSlot.None;
private float pendingBuildTime; private float pendingBuildTime;
private Vector2Int pendingAnchor; private Vector2Int pendingAnchor;
@ -240,6 +236,17 @@ namespace TD.Gameplay
private int pendingGoldSpent; private int pendingGoldSpent;
private bool hasPendingInit; 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 -------------------------------------------------- // ----- Lifecycle --------------------------------------------------
/// <summary> /// <summary>
@ -261,7 +268,6 @@ namespace TD.Gameplay
return; return;
} }
pendingDefName = def != null ? def.name : string.Empty;
pendingOwner = owner; pendingOwner = owner;
pendingBuildTime = def != null ? def.BuildTime : 0f; pendingBuildTime = def != null ? def.BuildTime : 0f;
pendingAnchor = anchorTile; pendingAnchor = anchorTile;
@ -281,7 +287,6 @@ namespace TD.Gameplay
// message so clients see correct values on their first OnNetworkSpawn. // message so clients see correct values on their first OnNetworkSpawn.
if (IsServer && hasPendingInit) if (IsServer && hasPendingInit)
{ {
definitionName.Value = new FixedString64Bytes(pendingDefName ?? string.Empty);
ownerSlot.Value = pendingOwner; ownerSlot.Value = pendingOwner;
buildTime.Value = pendingBuildTime; buildTime.Value = pendingBuildTime;
anchor.Value = pendingAnchor; anchor.Value = pendingAnchor;
@ -290,6 +295,10 @@ namespace TD.Gameplay
hasPendingInit = false; 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. // Subscribe to value changes so visual updates are reactive.
currentStage.OnValueChanged += HandleStageChanged; currentStage.OnValueChanged += HandleStageChanged;
@ -344,14 +353,25 @@ namespace TD.Gameplay
private void Update() private void Update()
{ {
// While constructing, smoothly interpolate Y-scale through the stages // While constructing, advance the visual based on server time. Runs on every
// based on server time. This runs on every peer (server + clients) so // peer so visuals stay synchronized. Paused does NOT update — the visual is
// visuals stay synchronized regardless of who's looking. // frozen at the pause point.
// Paused stage does NOT update — Y-scale is frozen at the pause point.
if (currentStage.Value != BuildStage.Constructing) return; if (currentStage.Value != BuildStage.Constructing) return;
float yScale = ComputeConstructingYScale(); if (usePhases)
ApplyYScale(yScale); {
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 ------------------------------------------------- // ----- Server API -------------------------------------------------
@ -498,33 +518,117 @@ namespace TD.Gameplay
} }
private void ApplyStageVisual(BuildStage stage) 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) switch (stage)
{ {
case BuildStage.Queued: case BuildStage.Queued:
SwapMaterial(queuedMaterial); SwapMaterial(queuedMaterial); // green ghost — no owner tint
ApplyYScale(queuedYScale);
break; break;
case BuildStage.Constructing: case BuildStage.Constructing:
SwapMaterial(constructingMaterial); SwapMaterial(constructingMaterial);
ApplyOwnerTint(); ApplyOwnerTint();
ApplyYScale(ComputeConstructingYScale());
break; break;
case BuildStage.Paused: 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); SwapMaterial(pausedMaterial != null ? pausedMaterial : constructingMaterial);
ApplyOwnerTint(); ApplyOwnerTint();
// Freeze Y-scale at whatever the accumulated progress represents.
// ComputeConstructingYScale uses accumulatedConstructionTime alone
// when constructionStartServerTime is -1 (the pause sentinel).
ApplyYScale(ComputePausedYScale());
break; 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<MeshRenderer>(true);
}
}
// Activates only phase <paramref name="index"/> 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 // Stage index 0..stageCount-1 based on elapsed server time PLUS any accumulated
// time from previous Constructing runs (resume support). // time from previous Constructing runs (resume support).
// Returned Y-scale is (stageIndex + 1) / stageCount, so stage 0 = 1/4, // Returned Y-scale is (stageIndex + 1) / stageCount, so stage 0 = 1/4,

View file

@ -0,0 +1,32 @@
// Assets/_Project/Scripts/Towers/ConstructionPhaseSet.cs
using UnityEngine;
namespace TD.Towers
{
/// <summary>
/// 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 <see cref="TowerDefinition.ConstructionPhases"/>; build sites fall back to a
/// project-default set when a tower doesn't define one.
/// </summary>
/// <remarks>
/// The entries are <b>shape</b> prefabs only. <c>BuildSiteVisual</c> 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.
/// </remarks>
[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;
/// <summary>Number of construction phases in this set.</summary>
public int PhaseCount => phases != null ? phases.Length : 0;
/// <summary>The phase prefab at <paramref name="index"/>, or null if out of range.</summary>
public GameObject GetPhase(int index)
=> (phases != null && index >= 0 && index < phases.Length) ? phases[index] : null;
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c8f2a4e1b9d6437052a1e8c4f7b3d069
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -68,6 +68,11 @@ namespace TD.Towers
"replace with a real mesh when art is available.")] "replace with a real mesh when art is available.")]
public GameObject TowerPrefab; 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 " + [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 " + "the TowerPrefab by the 'TD/Generate Tower Icons' menu item, or assign a " +
"Sprite manually. Null falls back to the grey placeholder icon.")] "Sprite manually. Null falls back to the grey placeholder icon.")]