remove deprecated buff system
This commit is contained in:
parent
35ea90f71b
commit
330a929985
22 changed files with 11 additions and 615 deletions
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: eef6329e4294d86c6978ff9453b56956
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
%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: b01f9aea62ee8a1c1991bb2a097de224, type: 3}
|
|
||||||
m_Name: ExtraDamage25
|
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuffDefinition
|
|
||||||
DisplayName: 25% Increased Damage
|
|
||||||
Stat: 0
|
|
||||||
Multiplier: 1.25
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 9ec170e540b1f91cca762dcebe81a856
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 11400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
%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: 516e8faec948a4b3d977ec019376c438, type: 3}
|
|
||||||
m_Name: OffensiveBuffs
|
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuffCategory
|
|
||||||
DisplayName: Offensive Buffs
|
|
||||||
Cost: 5
|
|
||||||
Pool:
|
|
||||||
- {fileID: 11400000, guid: 9ec170e540b1f91cca762dcebe81a856, type: 2}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: d8ed3b9535538f7fc82be878cc307d26
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 11400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -20,7 +20,7 @@ MonoBehaviour:
|
||||||
Radius: 5
|
Radius: 5
|
||||||
enemyLayerMask:
|
enemyLayerMask:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Bits: 1024
|
m_Bits: 1152
|
||||||
areaVfxPrefab: {fileID: 1462673130280185047, guid: bf2f1f2a9a54e9162b80673e3f7eeaf6, type: 3}
|
areaVfxPrefab: {fileID: 1462673130280185047, guid: bf2f1f2a9a54e9162b80673e3f7eeaf6, type: 3}
|
||||||
areaSound:
|
areaSound:
|
||||||
clip: {fileID: 8300000, guid: 038ff3eab9aa0034bac26ccb648ef2a1, type: 3}
|
clip: {fileID: 8300000, guid: 038ff3eab9aa0034bac26ccb648ef2a1, type: 3}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ GameObject:
|
||||||
- component: {fileID: 2918837822014987993}
|
- component: {fileID: 2918837822014987993}
|
||||||
- component: {fileID: 7845089877743661692}
|
- component: {fileID: 7845089877743661692}
|
||||||
- component: {fileID: 4336209376377567030}
|
- component: {fileID: 4336209376377567030}
|
||||||
- component: {fileID: 2806524246861401760}
|
|
||||||
- component: {fileID: 2806524246861401799}
|
- component: {fileID: 2806524246861401799}
|
||||||
- component: {fileID: 2806524246861401801}
|
- component: {fileID: 2806524246861401801}
|
||||||
- component: {fileID: 5683786710272852339}
|
- component: {fileID: 5683786710272852339}
|
||||||
|
|
@ -53,7 +52,7 @@ MonoBehaviour:
|
||||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||||
GlobalObjectIdHash: 121878297
|
GlobalObjectIdHash: 1552073510
|
||||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||||
DeferredDespawnTick: 0
|
DeferredDespawnTick: 0
|
||||||
Ownership: 1
|
Ownership: 1
|
||||||
|
|
@ -107,21 +106,6 @@ MonoBehaviour:
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerMatchState
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerMatchState
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
--- !u!114 &2806524246861401760
|
|
||||||
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: 7775ee3a2f441b52480aabf54be6c1b6, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerBuffManager
|
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
|
||||||
categories:
|
|
||||||
- {fileID: 11400000, guid: d8ed3b9535538f7fc82be878cc307d26, type: 2}
|
|
||||||
--- !u!114 &2806524246861401799
|
--- !u!114 &2806524246861401799
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,6 @@ namespace TD.Combat
|
||||||
// Cached on OnNetworkSpawn — avoids GetComponent every Update.
|
// Cached on OnNetworkSpawn — avoids GetComponent every Update.
|
||||||
private TowerInstance towerInstance;
|
private TowerInstance towerInstance;
|
||||||
|
|
||||||
// Lazily resolved on first attack — the owner's slot isn't guaranteed to be
|
|
||||||
// set at spawn time, so we defer the lookup until combat actually begins.
|
|
||||||
private PlayerBuffManager ownerBuffManager;
|
|
||||||
private bool buffManagerResolved;
|
|
||||||
|
|
||||||
// Shared OverlapSphere result buffer. 32 covers any realistic enemy
|
// Shared OverlapSphere result buffer. 32 covers any realistic enemy
|
||||||
// density; size up if profiling reveals overflow.
|
// density; size up if profiling reveals overflow.
|
||||||
private static readonly Collider[] s_overlapBuffer = new Collider[32];
|
private static readonly Collider[] s_overlapBuffer = new Collider[32];
|
||||||
|
|
@ -239,25 +234,6 @@ namespace TD.Combat
|
||||||
ClearTarget();
|
ClearTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Buff multiplier lookup -------------------------------------
|
|
||||||
|
|
||||||
// Resolved lazily on the first attack because the owner slot NetworkVariable
|
|
||||||
// may not yet have replicated by the time OnNetworkSpawn runs.
|
|
||||||
private PlayerBuffManager GetOwnerBuffManager()
|
|
||||||
{
|
|
||||||
if (buffManagerResolved) return ownerBuffManager;
|
|
||||||
|
|
||||||
var slot = towerInstance.Owner;
|
|
||||||
if (slot == PlayerSlot.None) return null;
|
|
||||||
|
|
||||||
var matchState = PlayerMatchState.GetForSlot(slot);
|
|
||||||
if (matchState == null) return null;
|
|
||||||
|
|
||||||
ownerBuffManager = matchState.GetComponent<PlayerBuffManager>();
|
|
||||||
buffManagerResolved = true;
|
|
||||||
return ownerBuffManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Attack tick -------------------------------------------------
|
// ----- Attack tick -------------------------------------------------
|
||||||
|
|
||||||
private void TickAttack(TowerDefinition def)
|
private void TickAttack(TowerDefinition def)
|
||||||
|
|
@ -265,9 +241,7 @@ namespace TD.Combat
|
||||||
attackCooldown -= Time.deltaTime;
|
attackCooldown -= Time.deltaTime;
|
||||||
if (attackCooldown > 0f) return;
|
if (attackCooldown > 0f) return;
|
||||||
|
|
||||||
float effectiveFireRate = def.AttacksPerSecond
|
attackCooldown = 1f / def.AttacksPerSecond;
|
||||||
* (GetOwnerBuffManager()?.GetMultiplier(BuffStat.AttackSpeed) ?? 1f);
|
|
||||||
attackCooldown = 1f / effectiveFireRate;
|
|
||||||
Fire(def);
|
Fire(def);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,9 +458,7 @@ namespace TD.Combat
|
||||||
|
|
||||||
private void HitEnemy(in CombatProfile p, EnemyHealth target, PlayerSlot owner)
|
private void HitEnemy(in CombatProfile p, EnemyHealth target, PlayerSlot owner)
|
||||||
{
|
{
|
||||||
var multiplier = GetOwnerBuffManager()?.GetMultiplier(BuffStat.Damage) ?? 1f;
|
target.TakeDamage(p.Damage, p.DamageType, owner);
|
||||||
float effectiveDamage = p.Damage * multiplier;
|
|
||||||
target.TakeDamage(effectiveDamage, p.DamageType, owner);
|
|
||||||
ApplyStatusEffect(p, target, owner);
|
ApplyStatusEffect(p, target, owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -525,11 +497,9 @@ namespace TD.Combat
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float effectiveDamage = def.Damage
|
|
||||||
* (GetOwnerBuffManager()?.GetMultiplier(BuffStat.Damage) ?? 1f);
|
|
||||||
proj.InitializeServer(
|
proj.InitializeServer(
|
||||||
target,
|
target,
|
||||||
effectiveDamage,
|
def.Damage,
|
||||||
p.DamageType,
|
p.DamageType,
|
||||||
p.TargetType,
|
p.TargetType,
|
||||||
p.SplashRadius,
|
p.SplashRadius,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
// Assets/_Project/Scripts/Core/BuffStat.cs
|
|
||||||
namespace TD.Core
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Identifies which tower stat a <see cref="TD.Gameplay.BuffDefinition"/> modifies.
|
|
||||||
/// </summary>
|
|
||||||
public enum BuffStat : byte
|
|
||||||
{
|
|
||||||
Damage = 0,
|
|
||||||
AttackSpeed = 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: fc84fec503f24bc7693ee29558f45d27
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
// Assets/_Project/Scripts/Gameplay/ActiveBuff.cs
|
|
||||||
using System;
|
|
||||||
using Unity.Collections;
|
|
||||||
using Unity.Netcode;
|
|
||||||
using TD.Core;
|
|
||||||
|
|
||||||
namespace TD.Gameplay
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// One buff currently held by a player. Stored in
|
|
||||||
/// <see cref="PlayerBuffManager"/>'s NetworkList so all peers see the same set.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para><b>IEquatable.</b> NGO's NetworkList indexer setter short-circuits when
|
|
||||||
/// Equals returns true, silently dropping the write. Every mutable field
|
|
||||||
/// (only <see cref="IsActive"/>) is included in the comparison so toggling a
|
|
||||||
/// buff correctly propagates to clients.</para>
|
|
||||||
/// </remarks>
|
|
||||||
public struct ActiveBuff : INetworkSerializable, IEquatable<ActiveBuff>
|
|
||||||
{
|
|
||||||
/// <summary>Which tower stat this buff multiplies.</summary>
|
|
||||||
public BuffStat Stat;
|
|
||||||
|
|
||||||
/// <summary>Multiplicative factor, e.g. 1.25 for +25%.</summary>
|
|
||||||
public float Multiplier;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// False when disabled (e.g. by an enemy ability). Excluded from
|
|
||||||
/// <see cref="PlayerBuffManager.GetMultiplier"/> while false.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsActive;
|
|
||||||
|
|
||||||
/// <summary>Human-readable name for the HUD buff list.</summary>
|
|
||||||
public FixedString64Bytes DisplayName;
|
|
||||||
|
|
||||||
// ----- INetworkSerializable ---------------------------------------
|
|
||||||
|
|
||||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
|
||||||
{
|
|
||||||
byte statByte = (byte)Stat;
|
|
||||||
serializer.SerializeValue(ref statByte);
|
|
||||||
Stat = (BuffStat)statByte;
|
|
||||||
|
|
||||||
serializer.SerializeValue(ref Multiplier);
|
|
||||||
serializer.SerializeValue(ref IsActive);
|
|
||||||
serializer.SerializeValue(ref DisplayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- IEquatable -------------------------------------------------
|
|
||||||
|
|
||||||
public bool Equals(ActiveBuff other)
|
|
||||||
=> Stat == other.Stat
|
|
||||||
&& Multiplier == other.Multiplier
|
|
||||||
&& IsActive == other.IsActive
|
|
||||||
&& DisplayName == other.DisplayName;
|
|
||||||
|
|
||||||
public override bool Equals(object obj) => obj is ActiveBuff other && Equals(other);
|
|
||||||
|
|
||||||
public override int GetHashCode() => HashCode.Combine((int)Stat, Multiplier, IsActive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: b09203f8acc450bf3b3c4bd8ef3fe84e
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
// Assets/_Project/Scripts/Gameplay/BuffCategory.cs
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace TD.Gameplay
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A purchasable category of buffs. The player pays <see cref="Cost"/> gold
|
|
||||||
/// and receives a randomly chosen <see cref="BuffDefinition"/> from <see cref="Pool"/>.
|
|
||||||
/// </summary>
|
|
||||||
[CreateAssetMenu(fileName = "BuffCategory", menuName = "TD/Buffs/Buff Category")]
|
|
||||||
public class BuffCategory : ScriptableObject
|
|
||||||
{
|
|
||||||
[Tooltip("Name shown on the buff menu purchase button.")]
|
|
||||||
public string DisplayName;
|
|
||||||
|
|
||||||
[Tooltip("Gold cost to purchase one random buff from this category.")]
|
|
||||||
[Min(0)]
|
|
||||||
public int Cost;
|
|
||||||
|
|
||||||
[Tooltip("Set of buffs the player can receive. One is chosen uniformly at random.")]
|
|
||||||
public BuffDefinition[] Pool;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 516e8faec948a4b3d977ec019376c438
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
// Assets/_Project/Scripts/Gameplay/BuffDefinition.cs
|
|
||||||
using UnityEngine;
|
|
||||||
using TD.Core;
|
|
||||||
|
|
||||||
namespace TD.Gameplay
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// One possible buff that can be awarded to a player. Lives in a
|
|
||||||
/// <see cref="BuffCategory"/>'s pool and is drawn randomly on purchase.
|
|
||||||
/// </summary>
|
|
||||||
[CreateAssetMenu(fileName = "BuffDefinition", menuName = "TD/Buffs/Buff Definition")]
|
|
||||||
public class BuffDefinition : ScriptableObject
|
|
||||||
{
|
|
||||||
[Tooltip("Name shown in the buff menu and tooltip.")]
|
|
||||||
public string DisplayName;
|
|
||||||
|
|
||||||
[Tooltip("Which tower stat this buff multiplies.")]
|
|
||||||
public BuffStat Stat;
|
|
||||||
|
|
||||||
[Tooltip("Multiplicative factor applied to the stat. 1.25 = +25%.")]
|
|
||||||
[Min(1f)]
|
|
||||||
public float Multiplier = 1.25f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: b01f9aea62ee8a1c1991bb2a097de224
|
|
||||||
|
|
@ -151,13 +151,6 @@ namespace TD.Gameplay
|
||||||
// controllers handle right-click as cancel there) and when over HUD.
|
// controllers handle right-click as cancel there) and when over HUD.
|
||||||
if (isModal) return;
|
if (isModal) return;
|
||||||
|
|
||||||
// B: toggle buff menu.
|
|
||||||
if (keyboard != null && keyboard.bKey.wasPressedThisFrame
|
|
||||||
&& !HUDController.IsTextInputActive)
|
|
||||||
{
|
|
||||||
HUDController.Instance?.ToggleBuffMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab: select the builder, or (if already selected) recenter the camera on it.
|
// Tab: select the builder, or (if already selected) recenter the camera on it.
|
||||||
if (keyboard != null && keyboard.tabKey.wasPressedThisFrame
|
if (keyboard != null && keyboard.tabKey.wasPressedThisFrame
|
||||||
&& !HUDController.IsTextInputActive)
|
&& !HUDController.IsTextInputActive)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-player set of granted builder effects. Lives on the Player prefab alongside
|
/// Per-player set of granted builder effects. Lives on the Player prefab alongside
|
||||||
/// <see cref="PlayerGoldManager"/>, <see cref="PlayerTowerDeck"/>, and
|
/// <see cref="PlayerGoldManager"/> and <see cref="PlayerTowerDeck"/>.
|
||||||
/// <see cref="PlayerBuffManager"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para><b>List of granted kinds.</b> A builder effect carries no per-grant state (no
|
/// <para><b>List of granted kinds.</b> A builder effect carries no per-grant state (no
|
||||||
|
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
// Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Unity.Collections;
|
|
||||||
using Unity.Netcode;
|
|
||||||
using UnityEngine;
|
|
||||||
using TD.Core;
|
|
||||||
|
|
||||||
namespace TD.Gameplay
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Holds all buffs currently owned by one player. Lives on the Player prefab
|
|
||||||
/// alongside <see cref="PlayerGoldManager"/> and <see cref="PlayerMatchState"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para><b>Purchase flow.</b> The owning client calls
|
|
||||||
/// <see cref="RequestPurchaseBuffRpc"/> with a category index. The server
|
|
||||||
/// validates gold, deducts the cost, picks a random
|
|
||||||
/// <see cref="BuffDefinition"/> from the category's pool, and appends an
|
|
||||||
/// <see cref="ActiveBuff"/> to the replicated <see cref="buffs"/> list.</para>
|
|
||||||
///
|
|
||||||
/// <para><b>Multiplier query.</b> <see cref="GetMultiplier"/> is called by
|
|
||||||
/// <see cref="TD.Combat.TowerCombat"/> on the server each time a tower fires.
|
|
||||||
/// It multiplies all active buffs for the requested stat together.</para>
|
|
||||||
///
|
|
||||||
/// <para><b>Enable / disable.</b> Enemies can call <see cref="ServerDisableBuff"/>
|
|
||||||
/// and <see cref="ServerEnableBuff"/> by index to temporarily suppress buffs
|
|
||||||
/// without removing them from the list.</para>
|
|
||||||
/// </remarks>
|
|
||||||
public class PlayerBuffManager : NetworkBehaviour
|
|
||||||
{
|
|
||||||
// ----- Static registry (mirrors PlayerGoldManager pattern) -----------
|
|
||||||
|
|
||||||
private static readonly Dictionary<ulong, PlayerBuffManager> s_byClientId
|
|
||||||
= new Dictionary<ulong, PlayerBuffManager>();
|
|
||||||
|
|
||||||
/// <summary>Returns the PlayerBuffManager owned by the given client, or null.</summary>
|
|
||||||
public static PlayerBuffManager GetForClient(ulong clientId)
|
|
||||||
{
|
|
||||||
s_byClientId.TryGetValue(clientId, out var mgr);
|
|
||||||
return mgr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Convenience: the local client's own buff manager.</summary>
|
|
||||||
public static PlayerBuffManager Local
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var nm = NetworkManager.Singleton;
|
|
||||||
if (nm == null) return null;
|
|
||||||
return GetForClient(nm.LocalClientId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Inspector ------------------------------------------------------
|
|
||||||
|
|
||||||
[Tooltip("Purchasable buff categories shown in the buff menu. Index in this " +
|
|
||||||
"array is the categoryIndex passed to RequestPurchaseBuffRpc.")]
|
|
||||||
[SerializeField] private BuffCategory[] categories;
|
|
||||||
|
|
||||||
private NetworkList<ActiveBuff> buffs;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
buffs = new NetworkList<ActiveBuff>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- NGO lifecycle ----------------------------------------------
|
|
||||||
|
|
||||||
public override void OnNetworkSpawn()
|
|
||||||
{
|
|
||||||
s_byClientId[OwnerClientId] = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnNetworkDespawn()
|
|
||||||
{
|
|
||||||
if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
|
|
||||||
s_byClientId.Remove(OwnerClientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Public API -------------------------------------------------
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the combined multiplier for <paramref name="stat"/> across all
|
|
||||||
/// active buffs owned by this player. Returns 1.0 if no matching buffs
|
|
||||||
/// are active. Safe to call from any peer.
|
|
||||||
/// </summary>
|
|
||||||
public float GetMultiplier(BuffStat stat)
|
|
||||||
{
|
|
||||||
float result = 1f;
|
|
||||||
for (int i = 0; i < buffs.Count; i++)
|
|
||||||
{
|
|
||||||
var buff = buffs[i];
|
|
||||||
if (buff.Stat == stat && buff.IsActive)
|
|
||||||
result *= buff.Multiplier;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the number of buff categories available, for building the UI.
|
|
||||||
/// </summary>
|
|
||||||
public int CategoryCount => categories?.Length ?? 0;
|
|
||||||
|
|
||||||
/// <summary>Returns the category at the given index, or null.</summary>
|
|
||||||
public BuffCategory GetCategory(int index)
|
|
||||||
=> (categories != null && index >= 0 && index < categories.Length)
|
|
||||||
? categories[index]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
/// <summary>Read-only access to the buff list for UI rendering.</summary>
|
|
||||||
public NetworkList<ActiveBuff> Buffs => buffs;
|
|
||||||
|
|
||||||
// ----- Server helpers ---------------------------------------------
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Server-only: disables the buff at <paramref name="index"/>.
|
|
||||||
/// The buff remains in the list and can be re-enabled.
|
|
||||||
/// </summary>
|
|
||||||
public void ServerDisableBuff(int index)
|
|
||||||
{
|
|
||||||
if (!IsServer) return;
|
|
||||||
if (index < 0 || index >= buffs.Count) return;
|
|
||||||
|
|
||||||
var b = buffs[index];
|
|
||||||
if (!b.IsActive) return;
|
|
||||||
b.IsActive = false;
|
|
||||||
buffs[index] = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Server-only: re-enables the buff at <paramref name="index"/>.</summary>
|
|
||||||
public void ServerEnableBuff(int index)
|
|
||||||
{
|
|
||||||
if (!IsServer) return;
|
|
||||||
if (index < 0 || index >= buffs.Count) return;
|
|
||||||
|
|
||||||
var b = buffs[index];
|
|
||||||
if (b.IsActive) return;
|
|
||||||
b.IsActive = true;
|
|
||||||
buffs[index] = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Purchase RPC -----------------------------------------------
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Owning-client entry point. Sends a request to the server to purchase
|
|
||||||
/// a random buff from the category at <paramref name="categoryIndex"/>.
|
|
||||||
/// The server validates gold and adds the buff if the purchase succeeds.
|
|
||||||
/// </summary>
|
|
||||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
|
|
||||||
public void RequestPurchaseBuffRpc(int categoryIndex)
|
|
||||||
{
|
|
||||||
if (categories == null || categoryIndex < 0 || categoryIndex >= categories.Length)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("[PlayerBuffManager] RequestPurchaseBuff: invalid category index.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var category = categories[categoryIndex];
|
|
||||||
if (category == null || category.Pool == null || category.Pool.Length == 0)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("[PlayerBuffManager] RequestPurchaseBuff: category has no buffs.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var goldManager = PlayerGoldManager.GetForClient(OwnerClientId);
|
|
||||||
if (goldManager == null)
|
|
||||||
{
|
|
||||||
Debug.LogError("[PlayerBuffManager] RequestPurchaseBuff: no PlayerGoldManager found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (goldManager.CurrentGold < category.Cost)
|
|
||||||
{
|
|
||||||
Debug.Log($"[PlayerBuffManager] Client {OwnerClientId} cannot afford " +
|
|
||||||
$"'{category.DisplayName}' (cost {category.Cost}, " +
|
|
||||||
$"gold {goldManager.CurrentGold}).");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
goldManager.DeductGold(category.Cost);
|
|
||||||
|
|
||||||
var def = category.Pool[Random.Range(0, category.Pool.Length)];
|
|
||||||
buffs.Add(new ActiveBuff
|
|
||||||
{
|
|
||||||
Stat = def.Stat,
|
|
||||||
Multiplier = def.Multiplier,
|
|
||||||
IsActive = true,
|
|
||||||
DisplayName = new FixedString64Bytes(def.DisplayName ?? string.Empty),
|
|
||||||
});
|
|
||||||
|
|
||||||
Debug.Log($"[PlayerBuffManager] Client {OwnerClientId} purchased buff " +
|
|
||||||
$"'{def.DisplayName}' ({def.Stat} ×{def.Multiplier}).");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 7775ee3a2f441b52480aabf54be6c1b6
|
|
||||||
|
|
@ -7,8 +7,8 @@ namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-player set of unlocked tower types — the player's "deck". Lives on the
|
/// Per-player set of unlocked tower types — the player's "deck". Lives on the
|
||||||
/// Player prefab alongside <see cref="PlayerGoldManager"/>,
|
/// Player prefab alongside <see cref="PlayerGoldManager"/> and
|
||||||
/// <see cref="PlayerMatchState"/>, and <see cref="PlayerBuffManager"/>.
|
/// <see cref="PlayerMatchState"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para><b>Roguelike keystone.</b> Players no longer see every tower in the match
|
/// <para><b>Roguelike keystone.</b> Players no longer see every tower in the match
|
||||||
|
|
|
||||||
|
|
@ -101,10 +101,6 @@ namespace TD.UI
|
||||||
|
|
||||||
// Match-end overlay — built once on Start and toggled on Phase changes.
|
// Match-end overlay — built once on Start and toggled on Phase changes.
|
||||||
private VisualElement matchEndOverlay;
|
private VisualElement matchEndOverlay;
|
||||||
|
|
||||||
// Buff menu overlay — toggled by the B key via ToggleBuffMenu().
|
|
||||||
private VisualElement buffMenuOverlay;
|
|
||||||
private VisualElement buffMenuContent;
|
|
||||||
private Label matchEndTitle;
|
private Label matchEndTitle;
|
||||||
|
|
||||||
// Draft overlay (roguelike between-waves draft). Non-modal: the card row floats at
|
// Draft overlay (roguelike between-waves draft). Non-modal: the card row floats at
|
||||||
|
|
@ -698,9 +694,6 @@ namespace TD.UI
|
||||||
// MatchState.OnPhaseChanged fires Victory or Defeat.
|
// MatchState.OnPhaseChanged fires Victory or Defeat.
|
||||||
BuildMatchEndOverlay(root);
|
BuildMatchEndOverlay(root);
|
||||||
|
|
||||||
// Build the buff menu overlay. Hidden until the player presses B.
|
|
||||||
BuildBuffMenuOverlay(root);
|
|
||||||
|
|
||||||
// Build the draft overlay (roguelike between-waves draft). Hidden until the
|
// Build the draft overlay (roguelike between-waves draft). Hidden until the
|
||||||
// local player has an active draft (or to show the paid "Buy Roll" button).
|
// local player has an active draft (or to show the paid "Buy Roll" button).
|
||||||
BuildDraftOverlay(root);
|
BuildDraftOverlay(root);
|
||||||
|
|
@ -1173,13 +1166,11 @@ namespace TD.UI
|
||||||
{
|
{
|
||||||
// Build buttons come from the LOCAL player's deck — the per-player
|
// Build buttons come from the LOCAL player's deck — the per-player
|
||||||
// unlocked set — not the global catalog. Each deck entry is a
|
// unlocked set — not the global catalog. Each deck entry is a
|
||||||
// TowerTypeId resolved back to its definition via the catalog. The
|
// TowerTypeId resolved back to its definition via the catalog.
|
||||||
// last grid slot is reserved for the Buffs button, so towers fill
|
|
||||||
// slots 0..GRID_MAX-2.
|
|
||||||
var deck = PlayerTowerDeck.Local;
|
var deck = PlayerTowerDeck.Local;
|
||||||
if (deck != null)
|
if (deck != null)
|
||||||
{
|
{
|
||||||
int count = Mathf.Min(deck.Count, GRID_MAX - 1);
|
int count = Mathf.Min(deck.Count, GRID_MAX);
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
int typeId = deck.GetTypeIdAt(i);
|
int typeId = deck.GetTypeIdAt(i);
|
||||||
|
|
@ -1189,7 +1180,6 @@ namespace TD.UI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cells[GRID_MAX - 1] = CreateBuffMenuButton(HotkeyLayout[GRID_MAX - 1]);
|
|
||||||
}
|
}
|
||||||
else if (selection is TowerInstance tower)
|
else if (selection is TowerInstance tower)
|
||||||
{
|
{
|
||||||
|
|
@ -1374,9 +1364,7 @@ namespace TD.UI
|
||||||
|
|
||||||
// Sell action for a completed tower. Refund is computed on the TowerInstance (single
|
// Sell action for a completed tower. Refund is computed on the TowerInstance (single
|
||||||
// source of truth shared with the server) so the badge shows exactly what the player
|
// source of truth shared with the server) so the badge shows exactly what the player
|
||||||
// gets back. Hotkey is Key.None: the bottom-right slot's letter (B) is already the
|
// gets back. The button stays click-only (Key.None).
|
||||||
// global buff-menu toggle — see CreateBuffMenuButton — so binding it here would fire
|
|
||||||
// both. The button stays click-only.
|
|
||||||
private VisualElement CreateSellButton(TowerInstance tower, Key hotkey)
|
private VisualElement CreateSellButton(TowerInstance tower, Key hotkey)
|
||||||
{
|
{
|
||||||
// Only the tower's owner can sell it (the server enforces this too). A non-owner
|
// Only the tower's owner can sell it (the server enforces this too). A non-owner
|
||||||
|
|
@ -1399,17 +1387,6 @@ namespace TD.UI
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VisualElement CreateBuffMenuButton(Key hotkey)
|
|
||||||
{
|
|
||||||
// Key.None so HandleHotkeys doesn't register B here — BuilderInputController
|
|
||||||
// already fires ToggleBuffMenu on B globally. Registering it in both places
|
|
||||||
// causes a double-toggle on the same frame when the builder is selected.
|
|
||||||
return CreateActionButton(
|
|
||||||
costText: "Buffs [B]",
|
|
||||||
hotkey: Key.None,
|
|
||||||
onClick: () => ToggleBuffMenu());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel action for an in-progress build. Fires the owner-only RPC; the
|
// Cancel action for an in-progress build. Fires the owner-only RPC; the
|
||||||
// server cancels the matching job (or, for shelved sites, refunds + despawns
|
// server cancels the matching job (or, for shelved sites, refunds + despawns
|
||||||
// directly), full gold is refunded, the BuildSiteVisual is despawned, and
|
// directly), full gold is refunded, the BuildSiteVisual is despawned, and
|
||||||
|
|
@ -1712,149 +1689,6 @@ namespace TD.UI
|
||||||
|
|
||||||
// ----- Chat feed + input -----------------------------------------
|
// ----- Chat feed + input -----------------------------------------
|
||||||
|
|
||||||
// ----- Buff menu overlay ------------------------------------------
|
|
||||||
|
|
||||||
private void BuildBuffMenuOverlay(VisualElement root)
|
|
||||||
{
|
|
||||||
buffMenuOverlay = new VisualElement();
|
|
||||||
buffMenuOverlay.style.position = Position.Absolute;
|
|
||||||
buffMenuOverlay.style.left = 0;
|
|
||||||
buffMenuOverlay.style.right = 0;
|
|
||||||
buffMenuOverlay.style.top = 0;
|
|
||||||
buffMenuOverlay.style.bottom = 0;
|
|
||||||
buffMenuOverlay.style.alignItems = Align.Center;
|
|
||||||
buffMenuOverlay.style.justifyContent = Justify.Center;
|
|
||||||
buffMenuOverlay.style.backgroundColor = new Color(0f, 0f, 0f, 0.5f);
|
|
||||||
buffMenuOverlay.style.display = DisplayStyle.None;
|
|
||||||
buffMenuOverlay.pickingMode = PickingMode.Position;
|
|
||||||
|
|
||||||
var panel = new VisualElement();
|
|
||||||
panel.style.minWidth = 340;
|
|
||||||
panel.style.paddingTop = 20;
|
|
||||||
panel.style.paddingBottom = 20;
|
|
||||||
panel.style.paddingLeft = 28;
|
|
||||||
panel.style.paddingRight = 28;
|
|
||||||
panel.style.backgroundColor = new Color(0.08f, 0.08f, 0.10f, 0.95f);
|
|
||||||
panel.style.borderTopWidth = panel.style.borderBottomWidth =
|
|
||||||
panel.style.borderLeftWidth = panel.style.borderRightWidth = 2;
|
|
||||||
var border = new Color(0.4f, 0.4f, 0.45f);
|
|
||||||
panel.style.borderTopColor = panel.style.borderBottomColor =
|
|
||||||
panel.style.borderLeftColor = panel.style.borderRightColor = border;
|
|
||||||
|
|
||||||
var title = new Label("Buffs");
|
|
||||||
title.style.fontSize = 24;
|
|
||||||
title.style.color = Color.white;
|
|
||||||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
||||||
title.style.marginBottom = 12;
|
|
||||||
panel.Add(title);
|
|
||||||
|
|
||||||
// Scrollable content area: current buffs + purchase buttons.
|
|
||||||
// Rebuilt each time the menu is shown via RefreshBuffMenuContent().
|
|
||||||
buffMenuContent = new VisualElement();
|
|
||||||
panel.Add(buffMenuContent);
|
|
||||||
|
|
||||||
var closeBtn = new Button(() => SetBuffMenuVisible(false)) { text = "Close [B]" };
|
|
||||||
closeBtn.style.marginTop = 16;
|
|
||||||
closeBtn.style.height = 32;
|
|
||||||
panel.Add(closeBtn);
|
|
||||||
|
|
||||||
buffMenuOverlay.Add(panel);
|
|
||||||
root.Add(buffMenuOverlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Toggles the buff menu overlay. Called by <see cref="TD.Gameplay.BuilderInputController"/>
|
|
||||||
/// when the player presses B.
|
|
||||||
/// </summary>
|
|
||||||
public void ToggleBuffMenu()
|
|
||||||
{
|
|
||||||
bool nowVisible = buffMenuOverlay?.style.display == DisplayStyle.None;
|
|
||||||
SetBuffMenuVisible(nowVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetBuffMenuVisible(bool visible)
|
|
||||||
{
|
|
||||||
if (buffMenuOverlay == null) return;
|
|
||||||
buffMenuOverlay.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
|
||||||
if (visible) RefreshBuffMenuContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshBuffMenuContent()
|
|
||||||
{
|
|
||||||
if (buffMenuContent == null) return;
|
|
||||||
buffMenuContent.Clear();
|
|
||||||
|
|
||||||
var buffManager = TD.Gameplay.PlayerBuffManager.Local;
|
|
||||||
|
|
||||||
// Current buffs section.
|
|
||||||
var buffsHeader = new Label("Active Buffs");
|
|
||||||
buffsHeader.style.color = new Color(0.7f, 0.9f, 0.7f);
|
|
||||||
buffsHeader.style.fontSize = 14;
|
|
||||||
buffsHeader.style.marginBottom = 4;
|
|
||||||
buffMenuContent.Add(buffsHeader);
|
|
||||||
|
|
||||||
if (buffManager == null || buffManager.Buffs.Count == 0)
|
|
||||||
{
|
|
||||||
var none = new Label("None");
|
|
||||||
none.style.color = new Color(0.55f, 0.55f, 0.55f);
|
|
||||||
none.style.marginBottom = 8;
|
|
||||||
buffMenuContent.Add(none);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (int i = 0; i < buffManager.Buffs.Count; i++)
|
|
||||||
{
|
|
||||||
var buff = buffManager.Buffs[i];
|
|
||||||
string statName = buff.Stat == TD.Core.BuffStat.Damage ? "Damage" : "Attack Speed";
|
|
||||||
string activeTag = buff.IsActive ? "" : " [DISABLED]";
|
|
||||||
var row = new Label($"• {buff.DisplayName} ({statName} ×{buff.Multiplier:F2}){activeTag}");
|
|
||||||
row.style.color = buff.IsActive ? Color.white : new Color(0.5f, 0.5f, 0.5f);
|
|
||||||
row.style.fontSize = 13;
|
|
||||||
buffMenuContent.Add(row);
|
|
||||||
}
|
|
||||||
buffMenuContent.style.marginBottom = 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purchase buttons section.
|
|
||||||
var buyHeader = new Label("Purchase");
|
|
||||||
buyHeader.style.color = new Color(0.9f, 0.8f, 0.5f);
|
|
||||||
buyHeader.style.fontSize = 14;
|
|
||||||
buyHeader.style.marginTop = 8;
|
|
||||||
buyHeader.style.marginBottom = 4;
|
|
||||||
buffMenuContent.Add(buyHeader);
|
|
||||||
|
|
||||||
int categoryCount = buffManager?.CategoryCount ?? 0;
|
|
||||||
if (categoryCount == 0)
|
|
||||||
{
|
|
||||||
var none = new Label("No categories available.");
|
|
||||||
none.style.color = new Color(0.55f, 0.55f, 0.55f);
|
|
||||||
buffMenuContent.Add(none);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int localGold = TD.Gameplay.PlayerGoldManager.Local?.CurrentGold ?? 0;
|
|
||||||
for (int i = 0; i < categoryCount; i++)
|
|
||||||
{
|
|
||||||
var category = buffManager.GetCategory(i);
|
|
||||||
if (category == null) continue;
|
|
||||||
|
|
||||||
int idx = i; // capture for lambda
|
|
||||||
var btn = new Button(() =>
|
|
||||||
{
|
|
||||||
TD.Gameplay.PlayerBuffManager.Local?.RequestPurchaseBuffRpc(idx);
|
|
||||||
RefreshBuffMenuContent();
|
|
||||||
})
|
|
||||||
{
|
|
||||||
text = $"{category.DisplayName} — {category.Cost}g"
|
|
||||||
};
|
|
||||||
btn.style.height = 34;
|
|
||||||
btn.style.fontSize = 13;
|
|
||||||
btn.style.marginBottom = 4;
|
|
||||||
btn.SetEnabled(localGold >= category.Cost);
|
|
||||||
buffMenuContent.Add(btn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bottom-left chat panel. Anchored 12px from the left edge, with the
|
// Bottom-left chat panel. Anchored 12px from the left edge, with the
|
||||||
// bottom edge sitting above the 220px bottom-ui. Layout uses a flex
|
// bottom edge sitting above the 220px bottom-ui. Layout uses a flex
|
||||||
// column: scrollable feed on top, input below. The feed clips at
|
// column: scrollable feed on top, input below. The feed clips at
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue