Merge branch 'main' of https://git.marlboro-bc.duckdns.org/yeahweregames/UnityTowerDefense
This commit is contained in:
commit
4e11842d3e
16 changed files with 463 additions and 19 deletions
|
|
@ -20,4 +20,7 @@ MonoBehaviour:
|
||||||
LoreText: Crucified and brought back by God because her job wasn't finished. She's
|
LoreText: Crucified and brought back by God because her job wasn't finished. She's
|
||||||
here to bring death to to Xenos scum.
|
here to bring death to to Xenos scum.
|
||||||
BuilderPrefab: {fileID: 116861493430507844, guid: 3398cc5831880954487717577f61b6d7, type: 3}
|
BuilderPrefab: {fileID: 116861493430507844, guid: 3398cc5831880954487717577f61b6d7, type: 3}
|
||||||
Towers: []
|
Towers:
|
||||||
|
- {fileID: 11400000, guid: 0f693e29ca953e1439e10cb8f12e4b30, type: 2}
|
||||||
|
- {fileID: 11400000, guid: c4a1f7e92b6d8a4e3f0c9b1d6e5a2f80, type: 2}
|
||||||
|
- {fileID: 11400000, guid: d5b2e8fa3c7e9b5f4a1d0c2e7f6b3a91, type: 2}
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,7 @@ MonoBehaviour:
|
||||||
LoreText: King of the protoss, he's here to kick bubblegum and chew ass, and he's
|
LoreText: King of the protoss, he's here to kick bubblegum and chew ass, and he's
|
||||||
all out of ass.
|
all out of ass.
|
||||||
BuilderPrefab: {fileID: 116861493430507844, guid: 3398cc5831880954487717577f61b6d7, type: 3}
|
BuilderPrefab: {fileID: 116861493430507844, guid: 3398cc5831880954487717577f61b6d7, type: 3}
|
||||||
Towers: []
|
Towers:
|
||||||
|
- {fileID: 11400000, guid: 0f693e29ca953e1439e10cb8f12e4b30, type: 2}
|
||||||
|
- {fileID: 11400000, guid: c4a1f7e92b6d8a4e3f0c9b1d6e5a2f80, type: 2}
|
||||||
|
- {fileID: 11400000, guid: d5b2e8fa3c7e9b5f4a1d0c2e7f6b3a91, type: 2}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@ MonoBehaviour:
|
||||||
m_Script: {fileID: 11500000, guid: 7b353a757b6e6774d97e6fb8ba138fcc, type: 3}
|
m_Script: {fileID: 11500000, guid: 7b353a757b6e6774d97e6fb8ba138fcc, type: 3}
|
||||||
m_Name: BasicTower
|
m_Name: BasicTower
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Towers.TowerDefinition
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Towers.TowerDefinition
|
||||||
DisplayName: A basic tower for testing.
|
DisplayName: Basic Arrow
|
||||||
Description:
|
Description: Targets ground and air. Fires single-target arrows. The bread and butter
|
||||||
|
of any maze.
|
||||||
FootprintSize: {x: 2, y: 2}
|
FootprintSize: {x: 2, y: 2}
|
||||||
GoldCost: 25
|
GoldCost: 25
|
||||||
BuildTime: 0.5
|
BuildTime: 0.5
|
||||||
|
|
|
||||||
37
Assets/_Project/Definitions/Towers/SiegeCannon.asset
Normal file
37
Assets/_Project/Definitions/Towers/SiegeCannon.asset
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
%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: 7b353a757b6e6774d97e6fb8ba138fcc, type: 3}
|
||||||
|
m_Name: SiegeCannon
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Towers.TowerDefinition
|
||||||
|
DisplayName: Siege Cannon
|
||||||
|
Description: Ground-only. Lobs a ballistic shell that deals splash damage around the
|
||||||
|
point of impact.
|
||||||
|
FootprintSize: {x: 2, y: 2}
|
||||||
|
GoldCost: 60
|
||||||
|
BuildTime: 0.5
|
||||||
|
TowerPrefab: {fileID: 6482414459531823157, guid: 1511641f145758b469e64376d2a0d434, type: 3}
|
||||||
|
DamageType: 0
|
||||||
|
TargetPriority: 0
|
||||||
|
TargetType: 1
|
||||||
|
GroundedOnly: 1
|
||||||
|
Damage: 25
|
||||||
|
Range: 16
|
||||||
|
FireRate: 1
|
||||||
|
SplashRadius: 4
|
||||||
|
ChainCount: 0
|
||||||
|
ChainRange: 0
|
||||||
|
SlowFactor: 0
|
||||||
|
DotDamagePerSecond: 0
|
||||||
|
EffectDuration: 0
|
||||||
|
ProjectilePrefab: {fileID: 2664719039363295382, guid: dc2e4a4108e03874a8b2dab88dcc8fba, type: 3}
|
||||||
|
ProjectileSpeed: 12
|
||||||
|
UpgradePaths: []
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c4a1f7e92b6d8a4e3f0c9b1d6e5a2f80
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
36
Assets/_Project/Definitions/Towers/Wall.asset
Normal file
36
Assets/_Project/Definitions/Towers/Wall.asset
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
%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: 7b353a757b6e6774d97e6fb8ba138fcc, type: 3}
|
||||||
|
m_Name: Wall
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Towers.TowerDefinition
|
||||||
|
DisplayName: Wall
|
||||||
|
Description: Deals no damage and costs next to nothing. Use it to shape your maze.
|
||||||
|
FootprintSize: {x: 2, y: 2}
|
||||||
|
GoldCost: 1
|
||||||
|
BuildTime: 0.5
|
||||||
|
TowerPrefab: {fileID: 6482414459531823157, guid: 1511641f145758b469e64376d2a0d434, type: 3}
|
||||||
|
DamageType: 0
|
||||||
|
TargetPriority: 0
|
||||||
|
TargetType: 0
|
||||||
|
GroundedOnly: 0
|
||||||
|
Damage: 0
|
||||||
|
Range: 0
|
||||||
|
FireRate: 0
|
||||||
|
SplashRadius: 0
|
||||||
|
ChainCount: 0
|
||||||
|
ChainRange: 0
|
||||||
|
SlowFactor: 0
|
||||||
|
DotDamagePerSecond: 0
|
||||||
|
EffectDuration: 0
|
||||||
|
ProjectilePrefab: {fileID: 0}
|
||||||
|
ProjectileSpeed: 0
|
||||||
|
UpgradePaths: []
|
||||||
8
Assets/_Project/Definitions/Towers/Wall.asset.meta
Normal file
8
Assets/_Project/Definitions/Towers/Wall.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d5b2e8fa3c7e9b5f4a1d0c2e7f6b3a91
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -14,6 +14,7 @@ GameObject:
|
||||||
- component: {fileID: 7845089877743661692}
|
- component: {fileID: 7845089877743661692}
|
||||||
- component: {fileID: 4336209376377567030}
|
- component: {fileID: 4336209376377567030}
|
||||||
- component: {fileID: 2806524246861401760}
|
- component: {fileID: 2806524246861401760}
|
||||||
|
- component: {fileID: 2806524246861401799}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: Player
|
m_Name: Player
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
|
|
@ -117,3 +118,16 @@ MonoBehaviour:
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
categories:
|
categories:
|
||||||
- {fileID: 11400000, guid: d8ed3b9535538f7fc82be878cc307d26, type: 2}
|
- {fileID: 11400000, guid: d8ed3b9535538f7fc82be878cc307d26, type: 2}
|
||||||
|
--- !u!114 &2806524246861401799
|
||||||
|
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: b7e4c91a08f5d2461a3f8e6c25b09d74, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerTowerDeck
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
|
|
||||||
|
|
@ -19772,6 +19772,12 @@ MonoBehaviour:
|
||||||
towerDefinitions:
|
towerDefinitions:
|
||||||
- {fileID: 0}
|
- {fileID: 0}
|
||||||
- {fileID: 11400000, guid: 0f693e29ca953e1439e10cb8f12e4b30, type: 2}
|
- {fileID: 11400000, guid: 0f693e29ca953e1439e10cb8f12e4b30, type: 2}
|
||||||
|
- {fileID: 11400000, guid: c4a1f7e92b6d8a4e3f0c9b1d6e5a2f80, type: 2}
|
||||||
|
- {fileID: 11400000, guid: d5b2e8fa3c7e9b5f4a1d0c2e7f6b3a91, type: 2}
|
||||||
|
startingDeck:
|
||||||
|
- {fileID: 11400000, guid: d5b2e8fa3c7e9b5f4a1d0c2e7f6b3a91, type: 2}
|
||||||
|
- {fileID: 11400000, guid: 0f693e29ca953e1439e10cb8f12e4b30, type: 2}
|
||||||
|
- {fileID: 11400000, guid: c4a1f7e92b6d8a4e3f0c9b1d6e5a2f80, type: 2}
|
||||||
--- !u!4 &1507514109
|
--- !u!4 &1507514109
|
||||||
Transform:
|
Transform:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -22520,6 +22526,7 @@ MonoBehaviour:
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Dev.DevWaveControls
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Dev.DevWaveControls
|
||||||
hotkey: 94
|
hotkey: 94
|
||||||
|
grantTowerHotkey: 101
|
||||||
--- !u!4 &1731269687
|
--- !u!4 &1731269687
|
||||||
Transform:
|
Transform:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,21 @@ namespace TD.Dev
|
||||||
"Set to Key.None to use the OnGUI button only.")]
|
"Set to Key.None to use the OnGUI button only.")]
|
||||||
[SerializeField] private Key hotkey = Key.F9;
|
[SerializeField] private Key hotkey = Key.F9;
|
||||||
|
|
||||||
|
[Tooltip("Keyboard shortcut to grant the local player the next catalog tower not " +
|
||||||
|
"yet in their deck. Stand-in for the draft system so deck growth can be " +
|
||||||
|
"verified now. Set to Key.None to use the OnGUI button only.")]
|
||||||
|
[SerializeField] private Key grantTowerHotkey = Key.F8;
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (hotkey == Key.None) return;
|
|
||||||
|
|
||||||
var kb = Keyboard.current;
|
var kb = Keyboard.current;
|
||||||
if (kb == null) return; // no keyboard connected (e.g. headless server)
|
if (kb == null) return; // no keyboard connected (e.g. headless server)
|
||||||
if (!kb[hotkey].wasPressedThisFrame) return;
|
|
||||||
|
|
||||||
TryForceNextWave();
|
if (hotkey != Key.None && kb[hotkey].wasPressedThisFrame)
|
||||||
|
TryForceNextWave();
|
||||||
|
|
||||||
|
if (grantTowerHotkey != Key.None && kb[grantTowerHotkey].wasPressedThisFrame)
|
||||||
|
TryGrantNextTower();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGUI()
|
private void OnGUI()
|
||||||
|
|
@ -48,9 +54,44 @@ namespace TD.Dev
|
||||||
|
|
||||||
// Anchored below the top HUD bar so it doesn't overlap gold/wave/lives.
|
// Anchored below the top HUD bar so it doesn't overlap gold/wave/lives.
|
||||||
const float topOffset = 90f;
|
const float topOffset = 90f;
|
||||||
GUI.Box(new Rect(10, topOffset, 180, 60), "Dev: Wave Controls");
|
GUI.Box(new Rect(10, topOffset, 180, 95), "Dev: Wave Controls");
|
||||||
if (GUI.Button(new Rect(20, topOffset + 25, 160, 28), "Force Next Wave"))
|
if (GUI.Button(new Rect(20, topOffset + 25, 160, 28), "Force Next Wave"))
|
||||||
TryForceNextWave();
|
TryForceNextWave();
|
||||||
|
if (GUI.Button(new Rect(20, topOffset + 58, 160, 28), "Grant Next Tower"))
|
||||||
|
TryGrantNextTower();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dev stand-in for the draft system: grants the local player the first catalog
|
||||||
|
// tower they haven't unlocked yet, so deck growth + the HUD's live rebuild can be
|
||||||
|
// verified before the real draft exists. Server-only (host); ServerGrantTower
|
||||||
|
// no-ops elsewhere.
|
||||||
|
private void TryGrantNextTower()
|
||||||
|
{
|
||||||
|
if (NetworkManager.Singleton == null || !NetworkManager.Singleton.IsServer)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[DevWaveControls] Grant-tower requested off the server. Ignored.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var placement = TowerPlacementManager.Instance;
|
||||||
|
var deck = PlayerTowerDeck.Local;
|
||||||
|
if (placement == null || deck == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[DevWaveControls] Cannot grant tower — TowerPlacementManager " +
|
||||||
|
"or local PlayerTowerDeck is not ready.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (def, typeId) in placement.GetAvailableDefinitions())
|
||||||
|
{
|
||||||
|
if (deck.Contains(typeId)) continue;
|
||||||
|
if (deck.ServerGrantTower(typeId))
|
||||||
|
Debug.Log($"[DevWaveControls] Granted '{def.DisplayName}' (typeId {typeId}) " +
|
||||||
|
$"to the local deck.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log("[DevWaveControls] Local deck already contains every catalog tower.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TryForceNextWave()
|
private void TryForceNextWave()
|
||||||
|
|
|
||||||
150
Assets/_Project/Scripts/Gameplay/PlayerTowerDeck.cs
Normal file
150
Assets/_Project/Scripts/Gameplay/PlayerTowerDeck.cs
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/PlayerTowerDeck.cs
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Per-player set of unlocked tower types — the player's "deck". Lives on the
|
||||||
|
/// Player prefab alongside <see cref="PlayerGoldManager"/>,
|
||||||
|
/// <see cref="PlayerMatchState"/>, and <see cref="PlayerBuffManager"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Roguelike keystone.</b> Players no longer see every tower in the match
|
||||||
|
/// catalog. Each starts with a small base set (the starter towers) and grows the
|
||||||
|
/// deck through draft choices. This component is the data structure that growth
|
||||||
|
/// targets.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Identity.</b> The deck stores <c>TowerTypeId</c>s — indices into
|
||||||
|
/// <see cref="TowerPlacementManager"/>'s catalog (<c>towerDefinitions[]</c>), the
|
||||||
|
/// same identifier the placement RPC, <see cref="Builder"/>, and
|
||||||
|
/// <see cref="BuildJob"/> already use. No new network identifier is introduced.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Authority.</b> Server-only mutation (<see cref="ServerInitialize"/> /
|
||||||
|
/// <see cref="ServerGrantTower"/>). The owning client reads its own deck to build
|
||||||
|
/// the HUD command grid; the server reads it to gate placement
|
||||||
|
/// (<see cref="TowerPlacementManager"/> rejects a requested tower that isn't in the
|
||||||
|
/// requesting player's deck — the anti-cheat seam).</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Persistence caveat.</b> <c>TowerTypeId</c> is a catalog index — stable
|
||||||
|
/// within a single match but NOT across sessions. Cross-match deck persistence (a
|
||||||
|
/// later keystone) will need a stable per-tower id (asset GUID or a serialized
|
||||||
|
/// StableId on <c>TowerDefinition</c>).</para>
|
||||||
|
/// </remarks>
|
||||||
|
public class PlayerTowerDeck : NetworkBehaviour
|
||||||
|
{
|
||||||
|
// ----- Static registry (mirrors PlayerGoldManager) ----------------
|
||||||
|
|
||||||
|
private static readonly Dictionary<ulong, PlayerTowerDeck> s_byClientId
|
||||||
|
= new Dictionary<ulong, PlayerTowerDeck>();
|
||||||
|
|
||||||
|
/// <summary>Returns the deck owned by the given client, or null.</summary>
|
||||||
|
public static PlayerTowerDeck GetForClient(ulong clientId)
|
||||||
|
{
|
||||||
|
s_byClientId.TryGetValue(clientId, out var deck);
|
||||||
|
return deck;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Convenience: the local client's own deck. Null on a dedicated
|
||||||
|
/// server or before the local player has spawned.</summary>
|
||||||
|
public static PlayerTowerDeck Local
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var nm = NetworkManager.Singleton;
|
||||||
|
if (nm == null || !nm.IsClient) return null;
|
||||||
|
return GetForClient(nm.LocalClientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Networked state --------------------------------------------
|
||||||
|
|
||||||
|
// Unlocked tower type ids (indices into TowerPlacementManager's catalog).
|
||||||
|
// readPerm defaults to Everyone (consistent with the other per-player
|
||||||
|
// managers); only the owner consumes it for the HUD. writePerm Server.
|
||||||
|
private NetworkList<int> unlockedTypeIds;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired on every peer when the deck contents change (server init, tower
|
||||||
|
/// granted). The HUD subscribes to rebuild the command grid live when a draft
|
||||||
|
/// grant lands while the builder is already selected.
|
||||||
|
/// </summary>
|
||||||
|
public event System.Action OnDeckChanged;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
unlockedTypeIds = new NetworkList<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- NGO lifecycle ----------------------------------------------
|
||||||
|
|
||||||
|
public override void OnNetworkSpawn()
|
||||||
|
{
|
||||||
|
s_byClientId[OwnerClientId] = this;
|
||||||
|
unlockedTypeIds.OnListChanged += HandleListChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkDespawn()
|
||||||
|
{
|
||||||
|
unlockedTypeIds.OnListChanged -= HandleListChanged;
|
||||||
|
|
||||||
|
if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
|
||||||
|
s_byClientId.Remove(OwnerClientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleListChanged(NetworkListEvent<int> _) => OnDeckChanged?.Invoke();
|
||||||
|
|
||||||
|
// ----- Read API ---------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>Number of towers unlocked in this deck.</summary>
|
||||||
|
public int Count => unlockedTypeIds.Count;
|
||||||
|
|
||||||
|
/// <summary>The TowerTypeId at the given deck slot (0..Count-1).</summary>
|
||||||
|
public int GetTypeIdAt(int index) => unlockedTypeIds[index];
|
||||||
|
|
||||||
|
/// <summary>True if the given TowerTypeId is unlocked in this deck.</summary>
|
||||||
|
public bool Contains(int towerTypeId)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < unlockedTypeIds.Count; i++)
|
||||||
|
if (unlockedTypeIds[i] == towerTypeId) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Server mutation --------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: replace the deck with the given starting set. Called at match
|
||||||
|
/// start (<see cref="WaveManager"/>) so Retry / return-to-lobby cycles reset
|
||||||
|
/// the deck cleanly. Ignores ids <= 0 (the reserved catalog index) and
|
||||||
|
/// silently de-dupes.
|
||||||
|
/// </summary>
|
||||||
|
public void ServerInitialize(IReadOnlyList<int> startingTypeIds)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
|
||||||
|
unlockedTypeIds.Clear();
|
||||||
|
if (startingTypeIds == null) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < startingTypeIds.Count; i++)
|
||||||
|
{
|
||||||
|
int id = startingTypeIds[i];
|
||||||
|
if (id > 0 && !Contains(id)) unlockedTypeIds.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: add a tower type to this deck (a draft grant). No-op if the id
|
||||||
|
/// is invalid or already present. Returns true if it was actually added.
|
||||||
|
/// </summary>
|
||||||
|
public bool ServerGrantTower(int towerTypeId)
|
||||||
|
{
|
||||||
|
if (!IsServer) return false;
|
||||||
|
if (towerTypeId <= 0) return false;
|
||||||
|
if (Contains(towerTypeId)) return false;
|
||||||
|
|
||||||
|
unlockedTypeIds.Add(towerTypeId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Assets/_Project/Scripts/Gameplay/PlayerTowerDeck.cs.meta
Normal file
11
Assets/_Project/Scripts/Gameplay/PlayerTowerDeck.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b7e4c91a08f5d2461a3f8e6c25b09d74
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -71,12 +71,17 @@ namespace TD.Gameplay
|
||||||
"worst-case 90/second (9 players × 10 placements/second).")]
|
"worst-case 90/second (9 players × 10 placements/second).")]
|
||||||
[SerializeField] private int requestsPerFrame = 3;
|
[SerializeField] private int requestsPerFrame = 3;
|
||||||
|
|
||||||
[Tooltip("Tower definitions available in this match, indexed by TowerTypeId. " +
|
[Tooltip("Tower CATALOG for this match, indexed by TowerTypeId. This is the full " +
|
||||||
"Populate this with every TowerDefinition asset the current race roster " +
|
"pool of towers that can exist (be drafted) this match — NOT what any one " +
|
||||||
"contains. Index 0 is reserved; valid IDs start at 1. " +
|
"player can build. Per-player buildable sets live in PlayerTowerDeck. " +
|
||||||
"(Temporary: will be driven by RaceDefinition once Path E is complete.)")]
|
"Index 0 is reserved; valid IDs start at 1.")]
|
||||||
[SerializeField] private TowerDefinition[] towerDefinitions = new TowerDefinition[0];
|
[SerializeField] private TowerDefinition[] towerDefinitions = new TowerDefinition[0];
|
||||||
|
|
||||||
|
[Tooltip("The starter towers every player begins the match with. Each must also " +
|
||||||
|
"appear in the catalog above (that's where its TowerTypeId comes from). " +
|
||||||
|
"Resolved to TypeIds and granted to each PlayerTowerDeck at match start.")]
|
||||||
|
[SerializeField] private TowerDefinition[] startingDeck = new TowerDefinition[0];
|
||||||
|
|
||||||
// ----- Internal request queue -------------------------------------
|
// ----- Internal request queue -------------------------------------
|
||||||
|
|
||||||
private struct PlacementRequest
|
private struct PlacementRequest
|
||||||
|
|
@ -188,6 +193,16 @@ namespace TD.Gameplay
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deck membership — the requesting player must have unlocked this tower.
|
||||||
|
// Authoritative gate: a modded client cannot place a tower it hasn't drafted,
|
||||||
|
// even though every tower exists in the shared catalog.
|
||||||
|
var deck = PlayerTowerDeck.GetForClient(req.SenderClientId);
|
||||||
|
if (deck == null || !deck.Contains(req.TowerTypeId))
|
||||||
|
{
|
||||||
|
Reject(req, PlacementRejectionReason.TowerNotInDeck);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var loader = LevelLoader.Instance;
|
var loader = LevelLoader.Instance;
|
||||||
if (loader == null || !loader.IsLoaded)
|
if (loader == null || !loader.IsLoaded)
|
||||||
{
|
{
|
||||||
|
|
@ -652,6 +667,46 @@ namespace TD.Gameplay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reverse lookup: returns the TowerTypeId (catalog index) for a given
|
||||||
|
/// definition asset, or false if it isn't in the catalog. Used to resolve the
|
||||||
|
/// <see cref="startingDeck"/> assets into the TypeIds a <see cref="PlayerTowerDeck"/>
|
||||||
|
/// stores.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryGetTypeId(TowerDefinition def, out int typeId)
|
||||||
|
{
|
||||||
|
typeId = 0;
|
||||||
|
if (def == null) return false;
|
||||||
|
for (int i = 1; i < towerDefinitions.Length; i++)
|
||||||
|
{
|
||||||
|
if (towerDefinitions[i] == def) { typeId = i; return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the configured <see cref="startingDeck"/> assets to their catalog
|
||||||
|
/// TypeIds. Logs an error for any starter tower missing from the catalog (it
|
||||||
|
/// would have no TypeId and couldn't be placed). Called once at match start.
|
||||||
|
/// </summary>
|
||||||
|
public List<int> GetStartingDeckTypeIds()
|
||||||
|
{
|
||||||
|
var ids = new List<int>();
|
||||||
|
if (startingDeck == null) return ids;
|
||||||
|
|
||||||
|
foreach (var def in startingDeck)
|
||||||
|
{
|
||||||
|
if (def == null) continue;
|
||||||
|
if (TryGetTypeId(def, out int id))
|
||||||
|
ids.Add(id);
|
||||||
|
else
|
||||||
|
Debug.LogError($"[TowerPlacementManager] Starting-deck tower '{def.name}' " +
|
||||||
|
$"is not in the towerDefinitions catalog. Add it to the catalog " +
|
||||||
|
$"so it has a TowerTypeId, or it can't be granted/placed.");
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
private static PlayerSlot ClientIdToPlayerSlot(ulong clientId)
|
private static PlayerSlot ClientIdToPlayerSlot(ulong clientId)
|
||||||
=> PlayerMatchState.SlotForClient(clientId);
|
=> PlayerMatchState.SlotForClient(clientId);
|
||||||
|
|
||||||
|
|
@ -707,6 +762,10 @@ namespace TD.Gameplay
|
||||||
/// <summary>The requested tower type ID is not in the server's definition list.</summary>
|
/// <summary>The requested tower type ID is not in the server's definition list.</summary>
|
||||||
InvalidTowerType,
|
InvalidTowerType,
|
||||||
|
|
||||||
|
/// <summary>The requested tower exists in the catalog but the placing player has
|
||||||
|
/// not unlocked it in their deck. Draft it first.</summary>
|
||||||
|
TowerNotInDeck,
|
||||||
|
|
||||||
/// <summary>An unexpected server-side error occurred (e.g., LevelLoader not loaded,
|
/// <summary>An unexpected server-side error occurred (e.g., LevelLoader not loaded,
|
||||||
/// client not mapped to a PlayerSlot). Check server logs.</summary>
|
/// client not mapped to a PlayerSlot). Check server logs.</summary>
|
||||||
ServerError,
|
ServerError,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,10 @@ namespace TD.Gameplay
|
||||||
"Player must cancel pending jobs or wait for one to complete.")]
|
"Player must cancel pending jobs or wait for one to complete.")]
|
||||||
public string MessageJobLimitReached = "Builder queue is full.";
|
public string MessageJobLimitReached = "Builder queue is full.";
|
||||||
|
|
||||||
|
[Tooltip("Shown when the player tries to place a tower they haven't unlocked in " +
|
||||||
|
"their deck yet. Draft it first.")]
|
||||||
|
public string MessageTowerNotInDeck = "You haven't unlocked that tower yet.";
|
||||||
|
|
||||||
[Tooltip("Shown for unexpected server-side errors (invalid tower type, etc.). " +
|
[Tooltip("Shown for unexpected server-side errors (invalid tower type, etc.). " +
|
||||||
"Should rarely appear in normal play.")]
|
"Should rarely appear in normal play.")]
|
||||||
public string MessageServerError = "Placement failed. Please try again.";
|
public string MessageServerError = "Placement failed. Please try again.";
|
||||||
|
|
@ -92,6 +96,7 @@ namespace TD.Gameplay
|
||||||
case PlacementRejectionReason.OutOfRange: return MessageOutOfRange;
|
case PlacementRejectionReason.OutOfRange: return MessageOutOfRange;
|
||||||
case PlacementRejectionReason.BlocksPath: return MessageBlocksPath;
|
case PlacementRejectionReason.BlocksPath: return MessageBlocksPath;
|
||||||
case PlacementRejectionReason.JobLimitReached: return MessageJobLimitReached;
|
case PlacementRejectionReason.JobLimitReached: return MessageJobLimitReached;
|
||||||
|
case PlacementRejectionReason.TowerNotInDeck: return MessageTowerNotInDeck;
|
||||||
case PlacementRejectionReason.InvalidTowerType:
|
case PlacementRejectionReason.InvalidTowerType:
|
||||||
case PlacementRejectionReason.ServerError:
|
case PlacementRejectionReason.ServerError:
|
||||||
default: return MessageServerError;
|
default: return MessageServerError;
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,26 @@ namespace TD.Gameplay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed each player's tower deck with the starter set. Runs here (after the
|
||||||
|
// one-frame yield) so the TowerPlacementManager catalog is guaranteed spawned
|
||||||
|
// and its starting-deck assets resolve to valid TypeIds. Clearing-and-setting
|
||||||
|
// means Retry / return-to-lobby cycles reset the deck cleanly.
|
||||||
|
var placement = TowerPlacementManager.Instance;
|
||||||
|
if (placement != null)
|
||||||
|
{
|
||||||
|
var startingTypeIds = placement.GetStartingDeckTypeIds();
|
||||||
|
foreach (var pms in PlayerMatchState.AllPlayers)
|
||||||
|
{
|
||||||
|
var deck = PlayerTowerDeck.GetForClient(pms.OwnerClientId);
|
||||||
|
if (deck != null) deck.ServerInitialize(startingTypeIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[WaveManager] TowerPlacementManager not found at match start. " +
|
||||||
|
"Player decks were not seeded.");
|
||||||
|
}
|
||||||
|
|
||||||
if (ms.Phase == MatchPhase.Playing)
|
if (ms.Phase == MatchPhase.Playing)
|
||||||
StartNextWave();
|
StartNextWave();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,8 @@ namespace TD.UI
|
||||||
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 bool deckSubscribed; // true once we've hooked the local PlayerTowerDeck.OnDeckChanged
|
||||||
|
private PlayerTowerDeck subscribedDeck; // the deck we hooked, so we can unsubscribe the same instance
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -245,6 +247,26 @@ namespace TD.UI
|
||||||
matchStateSubscribed = true;
|
matchStateSubscribed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hook the local player's deck so the command grid rebuilds live when a draft
|
||||||
|
// grant lands while the builder is already selected. The local deck may not
|
||||||
|
// exist for the first few frames (spawn race) — retried each Update until it does.
|
||||||
|
private void TrySubscribeDeck()
|
||||||
|
{
|
||||||
|
var deck = PlayerTowerDeck.Local;
|
||||||
|
if (deck == null) return;
|
||||||
|
deck.OnDeckChanged += HandleDeckChanged;
|
||||||
|
subscribedDeck = deck;
|
||||||
|
deckSubscribed = true;
|
||||||
|
// The deck may have populated before we subscribed — rebuild now so the
|
||||||
|
// grid reflects the current contents.
|
||||||
|
PopulateGridForSelection(SelectionState.Instance?.SelectedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDeckChanged()
|
||||||
|
{
|
||||||
|
PopulateGridForSelection(SelectionState.Instance?.SelectedObject);
|
||||||
|
}
|
||||||
|
|
||||||
private void InitializeUI()
|
private void InitializeUI()
|
||||||
{
|
{
|
||||||
var doc = GetComponent<UIDocument>();
|
var doc = GetComponent<UIDocument>();
|
||||||
|
|
@ -374,6 +396,13 @@ namespace TD.UI
|
||||||
MatchState.Instance.OnPhaseChanged -= HandlePhaseChanged;
|
MatchState.Instance.OnPhaseChanged -= HandlePhaseChanged;
|
||||||
matchStateSubscribed = false;
|
matchStateSubscribed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deckSubscribed && subscribedDeck != null)
|
||||||
|
{
|
||||||
|
subscribedDeck.OnDeckChanged -= HandleDeckChanged;
|
||||||
|
}
|
||||||
|
deckSubscribed = false;
|
||||||
|
subscribedDeck = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TrySubscribeSelection()
|
private void TrySubscribeSelection()
|
||||||
|
|
@ -394,6 +423,9 @@ namespace TD.UI
|
||||||
if (!matchStateSubscribed)
|
if (!matchStateSubscribed)
|
||||||
TrySubscribeMatchState();
|
TrySubscribeMatchState();
|
||||||
|
|
||||||
|
if (!deckSubscribed)
|
||||||
|
TrySubscribeDeck();
|
||||||
|
|
||||||
RefreshGoldDisplay();
|
RefreshGoldDisplay();
|
||||||
RefreshMatchStateDisplays();
|
RefreshMatchStateDisplays();
|
||||||
UpdateBuildProgressIfShown();
|
UpdateBuildProgressIfShown();
|
||||||
|
|
@ -763,14 +795,23 @@ namespace TD.UI
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
int i = 0;
|
// Build buttons come from the LOCAL player's deck — the per-player
|
||||||
foreach (var (def, typeId) in placementManager.GetAvailableDefinitions())
|
// unlocked set — not the global catalog. Each deck entry is a
|
||||||
|
// TowerTypeId resolved back to its definition via the catalog. The
|
||||||
|
// last grid slot is reserved for the Buffs button, so towers fill
|
||||||
|
// slots 0..GRID_MAX-2.
|
||||||
|
var deck = PlayerTowerDeck.Local;
|
||||||
|
if (deck != null)
|
||||||
{
|
{
|
||||||
if (i >= GRID_MAX) break;
|
int count = Mathf.Min(deck.Count, GRID_MAX - 1);
|
||||||
cells[i] = CreateTowerButton(def, typeId, HotkeyLayout[i]);
|
for (int i = 0; i < count; i++)
|
||||||
i++;
|
{
|
||||||
|
int typeId = deck.GetTypeIdAt(i);
|
||||||
|
var def = TowerPlacementManager.GetDefinition(typeId);
|
||||||
|
if (def != null)
|
||||||
|
cells[i] = CreateTowerButton(def, typeId, HotkeyLayout[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
cells[GRID_MAX - 1] = CreateBuffMenuButton(HotkeyLayout[GRID_MAX - 1]);
|
cells[GRID_MAX - 1] = CreateBuffMenuButton(HotkeyLayout[GRID_MAX - 1]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue