// Assets/_Project/Scripts/Editor/Gameplay/EncounterGoldEntryDrawer.cs
using UnityEditor;
using UnityEngine;
using TD.Gameplay;
namespace TD.Editor.Gameplay
{
///
/// Custom property drawer for . 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.
///
///
/// Projections are estimates by necessity. 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
/// 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.
///
[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;
}
}