// 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; } }