Scale difficulty by run position; wipe towers at phase end
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>
This commit is contained in:
parent
4892d7253d
commit
16706a1ecf
154 changed files with 2608 additions and 287 deletions
|
|
@ -0,0 +1,122 @@
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 62f688cf14ba23b42bd0edd63a11ba62
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
// Assets/_Project/Scripts/Editor/Gameplay/WaveGoldEntryDrawer.cs
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using TD.Gameplay;
|
||||
|
||||
namespace TD.Editor.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom property drawer for <see cref="WaveGoldEntry"/>. Renders the standard fields
|
||||
/// followed by a read-only "Preview Total" line computed from the entry's values, so
|
||||
/// designers can see at a glance how much a single player could earn from a wave.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The preview includes the kill-reward portion only when the entry's <c>Wave</c>
|
||||
/// reference is assigned (it's needed to count enemies in that wave). When unset, the
|
||||
/// preview shows just <c>CompletionBonus + NoLeaksBonus</c> and labels itself so the
|
||||
/// designer knows to drop a WaveDefinition in for a complete number.
|
||||
/// </remarks>
|
||||
[CustomPropertyDrawer(typeof(WaveGoldEntry))]
|
||||
public class WaveGoldEntryDrawer : PropertyDrawer
|
||||
{
|
||||
private const float PreviewLineExtraHeight = 4f;
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
// Sum the children + one extra line for the preview label. We render children
|
||||
// ourselves rather than using EditorGUI.PropertyField default since we want a
|
||||
// foldout with our custom preview tacked onto the end.
|
||||
if (!property.isExpanded) return EditorGUIUtility.singleLineHeight;
|
||||
|
||||
float h = EditorGUIUtility.singleLineHeight; // foldout
|
||||
h += SpacedLineHeight() * 4; // Wave + GoldPerEnemy + Completion + NoLeaks
|
||||
h += SpacedLineHeight() + PreviewLineExtraHeight; // preview label
|
||||
return h;
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
// Foldout header.
|
||||
Rect headerRect = new Rect(position.x, position.y, position.width,
|
||||
EditorGUIUtility.singleLineHeight);
|
||||
property.isExpanded = EditorGUI.Foldout(headerRect, property.isExpanded, label,
|
||||
toggleOnLabelClick: true);
|
||||
if (!property.isExpanded) return;
|
||||
|
||||
float y = position.y + SpacedLineHeight();
|
||||
float indent = 14f;
|
||||
Rect Row()
|
||||
{
|
||||
Rect r = new Rect(position.x + indent, y, position.width - indent,
|
||||
EditorGUIUtility.singleLineHeight);
|
||||
y += SpacedLineHeight();
|
||||
return r;
|
||||
}
|
||||
|
||||
// Standard property fields.
|
||||
var waveProp = property.FindPropertyRelative("Wave");
|
||||
var perEnemyProp = property.FindPropertyRelative("GoldPerEnemy");
|
||||
var completionProp = property.FindPropertyRelative("CompletionBonus");
|
||||
var noLeaksProp = property.FindPropertyRelative("NoLeaksBonus");
|
||||
|
||||
EditorGUI.PropertyField(Row(), waveProp);
|
||||
EditorGUI.PropertyField(Row(), perEnemyProp);
|
||||
EditorGUI.PropertyField(Row(), completionProp);
|
||||
EditorGUI.PropertyField(Row(), noLeaksProp);
|
||||
|
||||
// Compute the preview total. We instantiate the same logic as the runtime
|
||||
// property — read the serialized values, count enemies in the optional Wave,
|
||||
// sum it all up. Done inline (rather than calling WaveGoldEntry.PreviewTotalGold
|
||||
// directly) because SerializedProperty edits aren't applied to the target object
|
||||
// until ApplyModifiedProperties runs at the end of the frame, so reading the
|
||||
// backing class instance here would show stale data while the user is typing.
|
||||
int perEnemy = perEnemyProp.intValue;
|
||||
int completion = completionProp.intValue;
|
||||
int noLeaks = noLeaksProp.intValue;
|
||||
var waveAsset = waveProp.objectReferenceValue as WaveDefinition;
|
||||
|
||||
int enemyCount = 0;
|
||||
if (waveAsset != null && waveAsset.Entries != null)
|
||||
{
|
||||
foreach (var e in waveAsset.Entries)
|
||||
{
|
||||
if (e.EnemyType != null && e.Count > 0)
|
||||
enemyCount += e.Count;
|
||||
}
|
||||
}
|
||||
|
||||
int total = perEnemy * enemyCount + completion + noLeaks;
|
||||
|
||||
// Preview line. Slightly dimmed style, italic, non-editable. Includes a
|
||||
// breakdown so the designer can see where the number came from.
|
||||
y += PreviewLineExtraHeight;
|
||||
Rect previewRect = new Rect(position.x + indent, y, position.width - indent,
|
||||
EditorGUIUtility.singleLineHeight);
|
||||
|
||||
string breakdown = waveAsset != null
|
||||
? $"Preview Total: {total} g ({perEnemy} × {enemyCount} enemies " +
|
||||
$"+ {completion} completion + {noLeaks} no-leak)"
|
||||
: $"Preview Total: {completion + noLeaks} g (bonuses only — assign Wave " +
|
||||
$"to include kill rewards)";
|
||||
|
||||
var prevStyle = new GUIStyle(EditorStyles.miniLabel) { fontStyle = FontStyle.Italic };
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUI.LabelField(previewRect, breakdown, prevStyle);
|
||||
}
|
||||
}
|
||||
|
||||
private static float SpacedLineHeight()
|
||||
=> EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: cc17000144b098340a210921594babca
|
||||
Loading…
Add table
Add a link
Reference in a new issue