diff --git a/Assets/_Project/Definitions/BuilderEffects.meta b/Assets/_Project/Definitions/BuilderEffects.meta
new file mode 100644
index 0000000..c619ee9
--- /dev/null
+++ b/Assets/_Project/Definitions/BuilderEffects.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3e6583debe83da83abe53dec4eb2454b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Definitions/BuilderEffects/GoldPerKillEffect.asset b/Assets/_Project/Definitions/BuilderEffects/GoldPerKillEffect.asset
new file mode 100644
index 0000000..3f1044b
--- /dev/null
+++ b/Assets/_Project/Definitions/BuilderEffects/GoldPerKillEffect.asset
@@ -0,0 +1,16 @@
+%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: 5ce77a7f27ed228a0ad0494dfc1a4749, type: 3}
+ m_Name: GoldPerKillEffect
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderEffects.GoldPerKillEffectDefinition
+ DisplayName: Gold Per Kill
+ BonusGoldPerKill: 1
diff --git a/Assets/_Project/Definitions/BuilderEffects/GoldPerKillEffect.asset.meta b/Assets/_Project/Definitions/BuilderEffects/GoldPerKillEffect.asset.meta
new file mode 100644
index 0000000..c0cfe61
--- /dev/null
+++ b/Assets/_Project/Definitions/BuilderEffects/GoldPerKillEffect.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 419fac0976a3e0a20827beff81826161
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Definitions/Draft/Draft_BuilderEffect_GoldPerKill.asset b/Assets/_Project/Definitions/Draft/Draft_BuilderEffect_GoldPerKill.asset
new file mode 100644
index 0000000..391470d
--- /dev/null
+++ b/Assets/_Project/Definitions/Draft/Draft_BuilderEffect_GoldPerKill.asset
@@ -0,0 +1,19 @@
+%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: 4eaef3264612070c48fbc6a987c89858, type: 3}
+ m_Name: Draft_BuilderEffect_GoldPerKill
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Draft.BuilderEffectDraftOption
+ DisplayName: Gold Per Kill
+ Description: Get more gold per kill
+ Icon: {fileID: 0}
+ Weight: 1
+ Kind: 0
diff --git a/Assets/_Project/Definitions/Draft/Draft_BuilderEffect_GoldPerKill.asset.meta b/Assets/_Project/Definitions/Draft/Draft_BuilderEffect_GoldPerKill.asset.meta
new file mode 100644
index 0000000..3d7d0d2
--- /dev/null
+++ b/Assets/_Project/Definitions/Draft/Draft_BuilderEffect_GoldPerKill.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 65ff25c1c8a89f7df8e88f71968c1c98
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/Player/Player.prefab b/Assets/_Project/Prefabs/Player/Player.prefab
index 666ff7b..33c1316 100644
--- a/Assets/_Project/Prefabs/Player/Player.prefab
+++ b/Assets/_Project/Prefabs/Player/Player.prefab
@@ -16,6 +16,7 @@ GameObject:
- component: {fileID: 2806524246861401760}
- component: {fileID: 2806524246861401799}
- component: {fileID: 2806524246861401801}
+ - component: {fileID: 5683786710272852339}
m_Layer: 0
m_Name: Player
m_TagString: Untagged
@@ -50,7 +51,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
- GlobalObjectIdHash: 1552073510
+ GlobalObjectIdHash: 121878297
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
@@ -129,7 +130,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b7e4c91a08f5d2461a3f8e6c25b09d74, type: 3}
- m_Name:
+ m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerTowerDeck
ShowTopMostFoldoutHeaderGroup: 1
--- !u!114 &2806524246861401801
@@ -142,6 +143,19 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2b8f4d1e6c3a497051d8e2f7a9c1b063, type: 3}
- m_Name:
+ m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Draft.PlayerDraft
ShowTopMostFoldoutHeaderGroup: 1
+--- !u!114 &5683786710272852339
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3493329038866903420}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 72dac94cde24af07ea0b5c77b228be34, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderUpgradeManager
+ ShowTopMostFoldoutHeaderGroup: 1
diff --git a/Assets/_Project/Scenes/Levels/9Player.unity b/Assets/_Project/Scenes/Levels/9Player.unity
index 14f1ebc..3264d21 100644
--- a/Assets/_Project/Scenes/Levels/9Player.unity
+++ b/Assets/_Project/Scenes/Levels/9Player.unity
@@ -27937,6 +27937,52 @@ BoxCollider:
serializedVersion: 3
m_Size: {x: 20, y: 1, z: 13}
m_Center: {x: 0, y: 0, z: 0}
+--- !u!1 &2079088111
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2079088113}
+ - component: {fileID: 2079088112}
+ m_Layer: 0
+ m_Name: BuilderEffectPool
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &2079088112
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2079088111}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 6226c6e672c5b358a9cdc2a1bee47daf, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderEffects.BuilderEffectPool
+ effects:
+ - {fileID: 11400000, guid: 419fac0976a3e0a20827beff81826161, type: 2}
+--- !u!4 &2079088113
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2079088111}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -5.96326, y: 0, z: -13.28562}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &2090447971
GameObject:
m_ObjectHideFlags: 0
@@ -28518,6 +28564,7 @@ MonoBehaviour:
options:
- {fileID: 11400000, guid: 6b1d3f8a2c5e4097b8a1d0c3e6f9b240, type: 2}
- {fileID: 11400000, guid: f4a2b8d1c6e93f05a7b2d4c8e1f6a930, type: 2}
+ - {fileID: 11400000, guid: 65ff25c1c8a89f7df8e88f71968c1c98, type: 2}
--- !u!4 &2139601601
Transform:
m_ObjectHideFlags: 0
@@ -28914,3 +28961,4 @@ SceneRoots:
- {fileID: 2139601601}
- {fileID: 176580462}
- {fileID: 516125087}
+ - {fileID: 2079088113}
diff --git a/Assets/_Project/Scripts/Core/BuilderEffectKind.cs b/Assets/_Project/Scripts/Core/BuilderEffectKind.cs
new file mode 100644
index 0000000..4a731f7
--- /dev/null
+++ b/Assets/_Project/Scripts/Core/BuilderEffectKind.cs
@@ -0,0 +1,14 @@
+// Assets/_Project/Scripts/Core/BuilderEffectKind.cs
+namespace TD.Core
+{
+ ///
+ /// Identifies which builder effect a
+ /// represents. Backs a
+ /// bitmask on , so keep this to 32 entries
+ /// or fewer (or widen the mask to ulong if it ever needs more).
+ ///
+ public enum BuilderEffectKind : byte
+ {
+ GoldPerKill = 0,
+ }
+}
diff --git a/Assets/_Project/Scripts/Core/BuilderEffectKind.cs.meta b/Assets/_Project/Scripts/Core/BuilderEffectKind.cs.meta
new file mode 100644
index 0000000..6eaad35
--- /dev/null
+++ b/Assets/_Project/Scripts/Core/BuilderEffectKind.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4e5626ef347960fd2880acfcefa7cc38
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderEffects.meta b/Assets/_Project/Scripts/Gameplay/BuilderEffects.meta
new file mode 100644
index 0000000..26d9048
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuilderEffects.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: dd9f1f667cfdcbf6d9ee44acc270f128
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectDefinition.cs b/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectDefinition.cs
new file mode 100644
index 0000000..12be8ff
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectDefinition.cs
@@ -0,0 +1,34 @@
+// Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectDefinition.cs
+using UnityEngine;
+using TD.Core;
+
+namespace TD.Gameplay.BuilderEffects
+{
+ ///
+ /// Base class for one builder effect — a passive perk that applies to every tower a
+ /// player's Builder constructs (e.g. "kills yield bonus gold"). Granted via the draft
+ /// () and tracked per-player by
+ /// .
+ ///
+ ///
+ /// One asset per kind. is fixed per subclass (each concrete
+ /// type overrides it to its own ), so there's no way to
+ /// mis-tag an asset in the inspector. uses it to build a
+ /// fixed-size, enum-indexed lookup table — no scanning to resolve a kind to its data.
+ ///
+ /// No shared "Apply" contract. Concrete subclasses (e.g.
+ /// ) carry whatever payload they need. Effects with
+ /// different application shapes — a simple value read at an existing game-event hook vs.
+ /// behavior that needs its own component on the tower — are still just subclasses; consumers
+ /// query the concrete type they care about directly (see
+ /// ).
+ ///
+ public abstract class BuilderEffectDefinition : ScriptableObject
+ {
+ /// Which builder effect this asset's data belongs to.
+ public abstract BuilderEffectKind Kind { get; }
+
+ [Tooltip("Name shown wherever a player's active builder effects are listed.")]
+ public string DisplayName;
+ }
+}
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectDefinition.cs.meta b/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectDefinition.cs.meta
new file mode 100644
index 0000000..039a631
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectDefinition.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3a939cb6af19202e1a9bddf01d9f1857
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectPool.cs b/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectPool.cs
new file mode 100644
index 0000000..0c926e1
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectPool.cs
@@ -0,0 +1,66 @@
+// Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectPool.cs
+using System;
+using UnityEngine;
+using TD.Core;
+
+namespace TD.Gameplay.BuilderEffects
+{
+ ///
+ /// Scene singleton holding every available this match,
+ /// authored as a flat array in the inspector for convenience. Internally it builds a
+ /// fixed-size table indexed by so lookups are a single array
+ /// access, not a scan.
+ ///
+ ///
+ /// Plain MonoBehaviour: identical on every peer (same assets), so there is nothing to sync.
+ /// The server reads it to resolve grants; clients read it to render an "active effects" list.
+ ///
+ public class BuilderEffectPool : MonoBehaviour
+ {
+ public static BuilderEffectPool Instance { get; private set; }
+
+ [Tooltip("Every BuilderEffectDefinition asset available this match. One entry per " +
+ "BuilderEffectKind — order doesn't matter, Kind on the asset itself decides " +
+ "its slot.")]
+ [SerializeField] private BuilderEffectDefinition[] effects;
+
+ // Fixed-size, enum-indexed lookup built once in Awake. Sized to the enum's entry count,
+ // not authored count, so an out-of-range Kind is a compile-time impossibility rather
+ // than a bounds check we'd otherwise need on every Get().
+ private BuilderEffectDefinition[] byKind;
+
+ private void Awake()
+ {
+ if (Instance != null && Instance != this)
+ {
+ Debug.LogError("[BuilderEffectPool] Multiple instances detected. Only one per scene.");
+ return;
+ }
+ Instance = this;
+
+ int kindCount = Enum.GetValues(typeof(BuilderEffectKind)).Length;
+ byKind = new BuilderEffectDefinition[kindCount];
+ if (effects == null) return;
+
+ for (int i = 0; i < effects.Length; i++)
+ {
+ var def = effects[i];
+ if (def == null) continue;
+ byKind[(int)def.Kind] = def;
+ }
+ }
+
+ private void OnDestroy()
+ {
+ if (Instance == this) Instance = null;
+ }
+
+ /// Returns the effect asset for , or null if none is
+ /// authored in this pool.
+ public BuilderEffectDefinition Get(BuilderEffectKind kind)
+ {
+ int i = (int)kind;
+ return (byKind != null && i >= 0 && i < byKind.Length) ? byKind[i] : null;
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectPool.cs.meta b/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectPool.cs.meta
new file mode 100644
index 0000000..438b507
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuilderEffects/BuilderEffectPool.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6226c6e672c5b358a9cdc2a1bee47daf
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderEffects/GoldPerKillEffectDefinition.cs b/Assets/_Project/Scripts/Gameplay/BuilderEffects/GoldPerKillEffectDefinition.cs
new file mode 100644
index 0000000..775d4c0
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuilderEffects/GoldPerKillEffectDefinition.cs
@@ -0,0 +1,22 @@
+// Assets/_Project/Scripts/Gameplay/BuilderEffects/GoldPerKillEffectDefinition.cs
+using UnityEngine;
+using TD.Core;
+
+namespace TD.Gameplay.BuilderEffects
+{
+ ///
+ /// Builder effect #1 — every tower the player owns yields extra gold per kill.
+ /// Read by , which
+ /// queries when awarding kill gold.
+ ///
+ [CreateAssetMenu(fileName = "GoldPerKillEffect", menuName = "TD/Builder Effects/Gold Per Kill")]
+ public class GoldPerKillEffectDefinition : BuilderEffectDefinition
+ {
+ public override BuilderEffectKind Kind => BuilderEffectKind.GoldPerKill;
+
+ [Tooltip("Flat bonus gold awarded on top of the normal kill reward, for every kill by " +
+ "any of this player's towers.")]
+ [Min(0)]
+ public int BonusGoldPerKill = 1;
+ }
+}
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderEffects/GoldPerKillEffectDefinition.cs.meta b/Assets/_Project/Scripts/Gameplay/BuilderEffects/GoldPerKillEffectDefinition.cs.meta
new file mode 100644
index 0000000..f6342a3
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuilderEffects/GoldPerKillEffectDefinition.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5ce77a7f27ed228a0ad0494dfc1a4749
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderUpgradeManager.cs b/Assets/_Project/Scripts/Gameplay/BuilderUpgradeManager.cs
new file mode 100644
index 0000000..0013ef9
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuilderUpgradeManager.cs
@@ -0,0 +1,125 @@
+// Assets/_Project/Scripts/Gameplay/BuilderUpgradeManager.cs
+using System.Collections.Generic;
+using Unity.Netcode;
+using UnityEngine;
+using TD.Core;
+using TD.Gameplay.BuilderEffects;
+
+namespace TD.Gameplay
+{
+ ///
+ /// Per-player set of granted builder effects. Lives on the Player prefab alongside
+ /// , , and
+ /// .
+ ///
+ ///
+ /// Bitmask, not a list. A builder effect carries no per-grant state (no level,
+ /// no enable/disable toggle) — it's either granted or it isn't — so the granted set is one
+ /// bitmask keyed by rather
+ /// than a NetworkList. "Has effect" and "grant effect" are both O(1) bit ops.
+ ///
+ /// Granted via draft.
+ /// calls when a player picks it. Effects are permanent for
+ /// the match — there is no revoke.
+ ///
+ /// Query, don't snapshot. Consumers (e.g. awarding
+ /// kill gold) query this manager live at the point of use rather than towers caching their
+ /// own copy — every tower a player owns benefits immediately, including ones built before
+ /// the effect was granted.
+ ///
+ /// Generic accessor, not one method per effect.
+ /// is the only way to read an effect's tunable data.
+ /// This keeps the API surface fixed as the effect roster grows — a new effect kind adds a
+ /// call site where it's consumed (e.g. a new line in WaveManager), not a new method
+ /// here.
+ ///
+ public class BuilderUpgradeManager : NetworkBehaviour
+ {
+ // ----- Static registry (mirrors PlayerGoldManager / PlayerTowerDeck) -----
+
+ private static readonly Dictionary s_byClientId
+ = new Dictionary();
+
+ /// Returns the manager owned by the given client, or null.
+ public static BuilderUpgradeManager GetForClient(ulong clientId)
+ {
+ s_byClientId.TryGetValue(clientId, out var mgr);
+ return mgr;
+ }
+
+ /// Convenience: the local client's own manager.
+ public static BuilderUpgradeManager Local
+ {
+ get
+ {
+ var nm = NetworkManager.Singleton;
+ if (nm == null || !nm.IsClient) return null;
+ return GetForClient(nm.LocalClientId);
+ }
+ }
+
+ // ----- Networked state --------------------------------------------
+
+ // One bit per BuilderEffectKind. readPerm Everyone (consistent with the other
+ // per-player managers); writePerm Server.
+ private readonly NetworkVariable grantedMask = new NetworkVariable(
+ value: 0,
+ readPerm: NetworkVariableReadPermission.Everyone,
+ writePerm: NetworkVariableWritePermission.Server
+ );
+
+ /// Fired on every peer when a new effect is granted.
+ public event System.Action OnUpgradesChanged;
+
+ // ----- NGO lifecycle ------------------------------------------------
+
+ public override void OnNetworkSpawn()
+ {
+ s_byClientId[OwnerClientId] = this;
+ grantedMask.OnValueChanged += HandleMaskChanged;
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ grantedMask.OnValueChanged -= HandleMaskChanged;
+
+ if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
+ s_byClientId.Remove(OwnerClientId);
+ }
+
+ private void HandleMaskChanged(uint previous, uint current) => OnUpgradesChanged?.Invoke();
+
+ // ----- Read API -----------------------------------------------------
+
+ /// True if this player has been granted the given effect.
+ public bool PlayerHasEffect(BuilderEffectKind kind) => (grantedMask.Value & BitFor(kind)) != 0;
+
+ ///
+ /// Returns this player's for
+ /// as , or null if the player doesn't have it (or the pool has no
+ /// asset for it). Callers extract whatever field they need — see the "generic accessor"
+ /// note above for why this isn't a per-effect method.
+ ///
+ public T GetEffectDefinition(BuilderEffectKind kind) where T : BuilderEffectDefinition
+ => PlayerHasEffect(kind) ? BuilderEffectPool.Instance?.Get(kind) as T : null;
+
+ // ----- Server mutation -----------------------------------------------
+
+ ///
+ /// Server-only: grants the given effect (a draft pick). No-op if already granted.
+ /// Returns true if it was actually granted.
+ ///
+ public bool ServerGrantEffect(BuilderEffectKind kind)
+ {
+ if (!IsServer) return false;
+
+ uint bit = BitFor(kind);
+ if ((grantedMask.Value & bit) != 0) return false;
+
+ grantedMask.Value |= bit;
+ return true;
+ }
+
+ private static uint BitFor(BuilderEffectKind kind) => 1u << (int)kind;
+ }
+}
diff --git a/Assets/_Project/Scripts/Gameplay/BuilderUpgradeManager.cs.meta b/Assets/_Project/Scripts/Gameplay/BuilderUpgradeManager.cs.meta
new file mode 100644
index 0000000..6da307b
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/BuilderUpgradeManager.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 72dac94cde24af07ea0b5c77b228be34
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/Draft/BuilderEffectDraftOption.cs b/Assets/_Project/Scripts/Gameplay/Draft/BuilderEffectDraftOption.cs
new file mode 100644
index 0000000..fb91dda
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/Draft/BuilderEffectDraftOption.cs
@@ -0,0 +1,37 @@
+// Assets/_Project/Scripts/Gameplay/Draft/BuilderEffectDraftOption.cs
+using UnityEngine;
+using TD.Core;
+using TD.Gameplay.BuilderEffects;
+
+namespace TD.Gameplay.Draft
+{
+ ///
+ /// Draft choice #2 — "gain a builder effect". Grants the player a passive effect that
+ /// applies to every tower they own, adding it to their .
+ ///
+ ///
+ /// Unlike , this carries the
+ /// directly rather than an asset reference — there's no id to resolve, the enum value is the
+ /// stable identifier and
+ /// already key off of.
+ ///
+ [CreateAssetMenu(fileName = "BuilderEffectOption", menuName = "TD/Draft/Builder Effect Option")]
+ public class BuilderEffectDraftOption : DraftOption
+ {
+ [Header("Payload")]
+ [Tooltip("The builder effect this option grants.")]
+ public BuilderEffectKind Kind;
+
+ public override bool IsValidFor(ulong clientId)
+ {
+ var upgrades = BuilderUpgradeManager.GetForClient(clientId);
+ return upgrades != null && !upgrades.PlayerHasEffect(Kind);
+ }
+
+ public override bool ServerApply(ulong clientId)
+ {
+ var upgrades = BuilderUpgradeManager.GetForClient(clientId);
+ return upgrades != null && upgrades.ServerGrantEffect(Kind);
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Gameplay/Draft/BuilderEffectDraftOption.cs.meta b/Assets/_Project/Scripts/Gameplay/Draft/BuilderEffectDraftOption.cs.meta
new file mode 100644
index 0000000..b0d473b
--- /dev/null
+++ b/Assets/_Project/Scripts/Gameplay/Draft/BuilderEffectDraftOption.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4eaef3264612070c48fbc6a987c89858
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Gameplay/WaveManager.cs b/Assets/_Project/Scripts/Gameplay/WaveManager.cs
index 5c8f7dd..d1d889b 100644
--- a/Assets/_Project/Scripts/Gameplay/WaveManager.cs
+++ b/Assets/_Project/Scripts/Gameplay/WaveManager.cs
@@ -3,6 +3,7 @@ using System.Collections;
using Unity.Netcode;
using UnityEngine;
using TD.Core;
+using TD.Gameplay.BuilderEffects;
using TD.Gameplay.Draft;
using TD.Levels;
using TD.UI;
@@ -524,22 +525,31 @@ namespace TD.Gameplay
var goldEntry = goldConfig?.GetWaveEntry(currentWaveIndex + 1);
if (goldEntry != null) killReward = goldEntry.GoldPerEnemy;
- // Award kill gold to the tower owner that landed the killing blow.
+ // Award kill gold to the tower owner that landed the killing blow. The builder's
+ // "gold per kill" effect (if granted) is queried live here rather than cached on
+ // the killing tower — see BuilderUpgradeManager's "query, don't snapshot" note.
PlayerSlot killerSlot = health.LastHitOwner;
+ int totalReward = killReward;
if (killerSlot != PlayerSlot.None && killReward > 0)
{
var pms = PlayerMatchState.GetForSlot(killerSlot);
if (pms != null)
+ {
+ var gpk = BuilderUpgradeManager.GetForClient(pms.OwnerClientId)
+ ?.GetEffectDefinition(BuilderEffectKind.GoldPerKill);
+ totalReward = killReward + (gpk?.BonusGoldPerKill ?? 0);
+
PlayerGoldManager.GetForClient(pms.OwnerClientId)
- ?.AwardGold(killReward);
+ ?.AwardGold(totalReward);
+ }
}
// Show a "+N" gold popup above the corpse on every peer. Capture the
// position here on the server — by the time the RPC fires on clients
// the death sequence will be moving the corpse, but the spawn point
// is good enough and we want the popup to anchor where the kill happened.
- if (killReward > 0)
- ShowGoldRewardClientRpc(health.transform.position, killReward);
+ if (totalReward > 0)
+ ShowGoldRewardClientRpc(health.transform.position, totalReward);
UnsubscribeEnemy(health);
DecrementAndCheckComplete();