First pass at full refactor to 2.0 design

Restructures the game around the cyclical run loop from Game Design Doc V2:
5 waves = a cycle, 3 cycles = a phase, each phase ends in a boss.

- New TD.Gameplay.Waves: WaveGroup / PhaseDefinition / RunDefinition author the
  run as draggable weighted pools; RunState owns phase/cycle position, the drawn
  wave slots, and the per-slot enemy buff sets. WaveManager's flat wave array is
  gone -- it now only runs the encounter RunState points at.
- New TD.Gameplay.EnemyUpgrades: the post-wave enemy-buff vote, with public
  live-replicated ballots so the HUD can show who voted for what.
- Inter-wave flow is now strictly sequential: draft -> vote -> build, each stage
  ending early once every player has acted.
- Enemy abilities inverted from per-instance random rolls to deterministic,
  stacking per-wave-slot sets. Six cards ship: Split (reworked), Flight, Blink,
  No Bounty, Gold Theft, Double Up.
- Tower upgrades are a two-step tree: a draft pick unlocks a node, gold converts
  an already-placed tower in place.
- Boss encounters flag their enemies and drive a boss HP bar.
- Player cap reduced to 3 via MatchRules.MaxPlayers.
- GoldConfig is now keyed by global encounter number rather than wave index.

Compiles clean; NOT yet verified in-engine. Editor wiring still required --
see Docs/2.0_Setup_Checklist.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Matt F 2026-07-30 17:45:11 -07:00
parent 7e5c3a8279
commit 4892d7253d
64 changed files with 4023 additions and 344 deletions

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 75cc168bfd0b91b41a313a4389d31910
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,66 @@
// Assets/_Project/Scripts/Editor/Core/PoolWeightDrawer.cs
using UnityEditor;
using UnityEngine;
using TD.Core;
namespace TD.Editor.Core
{
/// <summary>
/// Draws a <see cref="PoolWeightAttribute"/> field as a 01 slider, with an inline warning
/// rendered directly beneath it whenever the weight is zero.
/// </summary>
/// <remarks>
/// Deliberately does not clamp, revert, or disable the field — a designer muting an entry
/// while tuning is normal. The drawer's only job is to make the consequence visible at the
/// point of editing, since a zero-weight entry fails silently at runtime.
/// </remarks>
[CustomPropertyDrawer(typeof(PoolWeightAttribute))]
public class PoolWeightDrawer : PropertyDrawer
{
// Two lines of text alongside the icon column need more than one line of box height.
private const float HelpBoxLines = 2.4f;
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float h = EditorGUIUtility.singleLineHeight;
if (IsZero(property))
h += EditorGUIUtility.standardVerticalSpacing + HelpBoxHeight();
return h;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.Float)
{
EditorGUI.LabelField(position, label.text, "[PoolWeight] only supports float fields.");
return;
}
var sliderRect = new Rect(position.x, position.y, position.width,
EditorGUIUtility.singleLineHeight);
EditorGUI.Slider(sliderRect, property, 0f, 1f, label);
if (!IsZero(property)) return;
var attr = (PoolWeightAttribute)attribute;
var boxRect = new Rect(
position.x,
position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing,
position.width,
HelpBoxHeight());
EditorGUI.HelpBox(
boxRect,
$"Weight is 0 — this {attr.Noun} can never be drawn and will not appear in game. " +
$"Raise it above 0 to put it back in the pool.",
MessageType.Warning);
}
private static bool IsZero(SerializedProperty property)
=> property.propertyType == SerializedPropertyType.Float && property.floatValue <= 0f;
private static float HelpBoxHeight() => EditorGUIUtility.singleLineHeight * HelpBoxLines;
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8ba57381fc7e6a3479e59d84e45720d9

View file

@ -0,0 +1,69 @@
// Assets/_Project/Scripts/Editor/Gameplay/RunDefinitionEditor.cs
using UnityEditor;
using UnityEngine;
using TD.Gameplay.Waves;
namespace TD.Editor.Gameplay
{
/// <summary>
/// Inspector for <see cref="RunDefinition"/>. Draws the normal fields, then a live
/// validation panel reporting whether the authored pools can actually produce a run.
/// </summary>
/// <remarks>
/// The check that matters here is the distinct-enemy-type one: because enemy upgrades are
/// keyed to a wave slot, a phase must be able to fill its cycle with waves that use different
/// enemies. A pool can look healthy (plenty of entries) and still fail that. Reporting it in
/// the inspector turns a startup error into something a designer sees while authoring.
/// </remarks>
[CustomEditor(typeof(RunDefinition))]
public class RunDefinitionEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
var run = (RunDefinition)target;
EditorGUILayout.Space();
EditorGUILayout.LabelField("Validation", EditorStyles.boldLabel);
bool ok = run.Validate(out string report);
EditorGUILayout.HelpBox(report, ok ? MessageType.Info : MessageType.Error);
// Per-phase capacity readout — the fastest way to see how much headroom a pool has
// before the distinct-type requirement starts biting.
if (run.Phases != null)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Phase capacity", EditorStyles.boldLabel);
for (int i = 0; i < run.Phases.Length; i++)
{
var phase = run.Phases[i];
if (phase == null)
{
EditorGUILayout.LabelField($"Phase {i + 1}", "— empty —");
continue;
}
int distinct = phase.CountDistinctEnemyTypes();
string value = $"{distinct} distinct enemy type(s) / {run.WavesPerCycle} needed";
var style = new GUIStyle(EditorStyles.label)
{
normal = { textColor = distinct >= run.WavesPerCycle
? EditorStyles.label.normal.textColor
: new Color(0.9f, 0.4f, 0.3f) }
};
EditorGUILayout.LabelField(phase.Label, value, style);
}
}
EditorGUILayout.Space();
if (GUILayout.Button("Rebuild Wave Index"))
{
run.RebuildIndex();
Debug.Log($"[RunDefinition] Rebuilt wave index: {run.WaveCount} distinct wave(s).");
}
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5f5e8e0197efd154b92817bbb3028b35