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..5f07d31 --- /dev/null +++ b/Assets/_Project/Scripts/Gameplay/BuilderUpgradeManager.cs @@ -0,0 +1,122 @@ +// 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 + /// . + /// + /// + /// List of granted kinds. 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 a of values. Stored as + /// byte because NetworkList<T> requires T : IEquatable<T> + /// and plain enums don't implement that (see for the + /// same constraint on a struct); byte already does, for free. + /// + /// 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 -------------------------------------------- + + // Granted effect kinds, stored as byte (see class remarks for why not the enum + // directly). readPerm Everyone (consistent with the other per-player managers); + // writePerm Server — both are NetworkList's defaults. + private readonly NetworkList grantedKinds = new NetworkList(); + + /// 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; + grantedKinds.OnListChanged += HandleGrantedKindsChanged; + } + + public override void OnNetworkDespawn() + { + grantedKinds.OnListChanged -= HandleGrantedKindsChanged; + + if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this) + s_byClientId.Remove(OwnerClientId); + } + + private void HandleGrantedKindsChanged(NetworkListEvent change) => OnUpgradesChanged?.Invoke(); + + // ----- Read API ----------------------------------------------------- + + /// True if this player has been granted the given effect. + public bool PlayerHasEffect(BuilderEffectKind kind) => grantedKinds.Contains((byte)kind); + + /// + /// 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; + + byte value = (byte)kind; + if (grantedKinds.Contains(value)) return false; + + grantedKinds.Add(value); + return true; + } + } +} 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();