Follow-up pass on the 2.0 refactor, closing the gap between "waves are drawn at random" and "difficulty still lives in the enemy assets". - Enemy health is no longer authored per enemy type. New EnemyScalingConfig scales a per-zone HP *budget* by encounter number; per-enemy health is that budget divided by the wave's enemy count. Scaling the total rather than the per-enemy value keeps enemy count a texture knob (few tanks vs many swarmers) instead of a second, uncontrolled difficulty axis. EnemyDefinition.MaxHp becomes HpMultiplier, a deviation around 1.0. Speed stays archetype-only and unscaled -- escalating it compounds with health and invalidates tower balance mid-run. - GoldConfig entries no longer reference a WaveDefinition. Payout is a property of run position, not of which wave got drawn: WaveGoldEntry -> EncounterGoldEntry, Waves -> Encounters, keyed by global encounter number. Its inspector labels elements "Encounter N" and projects cumulative earnings. - The wave's voted buffs now show as icon badges in the top bar, read from the same RunState slot the spawn path uses so the display can't drift from what the enemies actually carry. Hover names the buff; unillustrated cards fall back to a lettered badge rather than vanishing. - Clearing a phase's boss destroys every built tower, unrefunded. Queued build jobs still refund -- those towers were never delivered. Runs with the field empty, so the walkability churn doesn't hit the re-path scheduler. - Fixed an RPC codegen break: [ClientRpc] requires a ClientRpc suffix, unlike the newer [Rpc(SendTo...)] style this file doesn't use. - Setup checklist reordered into dependency order; it previously asked for a RunDefinition two sections before creating one. Also carries the editor-side asset reorganisation into Definitions/RunDefinitions and the sprite move into Enemy/Player draft icon folders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
122 lines
5.6 KiB
C#
122 lines
5.6 KiB
C#
// Assets/_Project/Scripts/Editor/Gameplay/EncounterGoldEntryDrawer.cs
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
using TD.Gameplay;
|
||
|
||
namespace TD.Editor.Gameplay
|
||
{
|
||
/// <summary>
|
||
/// Custom property drawer for <see cref="EncounterGoldEntry"/>. Labels each element by its
|
||
/// run position ("Encounter 3") rather than "Element 2", and renders a projected-earnings
|
||
/// line beneath the fields so the economy can be tuned without arithmetic.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// <b>Projections are estimates by necessity.</b> Kill income depends on how many enemies the
|
||
/// encounter spawns, and the wave filling each slot is drawn at runtime from a phase pool —
|
||
/// so the drawer multiplies against <see cref="GoldConfig.PreviewEnemiesPerEncounter"/>
|
||
/// instead. The cumulative figure is the number worth watching: it answers "how much could a
|
||
/// player have banked by this point in the run", which is what tower prices get balanced
|
||
/// against.
|
||
/// </remarks>
|
||
[CustomPropertyDrawer(typeof(EncounterGoldEntry))]
|
||
public class EncounterGoldEntryDrawer : PropertyDrawer
|
||
{
|
||
private const float PreviewLineExtraHeight = 4f;
|
||
|
||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||
{
|
||
if (!property.isExpanded) return EditorGUIUtility.singleLineHeight;
|
||
|
||
float h = EditorGUIUtility.singleLineHeight; // foldout
|
||
h += SpacedLineHeight() * 3; // the three gold fields
|
||
h += SpacedLineHeight() * 2 + PreviewLineExtraHeight; // projection + cumulative
|
||
return h;
|
||
}
|
||
|
||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||
{
|
||
int encounterNumber = EncounterNumberFrom(property);
|
||
|
||
// Re-label the foldout by run position. Element indices are 0-based and read as
|
||
// off-by-one against everything else in the run (RunState, the HUD, the checklist),
|
||
// which made miscounting the boss slot easy.
|
||
var header = new GUIContent(encounterNumber > 0
|
||
? $"Encounter {encounterNumber}"
|
||
: label.text);
|
||
|
||
Rect headerRect = new Rect(position.x, position.y, position.width,
|
||
EditorGUIUtility.singleLineHeight);
|
||
property.isExpanded = EditorGUI.Foldout(headerRect, property.isExpanded, header,
|
||
toggleOnLabelClick: true);
|
||
if (!property.isExpanded) return;
|
||
|
||
float y = position.y + SpacedLineHeight();
|
||
const float indent = 14f;
|
||
Rect Row()
|
||
{
|
||
Rect r = new Rect(position.x + indent, y, position.width - indent,
|
||
EditorGUIUtility.singleLineHeight);
|
||
y += SpacedLineHeight();
|
||
return r;
|
||
}
|
||
|
||
var perEnemyProp = property.FindPropertyRelative("GoldPerEnemy");
|
||
var completionProp = property.FindPropertyRelative("CompletionBonus");
|
||
var noLeaksProp = property.FindPropertyRelative("NoLeaksBonus");
|
||
|
||
EditorGUI.PropertyField(Row(), perEnemyProp);
|
||
EditorGUI.PropertyField(Row(), completionProp);
|
||
EditorGUI.PropertyField(Row(), noLeaksProp);
|
||
|
||
// Read values off the SerializedProperties rather than the backing object: edits
|
||
// aren't applied to the instance until ApplyModifiedProperties runs at end of frame,
|
||
// so the object would show stale numbers while the designer is still typing.
|
||
int perEnemy = perEnemyProp.intValue;
|
||
int completion = completionProp.intValue;
|
||
int noLeaks = noLeaksProp.intValue;
|
||
|
||
var config = property.serializedObject.targetObject as GoldConfig;
|
||
int assumed = config != null ? config.PreviewEnemiesPerEncounter : 0;
|
||
|
||
int projected = perEnemy * assumed + completion + noLeaks;
|
||
|
||
y += PreviewLineExtraHeight;
|
||
var style = new GUIStyle(EditorStyles.miniLabel) { fontStyle = FontStyle.Italic };
|
||
|
||
using (new EditorGUI.DisabledScope(true))
|
||
{
|
||
EditorGUI.LabelField(
|
||
Row(),
|
||
$"Projected: {projected} g ({perEnemy} × {assumed} enemies " +
|
||
$"+ {completion} clear + {noLeaks} no-leak)",
|
||
style);
|
||
|
||
// Cumulative only makes sense once we know where this entry sits in the run.
|
||
if (config != null && encounterNumber > 0)
|
||
{
|
||
EditorGUI.LabelField(
|
||
Row(),
|
||
$"Cumulative by here: {config.ProjectedCumulativeThrough(encounterNumber)} g " +
|
||
$"(incl. {config.StartingGold} starting)",
|
||
style);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Recovers the 1-based run position from a path like "Encounters.Array.data[2]".
|
||
// Returns 0 when the entry isn't being drawn as an array element.
|
||
private static int EncounterNumberFrom(SerializedProperty property)
|
||
{
|
||
string path = property.propertyPath;
|
||
int open = path.LastIndexOf('[');
|
||
int close = path.LastIndexOf(']');
|
||
if (open < 0 || close < open) return 0;
|
||
|
||
string inner = path.Substring(open + 1, close - open - 1);
|
||
return int.TryParse(inner, out int index) ? index + 1 : 0;
|
||
}
|
||
|
||
private static float SpacedLineHeight()
|
||
=> EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
|
||
}
|
||
}
|