Adding the bones of the drafting system by removing the wall and siege towers from the base set and adding them temporarily as choices for the draft. Also adding Project_Context and Project_Roadmap documents to use with Claude
This commit is contained in:
parent
1d03689dad
commit
3ceebed8f6
22 changed files with 1060 additions and 431 deletions
8
Assets/_Project/Definitions/Draft.meta
Normal file
8
Assets/_Project/Definitions/Draft.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b2c3d4e5f6071829304a5b6c7d8e9f01
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
%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: 8c2d6e4f1a9b4c73d05e8f1a2b6c9d40, type: 3}
|
||||
m_Name: Draft_NewTower_SiegeCannon
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Draft.NewTowerDraftOption
|
||||
DisplayName: Siege Cannon
|
||||
Description: Add the Siege Cannon to your arsenal. Ground-only; lobs a shell that
|
||||
deals splash damage on impact.
|
||||
Icon: {fileID: 0}
|
||||
Weight: 1
|
||||
Tower: {fileID: 11400000, guid: c4a1f7e92b6d8a4e3f0c9b1d6e5a2f80, type: 2}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6b1d3f8a2c5e4097b8a1d0c3e6f9b240
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
19
Assets/_Project/Definitions/Draft/Draft_NewTower_Wall.asset
Normal file
19
Assets/_Project/Definitions/Draft/Draft_NewTower_Wall.asset
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 8c2d6e4f1a9b4c73d05e8f1a2b6c9d40, type: 3}
|
||||
m_Name: Draft_NewTower_Wall
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Draft.NewTowerDraftOption
|
||||
DisplayName: Wall
|
||||
Description: Add the Wall to your arsenal. No attack, near-free; shapes your maze.
|
||||
Icon: {fileID: 0}
|
||||
Weight: 1
|
||||
Tower: {fileID: 11400000, guid: d5b2e8fa3c7e9b5f4a1d0c2e7f6b3a91, type: 2}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7c2e4a9b3d6f51a80c9b2e1d4f7a3b51
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -15,6 +15,7 @@ GameObject:
|
|||
- component: {fileID: 4336209376377567030}
|
||||
- component: {fileID: 2806524246861401760}
|
||||
- component: {fileID: 2806524246861401799}
|
||||
- component: {fileID: 2806524246861401801}
|
||||
m_Layer: 0
|
||||
m_Name: Player
|
||||
m_TagString: Untagged
|
||||
|
|
@ -131,3 +132,16 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerTowerDeck
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
--- !u!114 &2806524246861401801
|
||||
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: 2b8f4d1e6c3a497051d8e2f7a9c1b063, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Draft.PlayerDraft
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
|
|
|
|||
|
|
@ -19775,9 +19775,7 @@ MonoBehaviour:
|
|||
- {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
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -28368,6 +28366,68 @@ Mesh:
|
|||
- serializedVersion: 1
|
||||
m_IndexStart: 0
|
||||
m_IndexCount: 0
|
||||
--- !u!1 &2139601598
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2139601601}
|
||||
- component: {fileID: 2139601600}
|
||||
- component: {fileID: 2139601599}
|
||||
m_Layer: 0
|
||||
m_Name: DraftController
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &2139601599
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2139601598}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9d3a7c5e1b8f42606a2d4e9f1c7b3850, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Draft.DraftService
|
||||
optionsPerDraft: 3
|
||||
rerollCost: 50
|
||||
--- !u!114 &2139601600
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2139601598}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5a7e1f3c9d2b4867a1c0e4f8b3d6792a, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Draft.DraftPool
|
||||
options:
|
||||
- {fileID: 11400000, guid: 6b1d3f8a2c5e4097b8a1d0c3e6f9b240, type: 2}
|
||||
- {fileID: 11400000, guid: 7c2e4a9b3d6f51a80c9b2e1d4f7a3b51, type: 2}
|
||||
--- !u!4 &2139601601
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2139601598}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 76.27885, y: 0.50001, z: 118.9994}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &2142639533
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -28748,3 +28808,4 @@ SceneRoots:
|
|||
- {fileID: 1430789337}
|
||||
- {fileID: 1097152085}
|
||||
- {fileID: 1889390964}
|
||||
- {fileID: 2139601601}
|
||||
|
|
|
|||
8
Assets/_Project/Scripts/Gameplay/Draft.meta
Normal file
8
Assets/_Project/Scripts/Gameplay/Draft.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a1b2c3d4e5f60718293a4b5c6d7e8f90
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
59
Assets/_Project/Scripts/Gameplay/Draft/DraftOption.cs
Normal file
59
Assets/_Project/Scripts/Gameplay/Draft/DraftOption.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Assets/_Project/Scripts/Gameplay/Draft/DraftOption.cs
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Gameplay.Draft
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for one authored draft choice. The draft system offers a player a
|
||||
/// weighted-random selection of these between waves; the player picks one and the
|
||||
/// server applies its effect.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>One asset per choice.</b> Concrete subclasses (e.g.
|
||||
/// <see cref="NewTowerDraftOption"/>) carry the payload and implement
|
||||
/// <see cref="ServerApply"/>. New choice types (systemic upgrade, builder ability,
|
||||
/// relic quest, enemy debuff) are added as new subclasses — the draft spine
|
||||
/// (<see cref="DraftService"/>, <see cref="PlayerDraft"/>, HUD) is type-agnostic.</para>
|
||||
///
|
||||
/// <para><b>Identity.</b> Options live in <see cref="DraftPool"/> and are referenced
|
||||
/// over the network by their pool index (<c>DraftOptionId</c>) — the same
|
||||
/// stable-within-a-match index pattern used by the tower catalog.</para>
|
||||
///
|
||||
/// <para><b>Server-only logic.</b> <see cref="IsValidFor"/> and
|
||||
/// <see cref="ServerApply"/> run on the server and operate on the target player's
|
||||
/// per-player components (deck, gold, etc.). The ScriptableObject holds no per-player
|
||||
/// state, so a single asset is shared across all players and matches.</para>
|
||||
/// </remarks>
|
||||
public abstract class DraftOption : ScriptableObject
|
||||
{
|
||||
[Header("Presentation")]
|
||||
[Tooltip("Name shown on the draft card.")]
|
||||
public string DisplayName;
|
||||
|
||||
[Tooltip("Short description shown on the draft card.")]
|
||||
[TextArea(2, 4)]
|
||||
public string Description;
|
||||
|
||||
[Tooltip("Icon shown on the draft card. Optional for placeholder content.")]
|
||||
public Sprite Icon;
|
||||
|
||||
[Header("Generation")]
|
||||
[Tooltip("Relative draw weight. Higher = offered more often. Rarer rewards use " +
|
||||
"lower weights. Must be > 0.")]
|
||||
[Min(0.0001f)]
|
||||
public float Weight = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: can this option validly be OFFERED to the given player right now?
|
||||
/// Lets the generator skip options that would be a no-op (e.g. a tower the player
|
||||
/// already owns). Default true; override when an option has prerequisites.
|
||||
/// </summary>
|
||||
public virtual bool IsValidFor(ulong clientId) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: apply this option's effect to the given player. Returns false if
|
||||
/// it could not be applied (caller treats as a no-op and logs).
|
||||
/// </summary>
|
||||
public abstract bool ServerApply(ulong clientId);
|
||||
}
|
||||
}
|
||||
11
Assets/_Project/Scripts/Gameplay/Draft/DraftOption.cs.meta
Normal file
11
Assets/_Project/Scripts/Gameplay/Draft/DraftOption.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3f9b1c7a2e8d4a16b0c5e9f2a7d34b81
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
64
Assets/_Project/Scripts/Gameplay/Draft/DraftPool.cs
Normal file
64
Assets/_Project/Scripts/Gameplay/Draft/DraftPool.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// Assets/_Project/Scripts/Gameplay/Draft/DraftPool.cs
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Gameplay.Draft
|
||||
{
|
||||
/// <summary>
|
||||
/// Scene singleton holding every <see cref="DraftOption"/> available this match. The
|
||||
/// array index is the option's <c>DraftOptionId</c> — the stable identifier used to
|
||||
/// replicate offers and picks over the network (mirrors the tower catalog pattern).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Plain MonoBehaviour: identical on every peer (same assets), so there is nothing to
|
||||
/// sync. The server reads it to generate offers and apply picks; clients read it to
|
||||
/// render draft cards from the replicated option ids.
|
||||
/// </remarks>
|
||||
public class DraftPool : MonoBehaviour
|
||||
{
|
||||
public static DraftPool Instance { get; private set; }
|
||||
|
||||
[Tooltip("Every DraftOption asset that can be offered this match. Array index is " +
|
||||
"the DraftOptionId used over the network.")]
|
||||
[SerializeField] private DraftOption[] options;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogError("[DraftPool] Multiple instances detected. Only one per scene.");
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
/// <summary>Number of options in the pool.</summary>
|
||||
public int Count => options?.Length ?? 0;
|
||||
|
||||
/// <summary>Returns the option at <paramref name="id"/> (its DraftOptionId), or null.</summary>
|
||||
public DraftOption Get(int id)
|
||||
=> (options != null && id >= 0 && id < options.Length) ? options[id] : null;
|
||||
|
||||
/// <summary>
|
||||
/// Server-only helper: collects the ids of every option currently valid to offer
|
||||
/// to <paramref name="clientId"/> (skips nulls and options whose
|
||||
/// <see cref="DraftOption.IsValidFor"/> returns false). Used by
|
||||
/// <see cref="DraftService"/> as the candidate set for the weighted draw.
|
||||
/// </summary>
|
||||
public void CollectValidIds(ulong clientId, List<int> into)
|
||||
{
|
||||
into.Clear();
|
||||
if (options == null) return;
|
||||
for (int i = 0; i < options.Length; i++)
|
||||
{
|
||||
if (options[i] == null) continue;
|
||||
if (options[i].IsValidFor(clientId)) into.Add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Project/Scripts/Gameplay/Draft/DraftPool.cs.meta
Normal file
11
Assets/_Project/Scripts/Gameplay/Draft/DraftPool.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5a7e1f3c9d2b4867a1c0e4f8b3d6792a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
132
Assets/_Project/Scripts/Gameplay/Draft/DraftService.cs
Normal file
132
Assets/_Project/Scripts/Gameplay/Draft/DraftService.cs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
// Assets/_Project/Scripts/Gameplay/Draft/DraftService.cs
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Gameplay.Draft
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative draft orchestrator: generates weighted-random option sets and
|
||||
/// offers them to players' <see cref="PlayerDraft"/> components. Scene singleton.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Plain MonoBehaviour — the replicated state lives on <see cref="PlayerDraft"/>; this
|
||||
/// is pure server logic plus two designer tunables (<see cref="optionsPerDraft"/>,
|
||||
/// <see cref="rerollCost"/>). It exists on every peer (scene object) but only the server
|
||||
/// drives it; clients read <see cref="RerollCost"/> for the HUD's "buy roll" button.
|
||||
///
|
||||
/// <para><b>Driven by <see cref="WaveManager"/>:</b> <see cref="ServerOfferToAll"/> at
|
||||
/// the start of each prep phase; <see cref="ServerAutoResolveAll"/> at prep end (and on
|
||||
/// dev force-advance) so unpicked free drafts aren't wasted.</para>
|
||||
/// </remarks>
|
||||
public class DraftService : MonoBehaviour
|
||||
{
|
||||
public static DraftService Instance { get; private set; }
|
||||
|
||||
[Tooltip("How many options each draft offers (cards shown). Standard roguelike draw is 3.")]
|
||||
[SerializeField] private int optionsPerDraft = 3;
|
||||
|
||||
[Tooltip("Gold cost for a player to buy one extra draft roll from the shop.")]
|
||||
[SerializeField] private int rerollCost = 50;
|
||||
|
||||
/// <summary>Gold cost of one bought reroll. Read by the HUD and the buy RPC.</summary>
|
||||
public int RerollCost => rerollCost;
|
||||
|
||||
// Scratch lists reused across generation to avoid per-draft GC. Server is
|
||||
// single-threaded, so sharing them across the per-player loop is safe.
|
||||
private readonly List<int> candidateScratch = new List<int>();
|
||||
private readonly List<int> resultScratch = new List<int>();
|
||||
|
||||
private static bool IsServer
|
||||
=> NetworkManager.Singleton != null && NetworkManager.Singleton.IsServer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogError("[DraftService] Multiple instances detected. Only one per scene.");
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
// ----- Server orchestration ---------------------------------------
|
||||
|
||||
/// <summary>Server-only: offer a fresh draft to every connected player.</summary>
|
||||
public void ServerOfferToAll()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (DraftPool.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[DraftService] No DraftPool in scene — no drafts will be offered.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var pms in PlayerMatchState.AllPlayers)
|
||||
ServerOfferTo(pms.OwnerClientId);
|
||||
}
|
||||
|
||||
/// <summary>Server-only: offer a fresh draft to one player (also used by the paid reroll).</summary>
|
||||
public void ServerOfferTo(ulong clientId)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
var draft = PlayerDraft.GetForClient(clientId);
|
||||
if (draft == null) return;
|
||||
|
||||
GenerateOptionIds(clientId, optionsPerDraft, resultScratch);
|
||||
draft.ServerOffer(resultScratch);
|
||||
}
|
||||
|
||||
/// <summary>Server-only: auto-resolve any unpicked drafts (called at prep end).</summary>
|
||||
public void ServerAutoResolveAll()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
foreach (var pms in PlayerMatchState.AllPlayers)
|
||||
PlayerDraft.GetForClient(pms.OwnerClientId)?.ServerAutoResolve();
|
||||
}
|
||||
|
||||
// ----- Generation -------------------------------------------------
|
||||
|
||||
// Weighted random draw WITHOUT replacement: picks `count` distinct option ids from
|
||||
// the set currently valid for this player. If fewer valid options exist than
|
||||
// requested, offers what's available (possibly zero).
|
||||
private void GenerateOptionIds(ulong clientId, int count, List<int> into)
|
||||
{
|
||||
into.Clear();
|
||||
var pool = DraftPool.Instance;
|
||||
if (pool == null) return;
|
||||
|
||||
pool.CollectValidIds(clientId, candidateScratch);
|
||||
|
||||
int draws = Mathf.Min(count, candidateScratch.Count);
|
||||
for (int n = 0; n < draws; n++)
|
||||
{
|
||||
float total = 0f;
|
||||
for (int i = 0; i < candidateScratch.Count; i++)
|
||||
total += OptionWeight(pool, candidateScratch[i]);
|
||||
|
||||
float r = Random.value * total;
|
||||
int chosen = candidateScratch.Count - 1; // fallback to last on float drift
|
||||
for (int i = 0; i < candidateScratch.Count; i++)
|
||||
{
|
||||
r -= OptionWeight(pool, candidateScratch[i]);
|
||||
if (r <= 0f) { chosen = i; break; }
|
||||
}
|
||||
|
||||
into.Add(candidateScratch[chosen]);
|
||||
candidateScratch.RemoveAt(chosen); // without replacement → distinct options
|
||||
}
|
||||
}
|
||||
|
||||
private static float OptionWeight(DraftPool pool, int id)
|
||||
{
|
||||
var opt = pool.Get(id);
|
||||
return opt != null ? Mathf.Max(0.0001f, opt.Weight) : 0.0001f;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Project/Scripts/Gameplay/Draft/DraftService.cs.meta
Normal file
11
Assets/_Project/Scripts/Gameplay/Draft/DraftService.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9d3a7c5e1b8f42606a2d4e9f1c7b3850
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// Assets/_Project/Scripts/Gameplay/Draft/NewTowerDraftOption.cs
|
||||
using UnityEngine;
|
||||
using TD.Towers;
|
||||
|
||||
namespace TD.Gameplay.Draft
|
||||
{
|
||||
/// <summary>
|
||||
/// Draft choice #1 — "add a new tower to your arsenal". Grants the player a tower,
|
||||
/// adding it to their <see cref="PlayerTowerDeck"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only offered when the tower is in the match catalog AND the player doesn't already
|
||||
/// own it, so the draft never wastes a slot on a no-op.
|
||||
/// </remarks>
|
||||
[CreateAssetMenu(fileName = "NewTowerOption", menuName = "TD/Draft/New Tower Option")]
|
||||
public class NewTowerDraftOption : DraftOption
|
||||
{
|
||||
[Header("Payload")]
|
||||
[Tooltip("The tower this option grants. Must also be present in the " +
|
||||
"TowerPlacementManager catalog (that's where its TowerTypeId comes from).")]
|
||||
public TowerDefinition Tower;
|
||||
|
||||
public override bool IsValidFor(ulong clientId)
|
||||
{
|
||||
var deck = PlayerTowerDeck.GetForClient(clientId);
|
||||
var pm = TowerPlacementManager.Instance;
|
||||
if (deck == null || pm == null || Tower == null) return false;
|
||||
|
||||
// Not in the catalog → no TypeId → can't be granted or placed.
|
||||
if (!pm.TryGetTypeId(Tower, out int typeId)) return false;
|
||||
|
||||
// Only offer towers the player hasn't unlocked yet.
|
||||
return !deck.Contains(typeId);
|
||||
}
|
||||
|
||||
public override bool ServerApply(ulong clientId)
|
||||
{
|
||||
var deck = PlayerTowerDeck.GetForClient(clientId);
|
||||
var pm = TowerPlacementManager.Instance;
|
||||
if (deck == null || pm == null || Tower == null) return false;
|
||||
|
||||
if (!pm.TryGetTypeId(Tower, out int typeId))
|
||||
{
|
||||
Debug.LogError($"[NewTowerDraftOption] '{Tower.name}' is not in the tower catalog; " +
|
||||
$"cannot grant. Add it to TowerPlacementManager.towerDefinitions.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return deck.ServerGrantTower(typeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8c2d6e4f1a9b4c73d05e8f1a2b6c9d40
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
167
Assets/_Project/Scripts/Gameplay/Draft/PlayerDraft.cs
Normal file
167
Assets/_Project/Scripts/Gameplay/Draft/PlayerDraft.cs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// Assets/_Project/Scripts/Gameplay/Draft/PlayerDraft.cs
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Gameplay.Draft
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-player draft state — the set of <see cref="DraftOption"/>s currently offered to
|
||||
/// one player. Lives on the Player prefab alongside <see cref="PlayerTowerDeck"/>,
|
||||
/// <see cref="PlayerGoldManager"/>, etc.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Offered set.</b> <see cref="currentOptionIds"/> holds the DraftOptionIds
|
||||
/// the player is choosing between (typically 3). Empty means no active draft. The
|
||||
/// owning client renders cards from these ids; the server reads them to validate a pick.</para>
|
||||
///
|
||||
/// <para><b>Authority.</b> The server offers (<see cref="ServerOffer"/>), resolves
|
||||
/// (<see cref="ServerResolve"/> / <see cref="ServerAutoResolve"/>), and applies the
|
||||
/// chosen option. The owning client only sends intent via
|
||||
/// <see cref="RequestPickRpc"/> and <see cref="RequestBuyRerollRpc"/>.</para>
|
||||
///
|
||||
/// <para><b>Generation</b> lives in <see cref="DraftService"/>; this component is just
|
||||
/// the replicated state + the pick/buy entry points.</para>
|
||||
/// </remarks>
|
||||
public class PlayerDraft : NetworkBehaviour
|
||||
{
|
||||
// ----- Static registry (mirrors PlayerGoldManager / PlayerTowerDeck) -----
|
||||
|
||||
private static readonly Dictionary<ulong, PlayerDraft> s_byClientId
|
||||
= new Dictionary<ulong, PlayerDraft>();
|
||||
|
||||
public static PlayerDraft GetForClient(ulong clientId)
|
||||
{
|
||||
s_byClientId.TryGetValue(clientId, out var draft);
|
||||
return draft;
|
||||
}
|
||||
|
||||
public static PlayerDraft Local
|
||||
{
|
||||
get
|
||||
{
|
||||
var nm = NetworkManager.Singleton;
|
||||
if (nm == null || !nm.IsClient) return null;
|
||||
return GetForClient(nm.LocalClientId);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Networked state --------------------------------------------
|
||||
|
||||
// The DraftOptionIds currently offered to this player. Empty = no active draft.
|
||||
private NetworkList<int> currentOptionIds;
|
||||
|
||||
/// <summary>Fired on every peer when the offered set changes (offered, picked,
|
||||
/// cleared). The HUD subscribes to show/hide/rebuild the draft overlay.</summary>
|
||||
public event System.Action OnDraftChanged;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
currentOptionIds = new NetworkList<int>();
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
s_byClientId[OwnerClientId] = this;
|
||||
currentOptionIds.OnListChanged += HandleListChanged;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
currentOptionIds.OnListChanged -= HandleListChanged;
|
||||
if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
|
||||
s_byClientId.Remove(OwnerClientId);
|
||||
}
|
||||
|
||||
private void HandleListChanged(NetworkListEvent<int> _) => OnDraftChanged?.Invoke();
|
||||
|
||||
// ----- Read API ---------------------------------------------------
|
||||
|
||||
/// <summary>True if the player currently has options to choose from.</summary>
|
||||
public bool HasActiveDraft => currentOptionIds.Count > 0;
|
||||
|
||||
public int OptionCount => currentOptionIds.Count;
|
||||
|
||||
public int GetOptionId(int index) => currentOptionIds[index];
|
||||
|
||||
private bool IsOffered(int optionId)
|
||||
{
|
||||
for (int i = 0; i < currentOptionIds.Count; i++)
|
||||
if (currentOptionIds[i] == optionId) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----- Server: offer / resolve ------------------------------------
|
||||
|
||||
/// <summary>Server-only: replace the offered set with <paramref name="ids"/>.</summary>
|
||||
public void ServerOffer(IReadOnlyList<int> ids)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
currentOptionIds.Clear();
|
||||
if (ids == null) return;
|
||||
for (int i = 0; i < ids.Count; i++)
|
||||
currentOptionIds.Add(ids[i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: apply one of the currently-offered options by id, then clear the
|
||||
/// draft. No-op (returns false) if the id wasn't offered or the option fails to apply.
|
||||
/// </summary>
|
||||
private bool ServerResolve(int optionId)
|
||||
{
|
||||
if (!IsServer) return false;
|
||||
if (!IsOffered(optionId)) return false;
|
||||
|
||||
var option = DraftPool.Instance != null ? DraftPool.Instance.Get(optionId) : null;
|
||||
bool applied = option != null && option.ServerApply(OwnerClientId);
|
||||
if (!applied)
|
||||
Debug.LogWarning($"[PlayerDraft] Option {optionId} could not be applied for " +
|
||||
$"client {OwnerClientId}.");
|
||||
|
||||
currentOptionIds.Clear();
|
||||
return applied;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only: if the player still has an unpicked draft, auto-resolve it by
|
||||
/// applying the first offered option. Called at prep end so a free reward is never
|
||||
/// wasted by an idle player.
|
||||
/// </summary>
|
||||
public void ServerAutoResolve()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (currentOptionIds.Count == 0) return;
|
||||
ServerResolve(currentOptionIds[0]);
|
||||
}
|
||||
|
||||
// ----- Owner → server RPCs ----------------------------------------
|
||||
|
||||
/// <summary>Owning client: pick one of the offered options by id.</summary>
|
||||
[Rpc(SendTo.Server, RequireOwnership = true)]
|
||||
public void RequestPickRpc(int optionId)
|
||||
{
|
||||
ServerResolve(optionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owning client: buy a fresh roll of options for gold. Rejected if the player can't
|
||||
/// afford it or already has an unresolved draft (resolve the current one first so a
|
||||
/// free pick is never silently overwritten).
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server, RequireOwnership = true)]
|
||||
public void RequestBuyRerollRpc()
|
||||
{
|
||||
if (HasActiveDraft) return; // resolve the pending draft before buying another
|
||||
|
||||
var service = DraftService.Instance;
|
||||
if (service == null) return;
|
||||
|
||||
var gold = PlayerGoldManager.GetForClient(OwnerClientId);
|
||||
int cost = service.RerollCost;
|
||||
if (gold == null || gold.CurrentGold < cost) return;
|
||||
|
||||
gold.DeductGold(cost);
|
||||
service.ServerOfferTo(OwnerClientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Project/Scripts/Gameplay/Draft/PlayerDraft.cs.meta
Normal file
11
Assets/_Project/Scripts/Gameplay/Draft/PlayerDraft.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2b8f4d1e6c3a497051d8e2f7a9c1b063
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -3,6 +3,7 @@ using System.Collections;
|
|||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
using TD.Gameplay.Draft;
|
||||
using TD.Levels;
|
||||
using TD.UI;
|
||||
|
||||
|
|
@ -318,6 +319,11 @@ namespace TD.Gameplay
|
|||
activeEnemyCount = 0;
|
||||
spawningComplete = false;
|
||||
heldEnemies.Clear();
|
||||
|
||||
// Force-advancing interrupts the prep phase, so resolve any drafts the players
|
||||
// had open before skipping ahead.
|
||||
DraftService.Instance?.ServerAutoResolveAll();
|
||||
|
||||
StartNextWave(skipPrep: true);
|
||||
}
|
||||
|
||||
|
|
@ -330,6 +336,10 @@ namespace TD.Gameplay
|
|||
// observe the value via NetworkVariable replication.
|
||||
if (!skipPrep)
|
||||
{
|
||||
// Open a draft for every player at the start of the build phase. They pick
|
||||
// during prep; any unpicked draft is auto-resolved when the timer expires.
|
||||
DraftService.Instance?.ServerOfferToAll();
|
||||
|
||||
prepCountdown.Value = def.PrepTime;
|
||||
float remaining = def.PrepTime;
|
||||
// Throttle network sync to ~10 Hz. NetworkVariable replicates on every
|
||||
|
|
@ -348,6 +358,10 @@ namespace TD.Gameplay
|
|||
nextSync = remaining - NetworkSyncInterval;
|
||||
}
|
||||
}
|
||||
|
||||
// Prep timer expired — auto-resolve any draft the player didn't pick so
|
||||
// the free reward isn't wasted.
|
||||
DraftService.Instance?.ServerAutoResolveAll();
|
||||
}
|
||||
// Ensure the countdown reads zero entering the spawn phase, regardless of
|
||||
// whether prep was skipped or just expired.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using UnityEngine.SceneManagement;
|
|||
using UnityEngine.UIElements;
|
||||
using TD.Core;
|
||||
using TD.Gameplay;
|
||||
using TD.Gameplay.Draft;
|
||||
using TD.Towers;
|
||||
using TD.UI.Minimap;
|
||||
|
||||
|
|
@ -105,6 +106,15 @@ namespace TD.UI
|
|||
private VisualElement buffMenuContent;
|
||||
private Label matchEndTitle;
|
||||
|
||||
// Draft overlay (roguelike between-waves draft). Non-modal: the card row floats at
|
||||
// top-center so the player can keep building during prep. The "Buy Roll" button is
|
||||
// shown when no draft is pending so a paid roll can be bought any time.
|
||||
private VisualElement draftPanel;
|
||||
private VisualElement draftCardRow;
|
||||
private Button draftBuyButton;
|
||||
private bool draftSubscribed;
|
||||
private PlayerDraft subscribedDraft;
|
||||
|
||||
// Chat panel (bottom-left, above portrait) — programmatic. The container
|
||||
// holds both the scrollable feed and the input. Highlight + scroll
|
||||
// interactivity are toggled on the container when typing.
|
||||
|
|
@ -267,6 +277,163 @@ namespace TD.UI
|
|||
PopulateGridForSelection(SelectionState.Instance?.SelectedObject);
|
||||
}
|
||||
|
||||
// Hook the local player's draft so the overlay rebuilds when options are offered,
|
||||
// picked, or rerolled. Retried each Update until the local PlayerDraft exists.
|
||||
private void TrySubscribeDraft()
|
||||
{
|
||||
var draft = PlayerDraft.Local;
|
||||
if (draft == null) return;
|
||||
draft.OnDraftChanged += HandleDraftChanged;
|
||||
subscribedDraft = draft;
|
||||
draftSubscribed = true;
|
||||
RebuildDraftCards();
|
||||
}
|
||||
|
||||
private void HandleDraftChanged()
|
||||
{
|
||||
RebuildDraftCards();
|
||||
}
|
||||
|
||||
// ----- Draft overlay ----------------------------------------------
|
||||
|
||||
// Non-modal draft UI: a card row floats at top-center while a draft is active, and
|
||||
// a "Buy Roll" button shows when no draft is pending. The host panel ignores picks
|
||||
// so empty space clicks through to the world — the player keeps building during prep.
|
||||
private void BuildDraftOverlay(VisualElement root)
|
||||
{
|
||||
draftPanel = new VisualElement();
|
||||
draftPanel.style.position = Position.Absolute;
|
||||
draftPanel.style.left = 0;
|
||||
draftPanel.style.right = 0;
|
||||
draftPanel.style.top = 70; // below the top bar
|
||||
draftPanel.style.alignItems = Align.Center;
|
||||
draftPanel.pickingMode = PickingMode.Ignore;
|
||||
draftPanel.style.display = DisplayStyle.None;
|
||||
|
||||
var box = new VisualElement();
|
||||
box.style.flexDirection = FlexDirection.Column;
|
||||
box.style.alignItems = Align.Center;
|
||||
box.pickingMode = PickingMode.Ignore;
|
||||
|
||||
draftCardRow = new VisualElement();
|
||||
draftCardRow.style.flexDirection = FlexDirection.Row;
|
||||
draftCardRow.pickingMode = PickingMode.Ignore;
|
||||
box.Add(draftCardRow);
|
||||
|
||||
draftBuyButton = new Button(() => PlayerDraft.Local?.RequestBuyRerollRpc())
|
||||
{
|
||||
text = "Buy Roll"
|
||||
};
|
||||
draftBuyButton.style.marginTop = 6;
|
||||
box.Add(draftBuyButton);
|
||||
|
||||
draftPanel.Add(box);
|
||||
root.Add(draftPanel);
|
||||
|
||||
UpdateDraftVisibility();
|
||||
}
|
||||
|
||||
// Rebuilds the option cards from the local player's current offer. Called on
|
||||
// OnDraftChanged (offered / picked / rerolled).
|
||||
private void RebuildDraftCards()
|
||||
{
|
||||
if (draftCardRow == null) return;
|
||||
draftCardRow.Clear();
|
||||
|
||||
var draft = PlayerDraft.Local;
|
||||
var pool = DraftPool.Instance;
|
||||
if (draft != null && pool != null)
|
||||
{
|
||||
for (int i = 0; i < draft.OptionCount; i++)
|
||||
{
|
||||
int id = draft.GetOptionId(i);
|
||||
var option = pool.Get(id);
|
||||
if (option == null) continue;
|
||||
draftCardRow.Add(CreateDraftCard(option, id, draft));
|
||||
}
|
||||
}
|
||||
|
||||
UpdateDraftVisibility();
|
||||
}
|
||||
|
||||
private VisualElement CreateDraftCard(DraftOption option, int optionId, PlayerDraft draft)
|
||||
{
|
||||
var card = new VisualElement();
|
||||
card.style.width = 150;
|
||||
card.style.marginLeft = card.style.marginRight = 4;
|
||||
card.style.paddingTop = card.style.paddingBottom = 10;
|
||||
card.style.paddingLeft = card.style.paddingRight = 10;
|
||||
card.style.backgroundColor = new Color(0.10f, 0.10f, 0.13f, 0.96f);
|
||||
card.style.borderTopWidth = card.style.borderBottomWidth =
|
||||
card.style.borderLeftWidth = card.style.borderRightWidth = 2;
|
||||
var border = new Color(0.4f, 0.4f, 0.45f);
|
||||
card.style.borderTopColor = card.style.borderBottomColor =
|
||||
card.style.borderLeftColor = card.style.borderRightColor = border;
|
||||
card.style.alignItems = Align.Center;
|
||||
|
||||
var title = new Label(option.DisplayName);
|
||||
title.style.fontSize = 14;
|
||||
title.style.color = Color.white;
|
||||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
title.style.whiteSpace = WhiteSpace.Normal;
|
||||
title.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
title.style.marginBottom = 6;
|
||||
card.Add(title);
|
||||
|
||||
if (option.Icon != null)
|
||||
{
|
||||
var img = new Image { sprite = option.Icon };
|
||||
img.style.width = 48;
|
||||
img.style.height = 48;
|
||||
img.style.marginBottom = 6;
|
||||
card.Add(img);
|
||||
}
|
||||
|
||||
var desc = new Label(option.Description ?? "");
|
||||
desc.style.fontSize = 11;
|
||||
desc.style.color = new Color(0.82f, 0.82f, 0.82f);
|
||||
desc.style.whiteSpace = WhiteSpace.Normal;
|
||||
desc.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
desc.style.marginBottom = 8;
|
||||
card.Add(desc);
|
||||
|
||||
var pick = new Button(() => draft.RequestPickRpc(optionId)) { text = "Pick" };
|
||||
card.Add(pick);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// Per-frame: toggles the card row vs. the buy button and keeps the buy button's
|
||||
// label/enabled state in sync with gold. Cheap (mirrors the gold label's per-frame
|
||||
// refresh); only allocates a string while the buy button is shown.
|
||||
private void UpdateDraftVisibility()
|
||||
{
|
||||
if (draftPanel == null) return;
|
||||
|
||||
var draft = PlayerDraft.Local;
|
||||
var service = DraftService.Instance;
|
||||
var gold = PlayerGoldManager.Local;
|
||||
|
||||
bool hasDraft = draft != null && draft.HasActiveDraft;
|
||||
bool canShowBuy = draft != null && service != null && !hasDraft;
|
||||
|
||||
if (draftCardRow != null)
|
||||
draftCardRow.style.display = hasDraft ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
if (draftBuyButton != null)
|
||||
{
|
||||
draftBuyButton.style.display = canShowBuy ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
if (canShowBuy)
|
||||
{
|
||||
int cost = service.RerollCost;
|
||||
draftBuyButton.text = $"Buy Roll ({cost}g)";
|
||||
draftBuyButton.SetEnabled(gold != null && gold.CurrentGold >= cost);
|
||||
}
|
||||
}
|
||||
|
||||
draftPanel.style.display = (hasDraft || canShowBuy) ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
private void InitializeUI()
|
||||
{
|
||||
var doc = GetComponent<UIDocument>();
|
||||
|
|
@ -351,6 +518,10 @@ namespace TD.UI
|
|||
// Build the buff menu overlay. Hidden until the player presses B.
|
||||
BuildBuffMenuOverlay(root);
|
||||
|
||||
// Build the draft overlay (roguelike between-waves draft). Hidden until the
|
||||
// local player has an active draft (or to show the paid "Buy Roll" button).
|
||||
BuildDraftOverlay(root);
|
||||
|
||||
// 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
|
||||
// ChatService.PostLocalSystem on every peer.
|
||||
|
|
@ -403,6 +574,13 @@ namespace TD.UI
|
|||
}
|
||||
deckSubscribed = false;
|
||||
subscribedDeck = null;
|
||||
|
||||
if (draftSubscribed && subscribedDraft != null)
|
||||
{
|
||||
subscribedDraft.OnDraftChanged -= HandleDraftChanged;
|
||||
}
|
||||
draftSubscribed = false;
|
||||
subscribedDraft = null;
|
||||
}
|
||||
|
||||
private void TrySubscribeSelection()
|
||||
|
|
@ -426,10 +604,14 @@ namespace TD.UI
|
|||
if (!deckSubscribed)
|
||||
TrySubscribeDeck();
|
||||
|
||||
if (!draftSubscribed)
|
||||
TrySubscribeDraft();
|
||||
|
||||
RefreshGoldDisplay();
|
||||
RefreshMatchStateDisplays();
|
||||
UpdateBuildProgressIfShown();
|
||||
UpdateEnemyInfoIfShown();
|
||||
UpdateDraftVisibility();
|
||||
HandleChatInput();
|
||||
|
||||
// Skip gameplay hotkeys while the chat input is focused — letters
|
||||
|
|
|
|||
97
Project_Context.md
Normal file
97
Project_Context.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Unity Tower Defense — Project Context
|
||||
|
||||
## Purpose
|
||||
|
||||
A snapshot of **where the project is and how it works** — the authoritative reference for current architecture, implemented systems, conventions, and known debt. It pairs with [`Project_Roadmap.md`](Project_Roadmap.md), which is the forward-looking plan. When the two disagree, this document describes *what exists today*; the roadmap describes *what's planned next*.
|
||||
|
||||
Last substantial update: 2026-06-23.
|
||||
|
||||
---
|
||||
|
||||
## Game overview
|
||||
|
||||
A **co-op tower-defense / roguelike hybrid** for up to 9 players.
|
||||
|
||||
- **Maze defense (Wintermaul-style):** players build towers to force enemies along a longer path through their own zone. Lives are a **shared pool**; gold is **per-player**.
|
||||
- **Roguelike layer (now core to the design):** every player starts with the **same three towers** and builds out a personal "deck"/"build" through a **3-option draft** presented at match start and after every wave. Choices span new towers, systemic upgrades, builder abilities, enemy debuffs, and relic quests. See [`Project_Roadmap.md`](Project_Roadmap.md) for the full design.
|
||||
- **Target platform:** Steam (Windows / Linux / Steam Deck).
|
||||
- **Visual direction (aspirational):** "painted tabletop miniature" look with Spider-Verse-style stepped (on-2s) enemy animation. Current visuals are **placeholder** (primitive meshes / cones, sourced creature models).
|
||||
|
||||
## Engine & tech
|
||||
|
||||
Unity **6.4 (6000.4.4f1)**, URP, IL2CPP, .NET Standard 2.1, Linear color space, new Input System, Force Text serialization. Netcode: **Netcode for GameObjects (NGO) 2.x**.
|
||||
|
||||
## Repository & collaboration
|
||||
|
||||
- Self-hosted **Forgejo**: `https://git.marlboro-bc.duckdns.org/yeahweregames/UnityTowerDefense` (git remote **`origin`**). A legacy GitHub mirror exists as remote **`github`**.
|
||||
- **Multiple contributors** work in parallel on feature branches merged to `main` via PRs. Expect `main` to move between sessions; rebase/branch off the latest.
|
||||
- **IP guardrail:** the prototype uses Games-Workshop-adjacent placeholder content (race names, sourced models). The repo stays **private**, no public builds/demos, until that content is replaced. Maintain a plain-text asset manifest of IP-derived assets to swap before any public release.
|
||||
|
||||
---
|
||||
|
||||
## Architecture & conventions
|
||||
|
||||
- **Server-authoritative gameplay; local-only UI/visual state.** Only gameplay-meaningful state is networked. Selection, placement ghosts, animation, and the paint cursor are client-local.
|
||||
- **Data-driven via ScriptableObjects:** `TowerDefinition`, `EnemyDefinition`, `WaveDefinition`, `RaceDefinition`, `GoldConfig`, `BuffDefinition`/`BuffCategory`, `DraftOption` (+ `NewTowerDraftOption`). Designers tune stats in assets, not code.
|
||||
- **Per-player state pattern:** `NetworkBehaviour`s on the **Player prefab** with a static `GetForClient(clientId)` / `Local` registry. Current set: `PlayerGoldManager`, `PlayerMatchState`, `PlayerBuffManager`, `PlayerTowerDeck`, `PlayerDraft`.
|
||||
- **Networked identifiers are catalog indices.** `TowerTypeId` indexes `TowerPlacementManager.towerDefinitions[]`; `DraftOptionId` indexes `DraftPool`. Stable **within a match**, not across sessions — cross-match persistence will need stable IDs (asset GUID or a serialized StableId).
|
||||
- **Namespaces:** `TD.Core` (enums, palettes, grid math), `TD.Gameplay` (builder, placement, match/wave/pathfinding state, economy, enemies, deck), `TD.Gameplay.Draft` (draft system), `TD.Combat` (TowerCombat/Projectile), `TD.Levels` (in-engine authoring + bake), `TD.UI`, `TD.Net`.
|
||||
- **Scenes:** `MainMenu` → `Lobby` → a Match level (`9Player`, `Main`).
|
||||
|
||||
### Engineering principles (carried across sessions)
|
||||
|
||||
- **Debug to root cause.** No defensive workarounds or per-frame state-correcting hacks — find and fix the actual cause.
|
||||
- **Restate design intent before coding.** Confirmed valuable repeatedly; prevents rework.
|
||||
- **Server owns gameplay; clients render.** Authoritative state on the server, visualization derived locally on each peer.
|
||||
|
||||
---
|
||||
|
||||
## Implemented systems
|
||||
|
||||
### Match foundation
|
||||
- `MatchState` (NetworkBehaviour scene singleton): phase (`Lobby`/`CountDown`/`Playing`/`Victory`/`Defeat`), current wave, shared lives. `PlayerMatchState` (per-player): slot allocation, race selection, ready state. Authoritative client-id → `PlayerSlot` mapping.
|
||||
|
||||
### Combat & towers
|
||||
- `TowerCombat` (server targeting loop; `OnTargetAcquired`/`OnTargetLost`/`OnFire` events for future animation), `Projectile`, hitscan + projectile paths, Single/Splash/Chain target types, damage types, range indicator. **Air-targeting** done: `GroundedOnly` towers skip flying enemies. Range checks are **3D**.
|
||||
- `TowerPlacementManager` (server-validated placement queue), `Builder` build queue with staged construction/pause/cancel/refund, `BuildSiteVisual` ghosts, owner tinting, RTS selection.
|
||||
|
||||
### Enemies & pathfinding
|
||||
- `EnemyHealth` (replicated HP, damage types, `IsFlying`, held state), `EnemyStatus` (lingering effects: slow/DoT), `EnemyMovement` (A* path following, zone-leak attribution, death/sink sequence).
|
||||
- `PathfindingService` (A* on the runtime walkability grid, octile heuristic, corner-cut prevention, line-of-sight path smoothing; re-paths on tower placement/removal).
|
||||
- **Flying enemies:** path on the **baked terrain grid** (`LevelLoader.IsBaseWalkable`) so they soar over towers; compute once, never re-path; spawn elevated by `EnemyDefinition.FlightHeight`.
|
||||
- Content: ~10 enemy definitions/prefabs (Crystal Golem / Cyclops / Ent variants, Undead Drake = the flying test enemy), 10 wave definitions.
|
||||
|
||||
### Lobby, connection & races
|
||||
- `MainMenu` + `Lobby` scenes; Direct-IP via `NetworkBootstrap` (the single seam for the deferred Steam swap) + `UnityTransport`; `LobbyService`, `SessionFlow`; **Quick Start** dev shortcut.
|
||||
- Race data + selection UI: `RaceDefinition`, `RaceRegistry`, 16-slot `RaceId`, `RaceSelectionOverlay`. Two placeholder races (both use the default builder). **Note:** "Race" is the code term; the roguelike design calls this concept "Builder" — naming reconciliation is an open decision.
|
||||
|
||||
### Economy
|
||||
- `PlayerGoldManager` (per-player, server-validated spend/award), `GoldConfig` (starting gold, per-wave kill rewards, completion + no-leak bonuses), `WaveManager` orchestration (prep countdown, per-zone spawning, kill-gold attribution, lives pool, Victory/Defeat).
|
||||
|
||||
### Roguelike — tower deck & draft (the current focus)
|
||||
- **`PlayerTowerDeck`** (per-player): the growable set of unlocked `TowerTypeId`s. Starts at the base set (seeded by `WaveManager` from `TowerPlacementManager.startingDeck`); placement is gated server-side (`TowerNotInDeck` rejection); the HUD build grid reads the local deck and rebuilds live on change. **Merged to `main`.**
|
||||
- **Draft system (Slice 1 — spine + "new tower"):** `DraftOption` (abstract SO) / `NewTowerDraftOption`, `DraftPool` (scene singleton catalog), `PlayerDraft` (per-player offered set + pick/buy-roll RPCs), `DraftService` (server weighted-random generation + offer/auto-resolve). The **prep phase is the draft window** (wave 1's prep = the match-start draft); unpicked drafts auto-resolve at prep end. Gold **"Buy Roll"** purchases an extra roll any time. Non-modal HUD overlay. **On branch `feature/draft-system`, verified working in-engine; pending commit/merge as of session end.**
|
||||
- Three base towers exist as data: **Basic Arrow** (ground+air single-target), **Siege Cannon** (ground-only splash), **Wall** (2×2, no damage, maze-shaping). *(Coworkers have begun replacing the placeholder cube visuals with cones.)*
|
||||
|
||||
### Paint system (PAUSED)
|
||||
- In-match paint (R/G/B + Reset) recolors owned towers and drives effects (Red=Splash, Green=Poison, Blue=Cold), server-authoritative. **Frozen** pending the roguelike reconciliation decision (it overlaps with systemic damage-type upgrades).
|
||||
|
||||
### Level authoring
|
||||
- In-engine `TD.Levels` authoring volumes (player zones, spawners, leak exits, goals) + `LevelData` + a bake pipeline producing walkability/placement/owner grids.
|
||||
|
||||
### HUD & dev tools
|
||||
- `HUDController` (UI Toolkit): gold/wave/lives/leaks, scoreboard, minimap, selection portrait + context panel, build/paint command grid, build progress, match-end overlay, chat, buff menu, and the new **draft overlay**.
|
||||
- `DevWaveControls`: **F9 / "Force Next Wave"**, **F8 / "Grant Next Tower"** (dev stand-in for deck growth).
|
||||
|
||||
---
|
||||
|
||||
## Known placeholders & technical debt
|
||||
|
||||
- **Visuals are placeholder** throughout (primitive/cone towers, sourced enemy models). See roadmap art TODOs.
|
||||
- **Enemies lack idle animation support** — needs adding across all enemy prefabs.
|
||||
- **Tower footprint visuals don't fill their 2×2 space** — the cone/mesh sits centered and reads smaller than the tile area it occupies; should visually reflect the full footprint.
|
||||
- **Two redundant tower lists:** `TowerPlacementManager.towerDefinitions[]` (by-index catalog) and `TowerRegistry.definitions[]` (by-name, for instance resolution). Candidate for consolidation.
|
||||
- **Catalog index 0 is a reserved sentinel** (valid `TowerTypeId`s start at 1) — easy to forget when wiring the catalog in the inspector.
|
||||
- **Catalog-index identifiers aren't session-stable** — blocks cross-match persistence until a stable ID is added.
|
||||
- **Paint system frozen**; **Race vs Builder** naming unresolved; the gold **"Buy Roll available any time"** rule is provisional and may change.
|
||||
- **Stubbed/unbuilt:** tower Upgrade/Sell actions (HUD buttons disabled), enemy resistances/weaknesses, in-match race-pick countdown.
|
||||
|
|
@ -2,464 +2,125 @@
|
|||
|
||||
## Purpose
|
||||
|
||||
This document consolidates outstanding work for the Unity Tower Defense project into a single sequenced plan. It supersedes the roadmap section of the existing context summary while preserving all engineering decisions, design rules, and architectural patterns documented there. The context summary remains the authoritative source for completed-work details and design rationale.
|
||||
The **forward-looking plan**: what's done, what's next, and the sequencing. Current-state architecture and the full list of implemented systems live in [`Project_Context.md`](Project_Context.md) — read that first for "how things work today." This document focuses on direction and remaining work.
|
||||
|
||||
The roadmap is organized in three phases:
|
||||
|
||||
- **Phase 1** — Functional gameplay completion. Everything required to have a playable, end-to-end tower defense match.
|
||||
- **Phase 2** — Visual prototype. Proves out the painted-miniature aesthetic on a single tower and a single enemy, using sourced reference content (Space Marine + Hormagaunt) as placeholders.
|
||||
- **Phase 3** — Path to commercial production. Notes on the IP swap-out, art lead hiring, and what changes when production art begins.
|
||||
Last substantial update: 2026-06-23 (reorganized around the roguelike pivot).
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Functional Gameplay Completion
|
||||
## The game
|
||||
|
||||
This phase carries forward all pending systems from the existing context document. The order below is the recommended sequencing; some items can run in parallel where noted.
|
||||
A **co-op tower-defense / roguelike hybrid** (up to 9 players, Steam). Wintermaul-style maze defense — build towers to snake enemies through your zone (shared lives, per-player gold) — fused with a roguelike draft.
|
||||
|
||||
### 1.1 Character pipeline mini-session (NEXT)
|
||||
### Roguelike core loop
|
||||
|
||||
Already identified as the immediate next task in the existing roadmap. Establishes the rigging and animation pipeline that everything downstream depends on.
|
||||
All players start with the **same three towers** — **Basic Arrow** (ground+air, single-target), **Siege Cannon** (ground-only, splash), **Wall** (no damage, near-free, maze-shaping). Players grow a personal **deck/"build"** through a **3-option draft** at match start and after every wave. The draft is free; players can also spend **gold to buy extra rolls**, creating the central tension: spend gold on towers (your maze) vs. spend on rolls (your build).
|
||||
|
||||
- Mixamo character + animation download walkthrough
|
||||
- Unity Humanoid rig setup
|
||||
- Animator Controller with idle / walk / build states
|
||||
- Replace Builder cylinder with rigged animated character
|
||||
- Connect Claude to Blender via MCP (first-time setup, walkthrough needed)
|
||||
**Five draft choice archetypes:**
|
||||
1. **New tower** — add a tower to your buildable arsenal.
|
||||
2. **Tower upgrade** — *systemic first* ("all towers of damage-type/projectile-type X gain Y", "unlock Lv.2 across the deck"); per-tower trees only later if needed.
|
||||
3. **Enemy debuff** — temporary (weaker / slower / downgrade a tier); cost scales with duration.
|
||||
4. **Builder ability** — active, targeted, in-wave, cooldown-based (e.g. Call Lightning, Haste, Stasis Trap).
|
||||
5. **Relic quest** — opt-in secondary objective granting a powerful passive Relic.
|
||||
|
||||
This pipeline becomes the foundation for both the prototype enemy and any humanoid towers in Phase 2.
|
||||
|
||||
### 1.2 HUD
|
||||
|
||||
Can run in parallel with later items in this phase. Several existing systems have stubs awaiting HUD consumers.
|
||||
|
||||
- Canvas + components: minimap, portrait, context panel, 4×3 tower/upgrade grid
|
||||
- Tower buttons calling `TowerPlacementController.BeginPlacement` (replaces `TowerPlacementTestTrigger`)
|
||||
- Rejection message display subscribing to `TowerPlacementController.OnRejectionMessageReady`
|
||||
- Build progress bar above active construction (consumes `BuildJob` / `BuildSiteVisual` state)
|
||||
- Cancel-shelved-tower button or context-menu gesture (deferred from Path D2)
|
||||
- Settings menu eventually owning `EdgePanEnabled`, scroll sensitivity, etc.
|
||||
|
||||
### 1.3 Match state and player slot mapping
|
||||
|
||||
Several systems currently hold stubbed mappings that must be resolved before multiplayer is meaningful.
|
||||
|
||||
- `MatchState` `NetworkBehaviour` — global match data: current wave, lives, race-pick timer
|
||||
- `PlayerMatchState` `NetworkBehaviour` — per-player: race selection, etc.
|
||||
- Authoritative client-id → `PlayerSlot` mapping. Replaces the five stubbed mappings in `TowerPlacementManager`, `TowerPlacementController`, `CameraController`, `PlayerBuilderSpawner`, and `Builder`.
|
||||
|
||||
### 1.4 Combat system
|
||||
|
||||
The most architecturally significant pending system. This blocks the Phase 2 tower animation work (towers can't have target-acquired/firing states to animate until something generates those states).
|
||||
|
||||
- `TowerCombat` component on tower prefabs. Component-per-behavior rather than flat fields on `TowerDefinition` — see existing lessons.
|
||||
- Targeting logic: range check, target acquisition, target prioritization (closest, lowest-HP, etc.)
|
||||
- Projectile prefabs and hitscan handling
|
||||
- Damage application against enemy HP
|
||||
|
||||
**Architectural note for Phase 2 setup:** when designing `TowerCombat`, expose the following events for visual consumers (these will drive tower animation in Phase 2):
|
||||
|
||||
- `OnTargetAcquired` — first enemy enters range
|
||||
- `OnTargetLost` — no enemies remaining in range
|
||||
- `OnFire` — weapon discharges (per `FireRate` cycle)
|
||||
- `CurrentTarget` reference for procedural rotation
|
||||
|
||||
### 1.5 Enemy system
|
||||
|
||||
- Enemy `NetworkBehaviour` (HP, movement, type, debuffs)
|
||||
- Spawning system driven by waves / match state
|
||||
- Enemy prefab pipeline for Phase 2 prototype
|
||||
|
||||
### 1.6 Pathfinding
|
||||
|
||||
- A* on the runtime walkability grid (consumes `LevelLoader.IsWalkable`)
|
||||
- 4-connected, no diagonals (matches existing maze validation)
|
||||
- Re-pathing on tower placement/removal events. Fire on: `TowerInstance` spawn/despawn, construction-start, construction-finish, shelved-tower despawn.
|
||||
|
||||
### 1.7 Lobby & Connection (COMPLETE — 2026-05-17)
|
||||
|
||||
Replaced the bare "Start Host" button with a proper main menu → lobby → match flow.
|
||||
Lobby is its own scene; players gather, pick races, ready up, and the host starts the match.
|
||||
|
||||
**v1 shipped (Direct IP, Option A host-leaves-closes-lobby):**
|
||||
|
||||
- `MainMenu` scene — Host / Join (with IP+port field) / Quit / **Quick Start** (dev shortcut)
|
||||
- `Lobby` scene — player list, per-player **Select Race** button opening the race-selection overlay, ready toggle, host-only Start button, Leave button
|
||||
- `LobbyService` `NetworkBehaviour` scene singleton — Start Match RPC, Return to Lobby RPC
|
||||
- `NetworkBootstrap` static — wraps `UnityTransport` host/join/disconnect. Designed for the Steam swap-out below: same call sites, different transport.
|
||||
- `SessionFlow` DontDestroyOnLoad singleton — routes disconnected peers back to MainMenu on host loss
|
||||
- `PlayerMatchState` extended with `IsReady` NetworkVariable and `SubmitRaceRpc` / `SubmitReadyRpc` for client → server submissions
|
||||
- `PlayerBuilderSpawner` refactored to spawn on `NetworkSceneManager.OnLoadEventCompleted` for the Match scene, not on initial player spawn. Pulls race-specific `BuilderPrefab` from `RaceRegistry` when available, falls back to the inspector default. Re-spawns cleanly across Match → Lobby → Match cycles.
|
||||
- Match-end overlay extended: **Retry** (back to lobby) AND **Return to Main Menu** (this player only)
|
||||
- Host leaves → all clients return to main menu via `SessionFlow.OnClientDisconnectCallback` (Option A). Same applies if the host clicks "Return to Main Menu" after a match.
|
||||
- **Race data + selection UI** delivered as part of 1.7 (originally scoped for 1.8): `RaceId` enum expanded to 16 slots, `RaceDefinition` ScriptableObject (Id, DisplayName, Icon, BuilderName, BuilderDescription, LoreText, BuilderPrefab, Towers stub), `RaceRegistry` (DontDestroyOnLoad singleton in MainMenu — single source of truth across all scenes), `RaceSelectionOverlay` (4×4 grid + detail panel, taken-race greying with picker badge, Esc/X close, server-side exclusivity enforced via `SubmitRaceRpc`).
|
||||
- **Quick Start dev button** in MainMenu — bypasses lobby: hosts → waits for `PlayerMatchState.Local` → sets Race1 + ready → loads Match scene directly. Coroutine in `MainMenuController.QuickStartCoroutine`.
|
||||
|
||||
### 1.7-Future Steam Lobby Migration (Option C — DEFERRED)
|
||||
|
||||
When the Steam SDK is integrated (post-1.7, before 1.8 or in parallel), the Direct IP backend is replaced with a Steam-backed lobby provider. **The lobby UI and gameplay code do not change.** The migration touches only `NetworkBootstrap` and adds a `LobbyProvider` abstraction behind it.
|
||||
|
||||
**What changes:**
|
||||
|
||||
- Add `Facepunch.Steamworks` (recommended) or `Steamworks.NET` to the project
|
||||
- Use Steam app ID **480 (Spacewar)** during development; switch to the actual app ID once Steam page is provisioned
|
||||
- Replace `UnityTransport` with `SteamNetworkingSocketsTransport` (community-maintained NGO transport)
|
||||
- Refactor `NetworkBootstrap` from static helpers into an `IConnectionProvider` interface with two implementations:
|
||||
- `DirectIpConnectionProvider` (existing behavior, kept for LAN testing and DRM-free distribution)
|
||||
- `SteamConnectionProvider` (Steam lobby create/join, friend invite via Steam overlay, Steam P2P sockets)
|
||||
- Add lobby browser UI to `MainMenu` scene (currently just Host/Join buttons)
|
||||
- Friend-invite flow: handled by Steam overlay (Shift+Tab → invite friend); join request lands in the existing Lobby scene
|
||||
- Host-leaves behavior upgrades from Option A → Option C: Steam lobbies persist independently of the game host, so a new host can be elected from remaining members rather than tearing the lobby down. **This is the deferred behavioral upgrade flagged for migration time.**
|
||||
|
||||
**Notes for the migration:**
|
||||
|
||||
- Lobby code already factored to read player state from `PlayerMatchState` (carries across scene loads) and lobby-wide state from `LobbyService` — no UI rewrite needed
|
||||
- The `IConnectionProvider` abstraction is the single seam between gameplay and transport; everything else stays put
|
||||
- Both providers should coexist in the codebase so DRM-free builds (itch.io, direct distribution) can still ship without Steam
|
||||
- Steam Direct fee ($100 USD) is required to publish on Steam but NOT for development — Spacewar app ID 480 is free to use
|
||||
|
||||
### 1.7.5 Terrain architecture
|
||||
|
||||
Decision deferred per existing context document; Builder code is already terrain-agnostic. Recommend deciding after the character pipeline mini-session and before Phase 2 begins (the visual prototype will look very different on Unity Terrain vs mesh-based terrain).
|
||||
|
||||
Options previously discussed: Unity Terrain, mesh-based (Blender), ProBuilder, per-tile heights via volumes.
|
||||
|
||||
### 1.8 Race system (Path E) — PARTIALLY COMPLETE
|
||||
|
||||
Data model and lobby selection UI shipped as part of 1.7 (2026-05-17). Remaining work is gameplay-side integration of the race payload during a match.
|
||||
|
||||
**Already done (delivered with 1.7):**
|
||||
|
||||
- `RaceDefinition` ScriptableObject with Identity / Builder / Lore / Gameplay-payload sections
|
||||
- `RaceRegistry` DontDestroyOnLoad singleton (placed in MainMenu, persists into Lobby + Match)
|
||||
- `RaceId` enum (None + Race1..Race16) — 16-slot grid reserved
|
||||
- Lobby race-selection overlay (4×4 grid + detail panel + exclusivity enforcement)
|
||||
- `PlayerMatchState.RaceSelection` NetworkVariable + `SubmitRaceRpc`
|
||||
- `PlayerBuilderSpawner` reads race-specific `BuilderPrefab` when available
|
||||
- Two placeholder RaceDefinition assets configured with the default builder (proves the data path works without race-specific content)
|
||||
|
||||
**Still pending:**
|
||||
|
||||
- Replace lobby race-pick with in-match race-pick overlay (timer-based auto-lock, `MatchState.RacePickTimer`) — OR retain lobby pick and remove the in-match flow concept. **Open decision** (see 1.10 reconciliation question).
|
||||
- Multi-builder race support — revisit `PlayerBuilderSpawner` to spawn N builders for races that need them
|
||||
- `TowerRegistry` filtering by active match's race rosters (currently shows all registered towers regardless of race)
|
||||
- Auto-discovery of `RaceDefinition` assets (today's flow: drag each asset into `RaceRegistry.Definitions` manually)
|
||||
- Replace `TowerPlacementManager.towerDefinitions[]` inspector array with race-driven discovery
|
||||
- Race-specific builder prefabs (currently every race uses the same default builder)
|
||||
- Real race content (display names, lore, icons, distinct builder visuals) — placeholder races exist but are interchangeable
|
||||
|
||||
### 1.9 Camera polish
|
||||
|
||||
Flagged for revisit; not blocking anything.
|
||||
|
||||
- Cursor-anchored zoom near map edges (Q3c flagged as revisitable)
|
||||
- Center-on-builder hotkey (e.g., Space)
|
||||
- Initial camera position taking race or match phase into account
|
||||
|
||||
### 1.10 Tower Customization & Meta-Progression (DEFERRED — DESIGN CAPTURED; IN-MATCH PAINT v1 + EFFECTS IMPLEMENTED)
|
||||
|
||||
Long-term cross-match progression loop. Recorded here so Phase 1.8 race-system design and Phase 2 visual prototype work can anticipate the customization data model when they're scheduled. Not blocking Phase 1 exit criteria — a single match is fully playable without it.
|
||||
|
||||
**Implemented — in-match paint v1 + paint-driven effects (2026-06-03 / 2026-06-09)**
|
||||
|
||||
A first-pass, in-match paint mechanic is built, verified in-engine, and now drives both tower visuals **and** projectile behavior.
|
||||
|
||||
Paint UI + recolor (2026-06-03):
|
||||
- **Build / Paint tabs** in the command grid (shown when the builder is selected). Build tab is the existing tower menu.
|
||||
- **Paint tab:** Red / Green / Blue swatches plus a **Reset** (clear) brush, selectable by click or **Q / W / E / R** hotkeys. The active brush swaps the cursor to a tinted paint circle.
|
||||
- **Click a tower you own → recolors it**; Reset reverts it to its owner color.
|
||||
- **Server-authoritative & networked:** `PaintColor` enum + `paintColor` `NetworkVariable` on `TowerInstance`, set via `RequestPaintServerRpc` (own-tower-only validation), replicated to all clients. Tint applied through the existing `MaterialPropertyBlock` path (`TowerInstance.ApplyTint`, which falls back to all child `MeshRenderer`s when none are explicitly listed).
|
||||
- **New code:** `TowerPaintController` (client-local, mirrors `TowerPlacementController`), `PaintColors` palette in Core, Paint tab + swatches in `HUDController`.
|
||||
|
||||
Paint-driven combat effects (2026-06-09):
|
||||
- `TowerCombat` resolves an effective **`CombatProfile`** per shot from the tower's `TowerDefinition` overridden by its `Paint` color, threaded through both the projectile and hitscan paths (unpainted towers fire as authored).
|
||||
- **Red = Splash** (radius 2 tiles), **Green = Poison DoT** (½ base damage/sec for 3s), **Blue = Cold slow** (50% speed for 2s). Reuses the existing `EnemyStatus.ApplyEffect` / splash systems. Magnitudes are named constants in `TowerCombat` (promote to a ScriptableObject if designer tuning without recompile is needed).
|
||||
- **Projectiles tint to the paint color**, replicated via a `PaintColor` `NetworkVariable` on `Projectile` (same pre-spawn → `OnNetworkSpawn` pattern as `TowerInstance`).
|
||||
|
||||
Still pending for the full meta-progression:
|
||||
- Persistence / player profile, end-of-match reward rolls, decals, stat-modifier stack, layering / set bonuses, and the customization menu — all remain deferred as described below.
|
||||
- The current paint→effect mapping is **override** (paint replaces authored effects) and a fixed 3-color set; the eventual customization system will generalize this to a layered modifier stack drawn from a reward pool.
|
||||
|
||||
**Core concept**
|
||||
|
||||
- Every player starts each match with the same **base tower set** — a small fixed roster (universal or per-race; see open question below).
|
||||
- **End of match awards a customization reward**, drawn from a Warhammer-themed pool. Win and loss draw from **different pools** — winning unlocks higher-tier / rarer rewards, losing still progresses at a slower rate so no match is truly wasted.
|
||||
- Reward types:
|
||||
- **Paint color** (e.g. Macragge Blue, Mephiston Red, Caliban Green) — applied to tower visuals, can carry gameplay effects (see below)
|
||||
- **Sticker decal** — chapter badges, regimental icons, faction sigils
|
||||
- Other Warhammer-aligned customizations: transfers, weathering, base styles, freehand-style elements
|
||||
- Customizations are **persistent across matches** via a player profile / save system.
|
||||
- Applied via a **customization menu** (does not exist yet) reachable from the main menu, showing owned customizations and per-tower application slots.
|
||||
|
||||
**Gameplay enhancements via customization**
|
||||
|
||||
Cosmetics double as upgrades. The pattern mirrors the real-world Warhammer hobby loop (collect → paint → customize) translated into mechanical progression:
|
||||
|
||||
- **Stat modifiers** — extended range, increased damage, faster fire rate, larger splash radius
|
||||
- **Damage-type changes** — applying a particular paint scheme grants Fire / Cold / Poison damage to an otherwise basic tower
|
||||
- **Special effects** — chain hits, slow effects, DoT — driven by specific decal or paint combinations
|
||||
- **Layering / set bonuses** — applying multiple customizations from the same "faction" might unlock additional effects (e.g. full Salamanders paint kit + chapter decal grants a flamer-style damage profile)
|
||||
|
||||
**Required systems (none exist yet)**
|
||||
|
||||
- **Player profile / persistence** — local save for now, account-bound later. Tracks owned customizations + per-tower application state.
|
||||
- **`CustomizationDefinition` ScriptableObject** — stat-delta fields, visual asset references (decal texture, color values), rarity tier, reward-pool tags.
|
||||
- **Tower stat modifier stack** — extend `TowerDefinition` (currently flat fields) to accept a layered modifier stack from applied customizations. Affects targeting, damage, range, fire rate at runtime.
|
||||
- **End-of-match reward roll** — server-authoritative draw from win/loss pool, replicated to the relevant player. Anti-cheat: server owns the roll, not the client.
|
||||
- **Customization menu UI** — main-menu-accessible. Browse owned, browse locked, apply / remove per tower in the base set.
|
||||
- **Networked applied-customization state during a match** — towers must visually reflect each owner's applied customizations on every peer, AND stat modifiers must be authoritative on the server.
|
||||
|
||||
**Open question — interaction with the Phase 1.8 race system**
|
||||
|
||||
Phase 1.8 currently assumes **race-driven tower rosters** (each race has its own distinct set of towers). This new direction suggests a flatter, customization-driven model. The two systems need reconciliation before either ships:
|
||||
|
||||
- **Option A:** Race remains the primary tower-set distinction. Customizations layer on top of race-specific towers. Each race owns its own customization pool.
|
||||
- **Option B:** Race becomes one customization category among many. Tower set is universal across players; "race" identity emerges from the player's chosen paint scheme / decal kit.
|
||||
- **Option C:** Hybrid — small universal base set + larger race-locked roster, both customizable. Customizations affect both.
|
||||
|
||||
Decision deferred until both 1.8 and 1.10 are actively scheduled. The choice has significant implications for content scope: Option A means N races × M customizations each; Option B means one tower set × (M customizations × N "factions"). Option B is dramatically less art work per tower.
|
||||
|
||||
**Why this is deferred**
|
||||
|
||||
- Not required for Phase 1 exit criteria (a single match plays fine without cross-match progression).
|
||||
- Phase 2 visual prototype doesn't depend on this — the painted-miniature aesthetic provides the visual scaffolding customization will eventually exploit, but the prototype tower's paint scheme can be hard-coded for the demo.
|
||||
- The data model interaction with Phase 1.8 needs to be resolved first (see open question above).
|
||||
|
||||
**When to schedule**
|
||||
|
||||
Earliest reasonable slot: after Phase 1 exit criteria are met and Phase 1.8 race-system design has the customization-interaction question resolved. Could run in parallel with Phase 2 (no overlap with visual prototype scope) or after Phase 2 if visual direction needs to prove out first.
|
||||
|
||||
### Phase 1 exit criteria
|
||||
|
||||
A complete match is playable end-to-end: race pick → builder spawn → tower placement and construction → wave spawning → enemies path through the maze → towers shoot enemies → enemies die or leak → match concludes. Visuals are placeholder. All systems in this section are functional.
|
||||
### Locked design decisions
|
||||
- Draft is **free** at match start + after each wave; a gold **shop buys extra rolls**, available any time *(provisional — may change)*.
|
||||
- **Per-player** decks/progression (each player drafts independently).
|
||||
- Tower **upgrades are systemic-first**.
|
||||
- **Draft pacing:** the prep phase before each wave is the draft window; unpicked drafts **auto-resolve at prep end** so no slow/AFK player stalls the group.
|
||||
- Draft options are **authored ScriptableObjects** drawn by **weighted random**.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Visual Prototype
|
||||
## Status at a glance
|
||||
|
||||
Begins only after Phase 1 exit criteria are met. This phase exists to prove out the painted-miniature visual direction on minimal content (one tower, one enemy), so the direction can be communicated to a future art lead with concrete, in-engine examples.
|
||||
**Done & merged:** functional match loop end-to-end (placement, construction, waves, pathfinding, combat, match-end), HUD, lobby + race selection, economy, enemies + A* pathfinding, **flying enemies + air-targeting**, **per-player tower deck** (roguelike keystone #1).
|
||||
|
||||
### Prototype scope and constraints
|
||||
**Done, pending commit/merge:** **draft system Slice 1** (the draft spine + "new tower" choice), verified working in-engine on branch `feature/draft-system`.
|
||||
|
||||
- **One tower** — Space Marine in Ultramarines colors, standing-ready pose, with idle / aim / fire animation states
|
||||
- **One enemy** — Tyranid Hormagaunt with walk / death animations, animated on 2s for the Spider-Verse-style stepped-frame effect
|
||||
- **Sourced reference content.** The Space Marine and Hormagaunt are placeholders pulled from existing third-party models. They are private prototype assets only — see Phase 3 for the swap-out plan.
|
||||
**Paused:** in-match Paint system (overlaps with systemic upgrades — reconcile before building further).
|
||||
|
||||
### IP guardrails for the prototype
|
||||
|
||||
These hold throughout Phase 2 and until the Phase 3 swap-out is complete.
|
||||
|
||||
- Repo `mafoster134/UnityTowerDefense` remains private on GitHub. No public builds, no itch.io demos, no public streams or videos showing GW-derived assets.
|
||||
- Internal multiplayer testing with friends over private connections is acceptable; anything resembling public distribution is not.
|
||||
- Don't redistribute GW source files. Kitbash from photos/concept art into your own meshes and textures rather than embedding GW-shipped assets directly. Cleaner swap-out later.
|
||||
- Maintain a plain-text **asset manifest** in the repo listing every asset using GW IP that needs replacement before commercial release.
|
||||
|
||||
### 2.1 Shared visual infrastructure (one-time investment)
|
||||
|
||||
Builds the technical scaffolding all Phase 2 assets (and all future production assets) consume.
|
||||
|
||||
#### 2.1.1 Painted-miniature URP shader
|
||||
|
||||
Custom Shader Graph variant of URP Lit, saved as `MiniatureStandard`.
|
||||
|
||||
- Standard PBR inputs: albedo, normal, metallic, roughness, AO
|
||||
- **Fresnel rim term** added to the lighting output. Drives a subtle lighter color along glancing angles. Fakes the drybrush highlights miniature painters apply to raised edges. Critical for the "photographed painted miniature" read.
|
||||
- **Crushed shadow curve** — adjustment that deepens the shadow-to-midtone transition. Mimics how miniatures look under photo-studio key lighting.
|
||||
- Optional: subtle screen-space painted-noise overlay at very low intensity for tactile texture.
|
||||
|
||||
This shader is reused by every painted-miniature asset going forward.
|
||||
|
||||
#### 2.1.2 Lighting and post-processing setup
|
||||
|
||||
Project-wide, applied via the existing `DefaultVolumeProfile` system.
|
||||
|
||||
- **Strong directional key light** at ~45° elevation, slightly warm
|
||||
- **Soft fill light** from opposite side, cool-tinted
|
||||
- **Subtle bounce light** from below to lift bases (GW studio-shot reference)
|
||||
- **AO intensity** raised slightly past realistic
|
||||
- **Tilt-shift DOF** in the volume profile — slight blur at top and bottom of frame to sell diorama scale
|
||||
- **Color grading** toward saturated, warm-shadowed
|
||||
- Slight **vignette**
|
||||
|
||||
Applied to URP-Medium first; ported to URP-Low / High / Ultra when those quality tiers are configured (existing pending work).
|
||||
|
||||
#### 2.1.3 On-2s animation system
|
||||
|
||||
A `MonoBehaviour` (working name `SteppedAnimator`) attached to objects whose animation should appear at ~12fps despite the game running at 60fps. Selectively applied to skeletal animation; effects, projectiles, and UI remain smooth.
|
||||
|
||||
Implementation approach:
|
||||
|
||||
- `LateUpdate` samples the current `Animator` pose
|
||||
- Quantizes time to 1/12-second steps
|
||||
- Holds previous-frame pose on intermediate frames
|
||||
- Configurable target framerate (default 12fps; can be tuned per-asset)
|
||||
|
||||
Design rule: stepped animation applies to **skeletal body animation and weapon-target tracking rotation only**. Muzzle flashes, projectiles, and UI run at full framerate. The contrast between stepped and smooth is what sells the Spider-Verse effect — global stepping looks broken.
|
||||
|
||||
#### 2.1.4 `TowerVisualController` component
|
||||
|
||||
New component on tower prefabs. Bridges the gameplay-side `TowerCombat` events (built in Phase 1.4) to the animation system.
|
||||
|
||||
- Subscribes to `TowerCombat.OnTargetAcquired`, `OnTargetLost`, `OnFire`
|
||||
- Sets Animator parameters: `HasTarget` (bool), `Fire` (trigger)
|
||||
- Drives procedural torso/turret rotation toward `TowerCombat.CurrentTarget` via `LateUpdate` LookAt on a designated pivot bone
|
||||
- Pure local visualization. No `NetworkBehaviour`, no networked state. Each client renders animation state from its own read of the networked combat state.
|
||||
|
||||
This component, like `MiniatureStandard` and `SteppedAnimator`, is one-time scaffolding that every future tower consumes.
|
||||
|
||||
### 2.2 Prototype tower — Space Marine (Ultramarine)
|
||||
|
||||
#### 2.2.1 Reference and source acquisition
|
||||
|
||||
- Pull 8–12 reference images of Ultramarine Tactical / Intercessor Marines in standing-ready poses. PureRef board.
|
||||
- Acquire base model from MyMiniFactory, Sketchfab, or CGTrader. Prefer "miniature-scale" models with proportions already correct for tabletop aesthetic over high-detail "video game" models.
|
||||
|
||||
#### 2.2.2 Mesh prep
|
||||
|
||||
- Decimate / retopo to 5k–15k tris
|
||||
- Re-UV (most source models have UVs optimized for 3D printing, not texturing)
|
||||
- Reasonable texel density for 2K texture target
|
||||
|
||||
#### 2.2.3 Texturing
|
||||
|
||||
Substance Painter (or alternative). Build the painted-Ultramarine look in layers:
|
||||
|
||||
1. **Base coat** — Macragge Blue equivalent. Slightly desaturated steely middle value.
|
||||
2. **Recess shading** — darker blue-violet wash driven by curvature map. Makes armor plates read as separate panels.
|
||||
3. **Edge highlighting** — Calgar Blue / Fenrisian Grey territory. Visibly broken and slightly streaky, not perfect lines. *Single most "miniature-painted" visual cue.*
|
||||
4. **Final extreme highlights** — near-white blue, applied only to the absolute sharpest edges, very sparingly.
|
||||
5. **Gold trim** on aquila, helmet rim, weapon details. NMM-painted gradients photograph better at top-down distance than real metallic.
|
||||
6. **Black undersuit and weapon casing** — flat very dark navy with subtle blue rim highlights. Avoid pure black.
|
||||
7. **Battle damage** — small chips on edges showing silver underneath the blue. Used very sparingly.
|
||||
8. **Base** — broken urban rubble or red Mars-like sand. Skull or two. Optional static grass tuft.
|
||||
|
||||
**Critical:** paint deliberate brushstroke variation into the albedo. Slight value/hue shifts across flat surfaces. Airbrushed-gradient textures will read as CGI; broken-application textures read as painted miniature.
|
||||
|
||||
Consider community-made "Warhammer / miniature / tabletop" smart materials in the Substance Share library as a starting point; customize from there.
|
||||
|
||||
#### 2.2.4 Animation states
|
||||
|
||||
Three states minimum, retargeted from Mixamo "rifle" animation set onto the Humanoid rig (leveraging the Phase 1.1 character pipeline work):
|
||||
|
||||
- **Idle** — relaxed stance, bolter held at low ready, subtle breathing/sway loop
|
||||
- **Aiming** — bolter raised, tracking target. Procedural torso pivot rotates toward `CurrentTarget` via `TowerVisualController` LateUpdate LookAt; clip provides upper-body pose.
|
||||
- **Firing** — short burst animation: muzzle flash effect, recoil pulse, optional casing eject
|
||||
|
||||
Animator Controller transitions:
|
||||
- Idle → Aiming: `HasTarget == true`
|
||||
- Aiming → Idle: `HasTarget == false` (with ~1 second blend back to relaxed pose)
|
||||
- Aiming → Firing: `Fire` trigger (returns to Aiming on completion)
|
||||
|
||||
Apply `SteppedAnimator` to the body Animator and the torso pivot rotation. Muzzle flash and projectiles remain smooth.
|
||||
|
||||
#### 2.2.5 Integration
|
||||
|
||||
- Create `TowerDefinition` asset. DisplayName "Space Marine" (will become original-faction equivalent in Phase 3). FootprintSize 2×2. GoldCost per economy tuning. BuildTime 4 seconds (matches existing test value). Combat fields per `TowerCombat` design from Phase 1.4.
|
||||
- Plug into `TowerPlacementManager.towerDefinitions[]` for testing (eventually replaced by race-driven discovery from Phase 1.8)
|
||||
- Verify the existing `BuildSiteVisual` ghost system handles the new prefab correctly (green/white/red tinting for build stages should work as-is)
|
||||
|
||||
#### 2.2.6 Time estimate
|
||||
|
||||
Roughly 25–43 hours total. Bulk of time in texturing (8–12h) and animation retargeting / state machine (6–10h). Shader and lighting are one-time investments amortized across all future towers.
|
||||
|
||||
### 2.3 Prototype enemy — Tyranid Hormagaunt
|
||||
|
||||
#### 2.3.1 Reference and source acquisition
|
||||
|
||||
- Reference images of Hormagaunts in scuttling poses. Note the four-legged-with-scythe-arms body plan.
|
||||
- Source model from same channels as the tower. Hormagaunts have a distinct silhouette that's forgiving for proportions but demands clean topology around the joints for the on-2s animation to read well.
|
||||
|
||||
#### 2.3.2 Mesh prep and rigging
|
||||
|
||||
- Decimate / retopo as for the tower
|
||||
- Custom rig (Mixamo's Humanoid rig won't fit the four-legged plan). Mixamo's "Quadruped" auto-rigger is a possibility, or hand-rig in Blender.
|
||||
- Skinning and weight painting
|
||||
|
||||
#### 2.3.3 Texturing
|
||||
|
||||
Same painted-miniature pipeline as the tower, tuned for organic Tyranid biology:
|
||||
|
||||
- Chitin plates with hard-edge gradients (purple-to-bone is the canonical Hyve Fleet Leviathan scheme)
|
||||
- Recess shading deeper than on the tower — Tyranid sculpts have very pronounced cavities
|
||||
- Subsurface treatment on flesh areas (mouth, claws, exposed muscle)
|
||||
- Bone highlights drybrushed onto chitin edges
|
||||
- Glossy claws and teeth
|
||||
|
||||
#### 2.3.4 Animations
|
||||
|
||||
Pulled from Mixamo "creature" set or hand-authored:
|
||||
|
||||
- Walk / scuttle cycle — fast, low to the ground
|
||||
- Death — stagger, collapse
|
||||
|
||||
Apply `SteppedAnimator` aggressively here. The scuttling gait at 12fps is the *signature* visual moment of the prototype — this is where the Spider-Verse effect is most visible and most rewarding. Generic Mixamo creature animation often looks floaty at 60fps; stepped to 12fps, it reads as deliberately stylized.
|
||||
|
||||
#### 2.3.5 Integration
|
||||
|
||||
- Hook into the Phase 1.5 enemy system. HP, speed, damage stats per enemy type design.
|
||||
- Walks the maze via Phase 1.6 pathfinding.
|
||||
- Takes damage from the Space Marine tower's projectiles via Phase 1.4 combat system.
|
||||
|
||||
#### 2.3.6 Time estimate
|
||||
|
||||
Roughly 17–29 hours. Quadruped rigging is the wildcard — could compress significantly with auto-rigging tools or expand if hand-rigged from scratch.
|
||||
|
||||
### 2.4 Demo scene
|
||||
|
||||
Final integration step. A small purpose-built scene demonstrating the visual direction in motion.
|
||||
|
||||
- Single Space Marine tower placed and animating
|
||||
- Wave of Hormagaunts spawning and pathing toward an exit
|
||||
- Tower acquires targets, aims, fires; Hormagaunts die in stepped-animation glory
|
||||
- Camera positioned to show the tabletop-diorama framing
|
||||
- Tilt-shift DOF, painted-miniature lighting and post all active
|
||||
|
||||
This scene becomes the artifact you show to candidate art leads in Phase 3.
|
||||
|
||||
### Phase 2 exit criteria
|
||||
|
||||
The demo scene runs in-editor showing the painted-miniature aesthetic working end-to-end with real gameplay (towers shooting enemies, enemies dying). The visual direction is clearly communicated. The shared infrastructure (`MiniatureStandard` shader, lighting setup, `SteppedAnimator`, `TowerVisualController`) is in place and reusable.
|
||||
**Next:** draft Slice 2 (systemic upgrades).
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Path to Commercial Production
|
||||
## Part A — Roguelike build-out (current focus)
|
||||
|
||||
Notes only. This phase is not actionable until Phase 2 is complete and the decision to pursue commercial release is made.
|
||||
### A1. Per-player tower deck — ✅ DONE (merged)
|
||||
Towers are unlocked per player, not globally available. See [`Project_Context.md`](Project_Context.md).
|
||||
|
||||
### 3.1 IP swap-out
|
||||
### A2. Draft spine + "new tower" (Slice 1) — ✅ DONE (branch `feature/draft-system`)
|
||||
Server generates 3 weighted options per player each prep phase; players pick via a non-modal HUD overlay; unpicked auto-resolve at prep end; gold "Buy Roll" purchases extra rolls. Only choice type is **New Tower** (grants into `PlayerTowerDeck`). *Commit/merge pending.*
|
||||
|
||||
The prototype Space Marine and Hormagaunt must be replaced before any public release. Two paths:
|
||||
### A3. Draft Slice 2 — systemic tower upgrades — ⏭ NEXT
|
||||
- Add an upgrade `DraftOption` subclass applying **systemic** modifiers: "all towers of damage-type X gain Y", "all projectile-type X …", "unlock Lv.2 across deck".
|
||||
- **Reconcile with `PlayerBuffManager`.** The existing gold-bought buff stack (global Damage/AttackSpeed multipliers) is the natural underlying apply mechanism — generalize it to type-filtered modifiers and make the draft (not the standalone buff menu) the entry point. Decide the fate of the old buff menu here.
|
||||
|
||||
- **Original-faction replacement.** Develop original factions in the painted-miniature aesthetic. The "heavy armored warrior order" archetype and "horde swarm" archetype are not owned by anyone; the specific GW execution is. Production art lead's first major task.
|
||||
- **GW license.** Possible but selective. GW has licensed indie tabletop-adjacent games; the bar is meaningful. Worth investigating if the prototype demo is strong enough to take to GW directly.
|
||||
### A4. Builder active abilities (draft choice #4)
|
||||
New in-wave, targeted, cooldown ability system (Call Lightning, Haste, Stasis Trap). New `DraftOption` subclass grants an ability to the builder; needs input/targeting + cooldown UI. Some builders may start with an ability unlocked.
|
||||
|
||||
The data model already supports clean swap-out: `TowerDefinition` is asset-referenced, so replacing the prefab and texture references is the only gameplay-side change. Code is unaffected.
|
||||
### A5. Relic quests (draft choice #5)
|
||||
Secondary-objective tracking ("place 50 towers in 5 min", "leak 0 for 3 waves") that, on completion, grants a powerful passive **Relic** (e.g. corpses leave slowing ice, electric hits stun). Needs objective tracking + relic passive/event hooks.
|
||||
|
||||
### 3.2 Art lead hiring
|
||||
### A6. Enemy debuffs (draft choice #3)
|
||||
Temporary debuffs purchasable in the draft (weaker / slower / **downgrade a tier**). "Downgrade" implies an **ordered enemy progression** the data doesn't model yet — add that first.
|
||||
|
||||
The Phase 2 demo scene is the primary recruitment artifact. Look for:
|
||||
### A7. Tower content beyond the starting three
|
||||
The "new tower" draft is only meaningful once the pool has towers **beyond** the starting set. Author additional tower types (definitions, prefabs, draft options). *(Until then, the new-tower draft has nothing valid to offer because all towers are starting towers.)*
|
||||
|
||||
- Tabletop miniature painting experience or demonstrable understanding of the aesthetic
|
||||
- 3D character/prop authoring at game-asset quality
|
||||
- Comfort with stylized PBR pipelines
|
||||
- Familiarity with Unity URP and Shader Graph (or willingness to learn)
|
||||
### A8. Cross-match persistence / meta-progression
|
||||
Player profile saving owned content; **win/loss reward pools** drawn server-side at match end; the customization menu. **Requires a session-stable tower/option ID** (catalog indices reset each match — see Context "technical debt").
|
||||
|
||||
### 3.3 Production scope expansion
|
||||
|
||||
- Multiple races (Phase 1.8 architecture supports this; content fills it)
|
||||
- Full tower roster per race (likely 6–10 towers each, with upgrade paths)
|
||||
- Full enemy roster (armored, fast, flying, boss types)
|
||||
- Map variety
|
||||
- Audio direction matching the visual direction (orchestral / industrial / hobbyist's-table ambient?)
|
||||
|
||||
### 3.4 Production guardrails carried from Phase 2
|
||||
|
||||
The asset manifest tracking GW-derived content must hit zero entries before any public build, public repo flip, or marketing material.
|
||||
### Open decisions (resolve as the relevant slice is scheduled)
|
||||
- **Paint reconciliation:** keep paint as a separate axis, fold it into the systemic-upgrade visuals, or retire it. (Currently paused.)
|
||||
- **Race vs Builder naming:** the code says "Race"; the design says "Builder." Rename, nest, or keep — decide before it calcifies.
|
||||
- **Gold-roll availability:** currently any time; may tighten to prep-only.
|
||||
|
||||
---
|
||||
|
||||
## Cross-phase principles preserved from existing context
|
||||
## Part B — Functional gameplay remainder (not roguelike-specific)
|
||||
|
||||
- **Debug to root cause.** No defensive workarounds, no per-frame state-correcting hacks. Find the actual cause and fix it correctly.
|
||||
- **Re-state design intent before coding.** Confirmed valuable across multiple sessions; continues into Phase 2 visual work where the same restate-before-build approach prevents asset rework.
|
||||
- **Server-authoritative gameplay; local-only UI/visual state.** Selection, animation state, ghost visuals all stay client-side. Only gameplay-meaningful state is networked.
|
||||
- **Existing engine and rendering decisions stand.** Unity 6.4 → next LTS migration plan; URP; IL2CPP; .NET Standard 2.1; Linear color space; new Input System; Force Text serialization.
|
||||
- **Tower Upgrade / Sell actions** — HUD buttons exist but are disabled. Sell first (refund + despawn), then per-tower upgrade if the systemic model leaves room for it.
|
||||
- **Enemy resistances / weaknesses** — `EnemyHealth.TakeDamage` has the stub slot; info panel has the placeholder.
|
||||
- **Terrain architecture decision** — Unity Terrain vs mesh vs ProBuilder. Builder code is terrain-agnostic; decide before heavy art.
|
||||
- **Camera polish** — cursor-anchored zoom near map edges, center-on-builder hotkey, race/phase-aware initial position.
|
||||
|
||||
---
|
||||
|
||||
## Part C — Near-term art & visual TODOs
|
||||
|
||||
These are concrete, scheduled-soon visual fixes (placeholders are otherwise point-isolated and swap cleanly):
|
||||
|
||||
- **Enemy idle animation support — NEW.** Add idle animation states to **all** enemy prefabs (enemies currently lack an idle; needed for held/spawned and between-waypoint states, and as the foundation for the stepped-animation direction below).
|
||||
- **Tower footprint visuals fill the full 2×2 — NEW.** Tower meshes (the new cones / placeholder shapes) sit centered and read smaller than the tile area they occupy. Update the visuals so a placed tower clearly fills its entire 2×2 footprint (matters for readability of maze packing and for the Wall especially).
|
||||
|
||||
---
|
||||
|
||||
## Part D — Visual prototype: painted-miniature aesthetic (deferred phase)
|
||||
|
||||
Begins after the roguelike core is solid. Proves out the "photographed painted tabletop miniature" + Spider-Verse stepped-animation look on minimal content, as a concrete artifact for a future art lead. **Placeholder hero content (Space Marine + Hormagaunt-style) is GW-adjacent and private-only — see Part E.**
|
||||
|
||||
**Shared infrastructure (one-time, reused by all future assets):**
|
||||
- **`MiniatureStandard` URP Shader Graph** — PBR + a **Fresnel rim** term (fakes drybrush edge highlights) and a **crushed-shadow** curve (studio-lit miniature look); optional low-intensity painted-noise overlay.
|
||||
- **Lighting/post** via the volume profile — warm key (~45°), cool fill, low bounce, slightly raised AO, **tilt-shift DOF** (diorama scale), saturated warm-shadow grade, slight vignette.
|
||||
- **`SteppedAnimator`** — samples the Animator pose and quantizes to ~12fps for **skeletal body + weapon-tracking only**; effects/projectiles/UI stay smooth (the contrast is what sells it). Builds on the enemy idle-animation work in Part C.
|
||||
- **`TowerVisualController`** — local-only bridge from `TowerCombat` events (`OnTargetAcquired`/`OnTargetLost`/`OnFire`, `CurrentTarget`) to Animator params + procedural turret rotation.
|
||||
|
||||
**Prototype content:** one hero tower (idle/aim/fire) and one enemy (walk/death, stepped), then a **demo scene** showing towers shooting enemies in the painted-miniature look. Exit criteria: the demo runs in-editor with the aesthetic working end-to-end and the shared infra reusable.
|
||||
|
||||
---
|
||||
|
||||
## Part E — Path to commercial production (notes only)
|
||||
|
||||
- **IP swap-out (required before any public release):** replace GW-adjacent placeholder content with original factions (the "armored warrior order" and "horde swarm" archetypes aren't owned; the specific GW execution is) — or pursue a GW license if the demo warrants. The data model already supports clean swap-out (asset-referenced definitions; code unaffected). The **asset manifest of IP-derived content must hit zero** before any public build, repo flip, or marketing.
|
||||
- **Steam lobby migration (deferred):** replace Direct-IP with a Steam-backed provider behind an `IConnectionProvider` abstraction — **`NetworkBootstrap` is the single seam**; lobby UI and gameplay don't change. Adds friend-invite via overlay, lobby browser, Steam P2P transport; upgrades host-leave handling (Option A → lobby persists / new host). Use Spacewar app ID 480 in dev. Keep Direct-IP for LAN / DRM-free builds.
|
||||
- **Production scope:** more towers/enemies/maps, audio direction matching the visual direction, art-lead hire (the Part D demo is the recruitment artifact).
|
||||
|
||||
---
|
||||
|
||||
## Cross-phase principles
|
||||
|
||||
- **Debug to root cause** — no defensive workarounds or per-frame correction hacks.
|
||||
- **Restate design intent before coding** — prevents rework; especially valuable for the roguelike systems and any art pipeline work.
|
||||
- **Server-authoritative gameplay; local-only UI/visual state.**
|
||||
- **Existing engine/rendering decisions stand:** Unity 6.4, URP, IL2CPP, .NET Standard 2.1, Linear color, new Input System, Force Text serialization, NGO 2.x.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue