Compare commits
4 commits
04ead32846
...
08a3848bc0
| Author | SHA1 | Date | |
|---|---|---|---|
| 08a3848bc0 | |||
| 3ada934e41 | |||
| 79cd331141 | |||
| 3737ad517c |
22 changed files with 710 additions and 64 deletions
|
|
@ -1,5 +1,9 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
|
<<<<<<< HEAD
|
||||||
guid: 01de85ee5d8a2014594d9910b1a6ff55
|
guid: 01de85ee5d8a2014594d9910b1a6ff55
|
||||||
|
=======
|
||||||
|
guid: cf294cbfb17f5a5d7846479a116fc7b3
|
||||||
|
>>>>>>> 31b0b5f (adding uncommited files (oops))
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|
|
||||||
8
Assets/_Project/Definitions/Buffs.meta
Normal file
8
Assets/_Project/Definitions/Buffs.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: eef6329e4294d86c6978ff9453b56956
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
17
Assets/_Project/Definitions/Buffs/ExtraDamage25.asset
Normal file
17
Assets/_Project/Definitions/Buffs/ExtraDamage25.asset
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
%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
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9ec170e540b1f91cca762dcebe81a856
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
18
Assets/_Project/Definitions/Buffs/OffensiveBuffs.asset
Normal file
18
Assets/_Project/Definitions/Buffs/OffensiveBuffs.asset
Normal 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: 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}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d8ed3b9535538f7fc82be878cc307d26
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -13,6 +13,7 @@ GameObject:
|
||||||
- component: {fileID: 2918837822014987993}
|
- component: {fileID: 2918837822014987993}
|
||||||
- component: {fileID: 7845089877743661692}
|
- component: {fileID: 7845089877743661692}
|
||||||
- component: {fileID: 4336209376377567030}
|
- component: {fileID: 4336209376377567030}
|
||||||
|
- component: {fileID: 2806524246861401760}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: Player
|
m_Name: Player
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
|
|
@ -47,7 +48,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
|
||||||
|
|
@ -101,3 +102,18 @@ 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}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ 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];
|
||||||
|
|
@ -211,6 +216,25 @@ 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;
|
||||||
|
buffManagerResolved = true;
|
||||||
|
|
||||||
|
var slot = towerInstance.Owner;
|
||||||
|
if (slot == PlayerSlot.None) return null;
|
||||||
|
|
||||||
|
var matchState = PlayerMatchState.GetForSlot(slot);
|
||||||
|
if (matchState == null) return null;
|
||||||
|
|
||||||
|
ownerBuffManager = matchState.GetComponent<PlayerBuffManager>();
|
||||||
|
return ownerBuffManager;
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Attack tick -------------------------------------------------
|
// ----- Attack tick -------------------------------------------------
|
||||||
|
|
||||||
private void TickAttack(TowerDefinition def)
|
private void TickAttack(TowerDefinition def)
|
||||||
|
|
@ -218,7 +242,9 @@ namespace TD.Combat
|
||||||
attackCooldown -= Time.deltaTime;
|
attackCooldown -= Time.deltaTime;
|
||||||
if (attackCooldown > 0f) return;
|
if (attackCooldown > 0f) return;
|
||||||
|
|
||||||
attackCooldown = 1f / def.FireRate;
|
float effectiveFireRate = def.FireRate
|
||||||
|
* (GetOwnerBuffManager()?.GetMultiplier(BuffStat.AttackSpeed) ?? 1f);
|
||||||
|
attackCooldown = 1f / effectiveFireRate;
|
||||||
Fire(def);
|
Fire(def);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,7 +342,9 @@ namespace TD.Combat
|
||||||
|
|
||||||
private void HitEnemy(TowerDefinition def, EnemyHealth target, PlayerSlot owner)
|
private void HitEnemy(TowerDefinition def, EnemyHealth target, PlayerSlot owner)
|
||||||
{
|
{
|
||||||
target.TakeDamage(def.Damage, def.DamageType, owner);
|
float effectiveDamage = def.Damage
|
||||||
|
* (GetOwnerBuffManager()?.GetMultiplier(BuffStat.Damage) ?? 1f);
|
||||||
|
target.TakeDamage(effectiveDamage, def.DamageType, owner);
|
||||||
ApplyStatusEffect(def, target, owner);
|
ApplyStatusEffect(def, target, owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,6 +368,8 @@ namespace TD.Combat
|
||||||
|
|
||||||
// ----- Projectile spawning -----------------------------------------
|
// ----- Projectile spawning -----------------------------------------
|
||||||
|
|
||||||
|
// TODO this seems wacky. Why do we calculate effective damage twice? Are they both used?
|
||||||
|
// Spawn projectile is void. Should it return a projectile?
|
||||||
private void SpawnProjectile(TowerDefinition def, EnemyHealth target)
|
private void SpawnProjectile(TowerDefinition def, EnemyHealth target)
|
||||||
{
|
{
|
||||||
var go = Instantiate(def.ProjectilePrefab, transform.position, Quaternion.identity);
|
var go = Instantiate(def.ProjectilePrefab, transform.position, Quaternion.identity);
|
||||||
|
|
@ -354,9 +384,11 @@ namespace TD.Combat
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float effectiveDamage = def.Damage
|
||||||
|
* (GetOwnerBuffManager()?.GetMultiplier(BuffStat.Damage) ?? 1f);
|
||||||
proj.InitializeServer(
|
proj.InitializeServer(
|
||||||
target,
|
target,
|
||||||
def.Damage,
|
effectiveDamage,
|
||||||
def.DamageType,
|
def.DamageType,
|
||||||
def.TargetType,
|
def.TargetType,
|
||||||
def.SplashRadius,
|
def.SplashRadius,
|
||||||
|
|
|
||||||
12
Assets/_Project/Scripts/Core/BuffStat.cs
Normal file
12
Assets/_Project/Scripts/Core/BuffStat.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Core/BuffStat.cs.meta
Normal file
2
Assets/_Project/Scripts/Core/BuffStat.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fc84fec503f24bc7693ee29558f45d27
|
||||||
61
Assets/_Project/Scripts/Gameplay/ActiveBuff.cs
Normal file
61
Assets/_Project/Scripts/Gameplay/ActiveBuff.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/ActiveBuff.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/ActiveBuff.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b09203f8acc450bf3b3c4bd8ef3fe84e
|
||||||
23
Assets/_Project/Scripts/Gameplay/BuffCategory.cs
Normal file
23
Assets/_Project/Scripts/Gameplay/BuffCategory.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/BuffCategory.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/BuffCategory.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 516e8faec948a4b3d977ec019376c438
|
||||||
24
Assets/_Project/Scripts/Gameplay/BuffDefinition.cs
Normal file
24
Assets/_Project/Scripts/Gameplay/BuffDefinition.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/BuffDefinition.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/BuffDefinition.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b01f9aea62ee8a1c1991bb2a097de224
|
||||||
|
|
@ -149,6 +149,14 @@ namespace TD.Gameplay
|
||||||
// Right-click. Suppressed entirely during a modal mode (placement/paint
|
// Right-click. Suppressed entirely during a modal mode (placement/paint
|
||||||
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
if (pointerOverHud) return;
|
if (pointerOverHud) return;
|
||||||
if (!mouse.rightButton.wasPressedThisFrame) return;
|
if (!mouse.rightButton.wasPressedThisFrame) return;
|
||||||
|
|
||||||
|
|
|
||||||
195
Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs
Normal file
195
Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
// 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, RequireOwnership = true)]
|
||||||
|
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}).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7775ee3a2f441b52480aabf54be6c1b6
|
||||||
|
|
@ -21,55 +21,60 @@ namespace TD.UI
|
||||||
[RequireComponent(typeof(UIDocument))]
|
[RequireComponent(typeof(UIDocument))]
|
||||||
public class HUDController : MonoBehaviour
|
public class HUDController : MonoBehaviour
|
||||||
{
|
{
|
||||||
|
public static HUDController Instance { get; private set; }
|
||||||
|
|
||||||
// ----- Inspector --------------------------------------------------
|
// ----- Inspector --------------------------------------------------
|
||||||
|
|
||||||
[Header("Scene References")]
|
[Header("Scene References")] [Tooltip("The local client's TowerPlacementController.")] [SerializeField]
|
||||||
[Tooltip("The local client's TowerPlacementController.")]
|
private TowerPlacementController placementController;
|
||||||
[SerializeField] private TowerPlacementController placementController;
|
|
||||||
|
|
||||||
[Tooltip("The local client's TowerPaintController (drives the Paint tab + paint cursor).")]
|
[Tooltip("The local client's TowerPaintController (drives the Paint tab + paint cursor).")]
|
||||||
[SerializeField] private TowerPaintController paintController;
|
[SerializeField] private TowerPaintController paintController;
|
||||||
|
|
||||||
[Tooltip("The TowerPlacementManager NetworkObject in the scene.")]
|
[Tooltip("The TowerPlacementManager NetworkObject in the scene.")] [SerializeField]
|
||||||
[SerializeField] private TowerPlacementManager placementManager;
|
private TowerPlacementManager placementManager;
|
||||||
|
|
||||||
[Tooltip("The local client's CameraController. Used by the minimap for click-to-jump " +
|
[Tooltip("The local client's CameraController. Used by the minimap for click-to-jump " +
|
||||||
"and drag-to-pan.")]
|
"and drag-to-pan.")]
|
||||||
[SerializeField] private CameraController cameraController;
|
[SerializeField]
|
||||||
|
private CameraController cameraController;
|
||||||
|
|
||||||
[Header("Settings")]
|
[Header("Settings")] [SerializeField] private float rejectionMessageDuration = 2.5f;
|
||||||
[SerializeField] private float rejectionMessageDuration = 2.5f;
|
|
||||||
|
|
||||||
[Tooltip("Maximum visible height of the chat feed in pixels. Content past this " +
|
[Tooltip("Maximum visible height of the chat feed in pixels. Content past this " +
|
||||||
"height is clipped — older messages scroll off the top of the visible area " +
|
"height is clipped — older messages scroll off the top of the visible area " +
|
||||||
"but stay in history (scroll up while chat is open to view).")]
|
"but stay in history (scroll up while chat is open to view).")]
|
||||||
[SerializeField] private float chatMaxHeight = 280f;
|
[SerializeField]
|
||||||
|
private float chatMaxHeight = 280f;
|
||||||
|
|
||||||
[Tooltip("Maximum messages kept in chat history. Defaults to effectively unlimited " +
|
[Tooltip("Maximum messages kept in chat history. Defaults to effectively unlimited " +
|
||||||
"(int.MaxValue) — every message sent during a match stays scrollable. " +
|
"(int.MaxValue) — every message sent during a match stays scrollable. " +
|
||||||
"Lower the value if a long match ever shows DOM perf issues; this field " +
|
"Lower the value if a long match ever shows DOM perf issues; this field " +
|
||||||
"is the safety valve, not a normal-play limit.")]
|
"is the safety valve, not a normal-play limit.")]
|
||||||
[SerializeField] private int chatMaxMessages = int.MaxValue;
|
[SerializeField]
|
||||||
|
private int chatMaxMessages = int.MaxValue;
|
||||||
|
|
||||||
[Tooltip("Color used for SYSTEM chat messages (e.g. 'Life Lost', income changes).")]
|
[Tooltip("Color used for SYSTEM chat messages (e.g. 'Life Lost', income changes).")] [SerializeField]
|
||||||
[SerializeField] private Color chatSystemColor = new Color(1f, 0.7f, 0.2f);
|
private Color chatSystemColor = new Color(1f, 0.7f, 0.2f);
|
||||||
|
|
||||||
[Tooltip("Color used for PLAYER chat message bodies. Sender prefix uses the player's slot color.")]
|
[Tooltip("Color used for PLAYER chat message bodies. Sender prefix uses the player's slot color.")]
|
||||||
[SerializeField] private Color chatPlayerColor = new Color(0.92f, 0.92f, 0.92f);
|
[SerializeField]
|
||||||
|
private Color chatPlayerColor = new Color(0.92f, 0.92f, 0.92f);
|
||||||
|
|
||||||
// ----- Cached UI element references -------------------------------
|
// ----- Cached UI element references -------------------------------
|
||||||
|
|
||||||
private Label goldLabel;
|
private Label goldLabel;
|
||||||
private Label waveLabel;
|
private Label waveLabel;
|
||||||
private Label livesLabel;
|
private Label livesLabel;
|
||||||
private Label nextWaveLabel; // prep countdown ("next: 0:12")
|
private Label nextWaveLabel; // prep countdown ("next: 0:12")
|
||||||
private Label leakedLabel; // local player's origin-leak count ("leaked: 3")
|
private Label leakedLabel; // local player's origin-leak count ("leaked: 3")
|
||||||
private Label incomeLabel; // top-bar per-wave gold-earned counter ("+150 g/wave")
|
private Label incomeLabel; // top-bar per-wave gold-earned counter ("+150 g/wave")
|
||||||
private VisualElement playerListContainer; // right-panel scoreboard rows
|
private VisualElement playerListContainer; // right-panel scoreboard rows
|
||||||
private Label portraitName;
|
private Label portraitName;
|
||||||
private Label levelLabel;
|
private Label levelLabel;
|
||||||
private VisualElement statLines;
|
private VisualElement statLines;
|
||||||
private VisualElement commandGrid;
|
private VisualElement commandGrid;
|
||||||
|
|
||||||
private VisualElement actionFrame; // hidden via display:none when no actions are available
|
private VisualElement actionFrame; // hidden via display:none when no actions are available
|
||||||
private VisualElement commandTabs; // Build/Paint tab row — shown only for a Builder selection
|
private VisualElement commandTabs; // Build/Paint tab row — shown only for a Builder selection
|
||||||
private Button tabBuild;
|
private Button tabBuild;
|
||||||
|
|
@ -77,6 +82,7 @@ namespace TD.UI
|
||||||
private VisualElement buildProgressContainer; // info-panel sub-view, shown for BuildSiteVisual selections
|
private VisualElement buildProgressContainer; // info-panel sub-view, shown for BuildSiteVisual selections
|
||||||
private VisualElement buildProgressFill; // width driven each frame from progress
|
private VisualElement buildProgressFill; // width driven each frame from progress
|
||||||
private Label buildProgressPercent;
|
private Label buildProgressPercent;
|
||||||
|
|
||||||
private Label ttTitle;
|
private Label ttTitle;
|
||||||
private Label ttDesc;
|
private Label ttDesc;
|
||||||
private Label ttStats;
|
private Label ttStats;
|
||||||
|
|
@ -89,19 +95,23 @@ namespace TD.UI
|
||||||
// without rebuilding the elements.
|
// without rebuilding the elements.
|
||||||
private VisualElement enemyHealthBar;
|
private VisualElement enemyHealthBar;
|
||||||
private VisualElement enemyHealthFill;
|
private VisualElement enemyHealthFill;
|
||||||
private Label enemyHealthText;
|
private Label enemyHealthText;
|
||||||
|
|
||||||
// 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;
|
||||||
private Label matchEndTitle;
|
|
||||||
|
// Buff menu overlay — toggled by the B key via ToggleBuffMenu().
|
||||||
|
private VisualElement buffMenuOverlay;
|
||||||
|
private VisualElement buffMenuContent;
|
||||||
|
private Label matchEndTitle;
|
||||||
|
|
||||||
// Chat panel (bottom-left, above portrait) — programmatic. The container
|
// Chat panel (bottom-left, above portrait) — programmatic. The container
|
||||||
// holds both the scrollable feed and the input. Highlight + scroll
|
// holds both the scrollable feed and the input. Highlight + scroll
|
||||||
// interactivity are toggled on the container when typing.
|
// interactivity are toggled on the container when typing.
|
||||||
private VisualElement chatContainer;
|
private VisualElement chatContainer;
|
||||||
private ScrollView chatFeed;
|
private ScrollView chatFeed;
|
||||||
private TextField chatInput;
|
private TextField chatInput;
|
||||||
private bool chatInputOpen;
|
private bool chatInputOpen;
|
||||||
|
|
||||||
// Frame on which the chat input was opened or closed. Enter on that frame
|
// Frame on which the chat input was opened or closed. Enter on that frame
|
||||||
// and the next one is ignored to prevent the open/close-triggering keypress
|
// and the next one is ignored to prevent the open/close-triggering keypress
|
||||||
|
|
@ -123,12 +133,12 @@ namespace TD.UI
|
||||||
private CommandTab activeTab = CommandTab.Build;
|
private CommandTab activeTab = CommandTab.Build;
|
||||||
|
|
||||||
private Coroutine rejectionFadeCoroutine;
|
private Coroutine rejectionFadeCoroutine;
|
||||||
private bool placementManagerReady; // true once TowerPlacementManager.Instance is non-null
|
private bool placementManagerReady; // true once TowerPlacementManager.Instance is non-null
|
||||||
private bool uiInitialized;
|
private bool uiInitialized;
|
||||||
private bool selectionSubscribed; // true once we've successfully hooked SelectionState.OnSelectionChanged
|
private bool selectionSubscribed; // true once we've successfully hooked SelectionState.OnSelectionChanged
|
||||||
private bool matchStateSubscribed; // true once OnPhaseChanged is hooked
|
private bool matchStateSubscribed; // true once OnPhaseChanged is hooked
|
||||||
private MinimapView minimapView;
|
private MinimapView minimapView;
|
||||||
private IPanel myPanel; // tracked separately so OnDestroy only clears the static if it still points at us
|
private IPanel myPanel; // tracked separately so OnDestroy only clears the static if it still points at us
|
||||||
|
|
||||||
// ----- Hotkeys ----------------------------------------------------
|
// ----- Hotkeys ----------------------------------------------------
|
||||||
//
|
//
|
||||||
|
|
@ -150,10 +160,15 @@ namespace TD.UI
|
||||||
private readonly struct HotkeyBinding
|
private readonly struct HotkeyBinding
|
||||||
{
|
{
|
||||||
public readonly Key Key;
|
public readonly Key Key;
|
||||||
public readonly VisualElement Button; // for enabledSelf gating
|
public readonly VisualElement Button; // for enabledSelf gating
|
||||||
public readonly System.Action Action;
|
public readonly System.Action Action;
|
||||||
|
|
||||||
public HotkeyBinding(Key k, VisualElement b, System.Action a)
|
public HotkeyBinding(Key k, VisualElement b, System.Action a)
|
||||||
{ Key = k; Button = b; Action = a; }
|
{
|
||||||
|
Key = k;
|
||||||
|
Button = b;
|
||||||
|
Action = a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Static hit-test probe --------------------------------------
|
// ----- Static hit-test probe --------------------------------------
|
||||||
|
|
@ -249,6 +264,7 @@ namespace TD.UI
|
||||||
|
|
||||||
// Cache element references — log a warning for any that are missing
|
// Cache element references — log a warning for any that are missing
|
||||||
// so UXML/USS mismatches surface immediately.
|
// so UXML/USS mismatches surface immediately.
|
||||||
|
|
||||||
goldLabel = Require<Label>(root, "gold-label");
|
goldLabel = Require<Label>(root, "gold-label");
|
||||||
waveLabel = Require<Label>(root, "wave-label");
|
waveLabel = Require<Label>(root, "wave-label");
|
||||||
livesLabel = Require<Label>(root, "lives-label");
|
livesLabel = Require<Label>(root, "lives-label");
|
||||||
|
|
@ -267,14 +283,15 @@ namespace TD.UI
|
||||||
|
|
||||||
if (tabBuild != null) tabBuild.clicked += () => SwitchTab(CommandTab.Build);
|
if (tabBuild != null) tabBuild.clicked += () => SwitchTab(CommandTab.Build);
|
||||||
if (tabPaint != null) tabPaint.clicked += () => SwitchTab(CommandTab.Paint);
|
if (tabPaint != null) tabPaint.clicked += () => SwitchTab(CommandTab.Paint);
|
||||||
|
|
||||||
buildProgressContainer = Require<VisualElement>(root, "build-progress");
|
buildProgressContainer = Require<VisualElement>(root, "build-progress");
|
||||||
buildProgressFill = Require<VisualElement>(root, "build-progress-fill");
|
buildProgressFill = Require<VisualElement>(root, "build-progress-fill");
|
||||||
buildProgressPercent = Require<Label>(root, "build-progress-percent");
|
buildProgressPercent = Require<Label>(root, "build-progress-percent");
|
||||||
ttTitle = Require<Label>(root, "tt-title");
|
ttTitle = Require<Label>(root, "tt-title");
|
||||||
ttDesc = Require<Label>(root, "tt-desc");
|
ttDesc = Require<Label>(root, "tt-desc");
|
||||||
ttStats = Require<Label>(root, "tt-stats");
|
ttStats = Require<Label>(root, "tt-stats");
|
||||||
ttCost = Require<Label>(root, "tt-cost");
|
ttCost = Require<Label>(root, "tt-cost");
|
||||||
rejectionLabel = Require<Label>(root, "rejection-label");
|
rejectionLabel = Require<Label>(root, "rejection-label");
|
||||||
|
|
||||||
// Map area and its transparent ancestors must not consume pointer
|
// Map area and its transparent ancestors must not consume pointer
|
||||||
// events so clicks reach the 3D scene underneath. The bottom-ui is now
|
// events so clicks reach the 3D scene underneath. The bottom-ui is now
|
||||||
|
|
@ -309,6 +326,9 @@ 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);
|
||||||
|
|
||||||
// Chat feed + input. Anchored bottom-left, just above the portrait/bottom-ui bar.
|
// Chat feed + input. Anchored bottom-left, just above the portrait/bottom-ui bar.
|
||||||
// Player typing toggled with Enter; system messages (e.g. life lost) post via
|
// Player typing toggled with Enter; system messages (e.g. life lost) post via
|
||||||
// ChatService.PostLocalSystem on every peer.
|
// ChatService.PostLocalSystem on every peer.
|
||||||
|
|
@ -323,11 +343,16 @@ namespace TD.UI
|
||||||
uiInitialized = true;
|
uiInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
TowerPlacementController.OnRejectionMessageReady += ShowRejectionMessage;
|
TowerPlacementController.OnRejectionMessageReady += ShowRejectionMessage;
|
||||||
WaveManager.OnLifeLost += HandleLifeLost;
|
WaveManager.OnLifeLost += HandleLifeLost;
|
||||||
ChatService.OnMessageReceived += HandleChatMessage;
|
ChatService.OnMessageReceived += HandleChatMessage;
|
||||||
// Try to subscribe now; if SelectionState.Awake hasn't run yet (Unity does
|
// Try to subscribe now; if SelectionState.Awake hasn't run yet (Unity does
|
||||||
// not guarantee Awake/OnEnable ordering across objects), Start will retry.
|
// not guarantee Awake/OnEnable ordering across objects), Start will retry.
|
||||||
TrySubscribeSelection();
|
TrySubscribeSelection();
|
||||||
|
|
@ -336,13 +361,14 @@ namespace TD.UI
|
||||||
private void OnDisable()
|
private void OnDisable()
|
||||||
{
|
{
|
||||||
TowerPlacementController.OnRejectionMessageReady -= ShowRejectionMessage;
|
TowerPlacementController.OnRejectionMessageReady -= ShowRejectionMessage;
|
||||||
WaveManager.OnLifeLost -= HandleLifeLost;
|
WaveManager.OnLifeLost -= HandleLifeLost;
|
||||||
ChatService.OnMessageReceived -= HandleChatMessage;
|
ChatService.OnMessageReceived -= HandleChatMessage;
|
||||||
if (selectionSubscribed && SelectionState.Instance != null)
|
if (selectionSubscribed && SelectionState.Instance != null)
|
||||||
{
|
{
|
||||||
SelectionState.Instance.OnSelectionChanged -= HandleSelectionChanged;
|
SelectionState.Instance.OnSelectionChanged -= HandleSelectionChanged;
|
||||||
selectionSubscribed = false;
|
selectionSubscribed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchStateSubscribed && MatchState.Instance != null)
|
if (matchStateSubscribed && MatchState.Instance != null)
|
||||||
{
|
{
|
||||||
MatchState.Instance.OnPhaseChanged -= HandlePhaseChanged;
|
MatchState.Instance.OnPhaseChanged -= HandlePhaseChanged;
|
||||||
|
|
@ -407,6 +433,7 @@ namespace TD.UI
|
||||||
buildProgressFill.style.width =
|
buildProgressFill.style.width =
|
||||||
new StyleLength(new Length(progress * 100f, LengthUnit.Percent));
|
new StyleLength(new Length(progress * 100f, LengthUnit.Percent));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buildProgressPercent != null)
|
if (buildProgressPercent != null)
|
||||||
{
|
{
|
||||||
buildProgressPercent.text = $"{Mathf.RoundToInt(progress * 100f)}%";
|
buildProgressPercent.text = $"{Mathf.RoundToInt(progress * 100f)}%";
|
||||||
|
|
@ -437,6 +464,8 @@ namespace TD.UI
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
|
|
||||||
minimapView?.Dispose();
|
minimapView?.Dispose();
|
||||||
minimapView = null;
|
minimapView = null;
|
||||||
|
|
||||||
|
|
@ -592,8 +621,8 @@ namespace TD.UI
|
||||||
nameLabel.style.color = PlayerColors.Get(pms.Slot);
|
nameLabel.style.color = PlayerColors.Get(pms.Slot);
|
||||||
nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||||
nameLabel.style.fontSize = 10;
|
nameLabel.style.fontSize = 10;
|
||||||
nameLabel.style.whiteSpace = WhiteSpace.NoWrap; // keep name on one line
|
nameLabel.style.whiteSpace = WhiteSpace.NoWrap; // keep name on one line
|
||||||
nameLabel.style.flexGrow = 1; // takes all leftover width
|
nameLabel.style.flexGrow = 1; // takes all leftover width
|
||||||
nameLabel.style.flexShrink = 1;
|
nameLabel.style.flexShrink = 1;
|
||||||
nameLabel.style.overflow = Overflow.Hidden;
|
nameLabel.style.overflow = Overflow.Hidden;
|
||||||
nameLabel.style.textOverflow = TextOverflow.Ellipsis;
|
nameLabel.style.textOverflow = TextOverflow.Ellipsis;
|
||||||
|
|
@ -637,6 +666,7 @@ namespace TD.UI
|
||||||
// gated by the lobby), but the values do.
|
// gated by the lobby), but the values do.
|
||||||
private static readonly System.Text.StringBuilder s_scoreboardSigBuf =
|
private static readonly System.Text.StringBuilder s_scoreboardSigBuf =
|
||||||
new System.Text.StringBuilder(64);
|
new System.Text.StringBuilder(64);
|
||||||
|
|
||||||
private string ComputeScoreboardSignature(System.Collections.Generic.List<PlayerMatchState> players)
|
private string ComputeScoreboardSignature(System.Collections.Generic.List<PlayerMatchState> players)
|
||||||
{
|
{
|
||||||
s_scoreboardSigBuf.Clear();
|
s_scoreboardSigBuf.Clear();
|
||||||
|
|
@ -646,12 +676,14 @@ namespace TD.UI
|
||||||
var gm = PlayerGoldManager.GetForClient(pms.OwnerClientId);
|
var gm = PlayerGoldManager.GetForClient(pms.OwnerClientId);
|
||||||
int gold = gm != null ? gm.CurrentGold : 0;
|
int gold = gm != null ? gm.CurrentGold : 0;
|
||||||
int leaks = (wm != null && pms.Slot != PlayerSlot.None)
|
int leaks = (wm != null && pms.Slot != PlayerSlot.None)
|
||||||
? wm.GetZoneLeakCount(pms.Slot) : 0;
|
? wm.GetZoneLeakCount(pms.Slot)
|
||||||
|
: 0;
|
||||||
s_scoreboardSigBuf.Append((int)pms.Slot).Append(':')
|
s_scoreboardSigBuf.Append((int)pms.Slot).Append(':')
|
||||||
.Append(pms.DisplayName ?? string.Empty).Append(':')
|
.Append(pms.DisplayName ?? string.Empty).Append(':')
|
||||||
.Append(gold).Append(':')
|
.Append(gold).Append(':')
|
||||||
.Append(leaks).Append(';');
|
.Append(leaks).Append(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
return s_scoreboardSigBuf.ToString();
|
return s_scoreboardSigBuf.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -659,7 +691,7 @@ namespace TD.UI
|
||||||
|
|
||||||
private const int GRID_COLS = 5;
|
private const int GRID_COLS = 5;
|
||||||
private const int GRID_ROWS = 3;
|
private const int GRID_ROWS = 3;
|
||||||
private const int GRID_MAX = GRID_COLS * GRID_ROWS;
|
private const int GRID_MAX = GRID_COLS * GRID_ROWS;
|
||||||
|
|
||||||
// First-time check that TowerPlacementManager exists. Once ready, populates the
|
// First-time check that TowerPlacementManager exists. Once ready, populates the
|
||||||
// grid for the current selection. Subsequent populates flow through
|
// grid for the current selection. Subsequent populates flow through
|
||||||
|
|
@ -669,7 +701,7 @@ namespace TD.UI
|
||||||
if (placementManager == null)
|
if (placementManager == null)
|
||||||
placementManager = TowerPlacementManager.Instance;
|
placementManager = TowerPlacementManager.Instance;
|
||||||
|
|
||||||
if (placementManager == null) return; // not spawned yet — retry next frame
|
if (placementManager == null) return; // not spawned yet — retry next frame
|
||||||
|
|
||||||
placementManagerReady = true;
|
placementManagerReady = true;
|
||||||
PopulateGridForSelection(SelectionState.Instance?.SelectedObject);
|
PopulateGridForSelection(SelectionState.Instance?.SelectedObject);
|
||||||
|
|
@ -684,7 +716,7 @@ namespace TD.UI
|
||||||
private void PopulateGridForSelection(ISelectable selection)
|
private void PopulateGridForSelection(ISelectable selection)
|
||||||
{
|
{
|
||||||
if (commandGrid == null) return;
|
if (commandGrid == null) return;
|
||||||
if (!placementManagerReady) return; // deferred to TryReadyPlacementManager
|
if (!placementManagerReady) return; // deferred to TryReadyPlacementManager
|
||||||
|
|
||||||
// Selection changed — invalidate the previous frame's hotkey bindings
|
// Selection changed — invalidate the previous frame's hotkey bindings
|
||||||
// before creating new ones. Without this, stale buttons from a previous
|
// before creating new ones. Without this, stale buttons from a previous
|
||||||
|
|
@ -694,8 +726,8 @@ namespace TD.UI
|
||||||
// Decide whether any actions exist for this selection. The action frame
|
// Decide whether any actions exist for this selection. The action frame
|
||||||
// is hidden entirely when there are none — matches the WC3-style UX.
|
// is hidden entirely when there are none — matches the WC3-style UX.
|
||||||
bool hasActions = selection is Builder
|
bool hasActions = selection is Builder
|
||||||
|| selection is TowerInstance
|
|| selection is TowerInstance
|
||||||
|| selection is BuildSiteVisual;
|
|| selection is BuildSiteVisual;
|
||||||
if (actionFrame != null)
|
if (actionFrame != null)
|
||||||
actionFrame.style.display = hasActions ? DisplayStyle.Flex : DisplayStyle.None;
|
actionFrame.style.display = hasActions ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
|
|
||||||
|
|
@ -713,7 +745,7 @@ namespace TD.UI
|
||||||
RefreshTabActiveState();
|
RefreshTabActiveState();
|
||||||
|
|
||||||
commandGrid.Clear();
|
commandGrid.Clear();
|
||||||
if (!hasActions) return; // grid stays empty; frame is hidden anyway
|
if (!hasActions) return; // grid stays empty; frame is hidden anyway
|
||||||
|
|
||||||
// Build the 15-cell action layout for the active selection kind.
|
// Build the 15-cell action layout for the active selection kind.
|
||||||
var cells = new VisualElement[GRID_MAX];
|
var cells = new VisualElement[GRID_MAX];
|
||||||
|
|
@ -738,12 +770,14 @@ namespace TD.UI
|
||||||
cells[i] = CreateTowerButton(def, typeId, HotkeyLayout[i]);
|
cells[i] = CreateTowerButton(def, typeId, HotkeyLayout[i]);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
cells[GRID_MAX - 1] = CreateBuffMenuButton(HotkeyLayout[GRID_MAX - 1]);
|
||||||
}
|
}
|
||||||
else if (selection is TowerInstance tower)
|
else if (selection is TowerInstance tower)
|
||||||
{
|
{
|
||||||
// WC3 layout convention: primary action top-left (Q), sell bottom-right (B).
|
// WC3 layout convention: primary action top-left (Q), sell bottom-right (B).
|
||||||
cells[0] = CreateUpgradeButton(tower, HotkeyLayout[0]);
|
cells[0] = CreateUpgradeButton(tower, HotkeyLayout[0]);
|
||||||
cells[GRID_MAX - 1] = CreateSellButton(tower, HotkeyLayout[GRID_MAX - 1]);
|
cells[GRID_MAX - 1] = CreateSellButton(tower, HotkeyLayout[GRID_MAX - 1]);
|
||||||
}
|
}
|
||||||
else if (selection is BuildSiteVisual bsv)
|
else if (selection is BuildSiteVisual bsv)
|
||||||
|
|
@ -892,6 +926,7 @@ namespace TD.UI
|
||||||
costLabel.pickingMode = PickingMode.Ignore;
|
costLabel.pickingMode = PickingMode.Ignore;
|
||||||
btn.Add(costLabel);
|
btn.Add(costLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -901,9 +936,12 @@ namespace TD.UI
|
||||||
private VisualElement CreateUpgradeButton(TowerInstance tower, Key hotkey)
|
private VisualElement CreateUpgradeButton(TowerInstance tower, Key hotkey)
|
||||||
{
|
{
|
||||||
var btn = CreateActionButton(
|
var btn = CreateActionButton(
|
||||||
costText: "", // tier cost unknown until upgrade system lands
|
costText: "", // tier cost unknown until upgrade system lands
|
||||||
hotkey: hotkey,
|
hotkey: hotkey,
|
||||||
onClick: () => { /* TODO: upgrade flow */ });
|
onClick: () =>
|
||||||
|
{
|
||||||
|
/* TODO: upgrade flow */
|
||||||
|
});
|
||||||
btn.SetEnabled(false);
|
btn.SetEnabled(false);
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
@ -915,12 +953,23 @@ namespace TD.UI
|
||||||
: 0;
|
: 0;
|
||||||
var btn = CreateActionButton(
|
var btn = CreateActionButton(
|
||||||
costText: sellValue > 0 ? $"+{sellValue}g" : "",
|
costText: sellValue > 0 ? $"+{sellValue}g" : "",
|
||||||
hotkey: hotkey,
|
hotkey: hotkey,
|
||||||
onClick: () => { /* TODO: sell flow */ });
|
onClick: () =>
|
||||||
|
{
|
||||||
|
/* TODO: sell flow */
|
||||||
|
});
|
||||||
btn.SetEnabled(false);
|
btn.SetEnabled(false);
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private VisualElement CreateBuffMenuButton(Key hotkey)
|
||||||
|
{
|
||||||
|
return CreateActionButton(
|
||||||
|
costText: "Buffs",
|
||||||
|
hotkey: hotkey,
|
||||||
|
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
|
||||||
|
|
@ -1208,6 +1257,149 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,12 @@
|
||||||
"com.unity.netcode.gameobjects": "2.11.0",
|
"com.unity.netcode.gameobjects": "2.11.0",
|
||||||
"com.unity.probuilder": "6.0.9",
|
"com.unity.probuilder": "6.0.9",
|
||||||
"com.unity.render-pipelines.universal": "17.4.0",
|
"com.unity.render-pipelines.universal": "17.4.0",
|
||||||
"com.unity.sdk.linux-x86_64": "1.0.2",
|
"com.unity.sdk.linux-x86_64": "1.1.0",
|
||||||
"com.unity.services.multiplayer": "2.1.3",
|
"com.unity.services.multiplayer": "2.1.3",
|
||||||
"com.unity.terrain-tools": "5.3.2",
|
"com.unity.terrain-tools": "5.3.2",
|
||||||
"com.unity.test-framework": "1.6.0",
|
"com.unity.test-framework": "1.6.0",
|
||||||
"com.unity.timeline": "1.8.12",
|
"com.unity.timeline": "1.8.12",
|
||||||
|
"com.unity.toolchain.linux-x86_64-linux": "1.1.0",
|
||||||
"com.unity.toolchain.win-x86_64-linux": "1.0.2",
|
"com.unity.toolchain.win-x86_64-linux": "1.0.2",
|
||||||
"com.unity.ugui": "2.0.0",
|
"com.unity.ugui": "2.0.0",
|
||||||
"com.unity.visualscripting": "1.9.11",
|
"com.unity.visualscripting": "1.9.11",
|
||||||
|
|
|
||||||
|
|
@ -221,11 +221,11 @@
|
||||||
"url": "https://packages.unity.com"
|
"url": "https://packages.unity.com"
|
||||||
},
|
},
|
||||||
"com.unity.sdk.linux-x86_64": {
|
"com.unity.sdk.linux-x86_64": {
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "registry",
|
"source": "registry",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"com.unity.sysroot.base": "1.0.2"
|
"com.unity.sysroot.base": "1.1.0"
|
||||||
},
|
},
|
||||||
"url": "https://packages.unity.com"
|
"url": "https://packages.unity.com"
|
||||||
},
|
},
|
||||||
|
|
@ -355,7 +355,7 @@
|
||||||
"url": "https://packages.unity.com"
|
"url": "https://packages.unity.com"
|
||||||
},
|
},
|
||||||
"com.unity.sysroot.base": {
|
"com.unity.sysroot.base": {
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
"source": "registry",
|
"source": "registry",
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
|
|
@ -403,6 +403,15 @@
|
||||||
},
|
},
|
||||||
"url": "https://packages.unity.com"
|
"url": "https://packages.unity.com"
|
||||||
},
|
},
|
||||||
|
"com.unity.toolchain.linux-x86_64-linux": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"depth": 0,
|
||||||
|
"source": "registry",
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.sysroot.base": "1.1.0"
|
||||||
|
},
|
||||||
|
"url": "https://packages.unity.com"
|
||||||
|
},
|
||||||
"com.unity.toolchain.win-x86_64-linux": {
|
"com.unity.toolchain.win-x86_64-linux": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue