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:
parent
7e5c3a8279
commit
4892d7253d
64 changed files with 4023 additions and 344 deletions
8
Assets/_Project/Scripts/Editor/Core.meta
Normal file
8
Assets/_Project/Scripts/Editor/Core.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 75cc168bfd0b91b41a313a4389d31910
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
66
Assets/_Project/Scripts/Editor/Core/PoolWeightDrawer.cs
Normal file
66
Assets/_Project/Scripts/Editor/Core/PoolWeightDrawer.cs
Normal 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 0–1 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8ba57381fc7e6a3479e59d84e45720d9
|
||||
|
|
@ -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).");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5f5e8e0197efd154b92817bbb3028b35
|
||||
Loading…
Add table
Add a link
Reference in a new issue