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>
127 lines
5.9 KiB
C#
127 lines
5.9 KiB
C#
// Assets/_Project/Scripts/Gameplay/GoldConfig.cs
|
||
using System;
|
||
using UnityEngine;
|
||
using UnityEngine.Serialization;
|
||
|
||
namespace TD.Gameplay
|
||
{
|
||
/// <summary>
|
||
/// Gold rules for one encounter slot, addressed by position in the run rather than by
|
||
/// which wave happens to occupy it.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// <b>Position, not identity.</b> Under the 2.0 run structure the waves filling each slot are
|
||
/// drawn at random from a phase pool, so "the gold for wave 3" means the run's third
|
||
/// encounter, whatever spawns in it. Payouts are a property of how far the players have
|
||
/// progressed — not of which enemy they happened to draw.
|
||
/// </remarks>
|
||
[Serializable]
|
||
public class EncounterGoldEntry
|
||
{
|
||
[Tooltip("Gold awarded to the killing player per enemy slain during this encounter. " +
|
||
"Applies uniformly to every enemy type.")]
|
||
public int GoldPerEnemy = 10;
|
||
|
||
[Tooltip("Flat bonus gold awarded to every active player when this encounter is fully " +
|
||
"cleared (all enemies dead or reached the goal).")]
|
||
public int CompletionBonus = 50;
|
||
|
||
[Tooltip("Extra bonus gold awarded to a player who cleared this encounter without any " +
|
||
"enemy from their own zone escaping their leak volume. Stacks on top of " +
|
||
"CompletionBonus.")]
|
||
public int NoLeaksBonus = 50;
|
||
|
||
/// <summary>
|
||
/// Guaranteed gold for a player who clears this encounter: the two bonuses, with no
|
||
/// kill income counted.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Kill income can't be known here — the wave occupying this slot is drawn at runtime, so
|
||
/// its enemy count varies per run. <see cref="GoldConfig.PreviewEnemiesPerEncounter"/>
|
||
/// supplies an estimate for the inspector's projected totals.
|
||
/// </remarks>
|
||
public int GuaranteedBonusGold => CompletionBonus + NoLeaksBonus;
|
||
|
||
/// <summary>
|
||
/// Projected best-case earnings for one player this encounter, given an assumed enemy
|
||
/// count. Editor tuning aid only — nothing at runtime reads it.
|
||
/// </summary>
|
||
public int ProjectedTotal(int assumedEnemyCount)
|
||
=> GoldPerEnemy * Mathf.Max(0, assumedEnemyCount) + GuaranteedBonusGold;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Single source of truth for every gold-related tunable in the game.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// <b>Wiring.</b> Assign one GoldConfig asset to <c>WaveManager.goldConfig</c> in the Match
|
||
/// scene. WaveManager seeds per-player starting gold from <see cref="StartingGold"/>, reads
|
||
/// <see cref="EncounterGoldEntry.GoldPerEnemy"/> for kill rewards, and awards the completion
|
||
/// and no-leak bonuses as each encounter clears.
|
||
///
|
||
/// <para><b>No per-enemy-type bounties.</b> Every enemy killed in a given encounter grants the
|
||
/// same reward regardless of <see cref="EnemyDefinition"/>. With enemy types now drawn at
|
||
/// random per phase, per-type bounties would make income depend on the luck of the draw
|
||
/// rather than on progression.</para>
|
||
/// </remarks>
|
||
[CreateAssetMenu(fileName = "GoldConfig", menuName = "TD/Gold Config", order = 2)]
|
||
public class GoldConfig : ScriptableObject
|
||
{
|
||
[Tooltip("Gold each player starts the match with. Same for every player.")]
|
||
public int StartingGold = 100;
|
||
|
||
[Tooltip("Gold rules per encounter, in run order. Element 0 = the run's first encounter. " +
|
||
"Encounters are counted continuously — cycles do NOT restart the count — so one " +
|
||
"phase of 5 waves × 3 cycles + a boss needs 16 entries. Missing entries pay " +
|
||
"zero for that encounter.")]
|
||
[FormerlySerializedAs("Waves")]
|
||
public EncounterGoldEntry[] Encounters;
|
||
|
||
[Header("Editor preview")]
|
||
[Tooltip("Assumed enemies per encounter, used ONLY to project totals in this inspector. " +
|
||
"Nothing at runtime reads it — the real count comes from whichever wave the run " +
|
||
"draws into each slot.")]
|
||
[Min(0)]
|
||
public int PreviewEnemiesPerEncounter = 40;
|
||
|
||
/// <summary>
|
||
/// Returns the gold rules for the given 1-based encounter number
|
||
/// (<c>RunState.GlobalEncounterNumber</c>), or null if out of range.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Keyed by <b>run position</b>, not by wave slot within a cycle. The same five waves come
|
||
/// round three times per phase, so a slot-keyed table would pay the same on cycle 3 as on
|
||
/// cycle 1 while the enemies grew steadily stronger from the buffs voted onto them. A
|
||
/// run-long counter lets payouts climb with the difficulty.
|
||
/// </remarks>
|
||
public EncounterGoldEntry GetEncounterEntry(int encounterNumber)
|
||
{
|
||
if (Encounters == null) return null;
|
||
int index = encounterNumber - 1;
|
||
if (index < 0 || index >= Encounters.Length) return null;
|
||
return Encounters[index];
|
||
}
|
||
|
||
/// <summary>Number of authored encounter entries.</summary>
|
||
public int EncounterCount => Encounters?.Length ?? 0;
|
||
|
||
/// <summary>
|
||
/// Editor tuning aid: cumulative projected earnings for one player from the run's first
|
||
/// encounter through <paramref name="encounterNumber"/> inclusive, assuming
|
||
/// <see cref="PreviewEnemiesPerEncounter"/> kills each time and no leaks.
|
||
/// </summary>
|
||
public int ProjectedCumulativeThrough(int encounterNumber)
|
||
{
|
||
if (Encounters == null) return StartingGold;
|
||
|
||
int total = StartingGold;
|
||
int last = Mathf.Min(encounterNumber, Encounters.Length);
|
||
for (int i = 0; i < last; i++)
|
||
{
|
||
if (Encounters[i] == null) continue;
|
||
total += Encounters[i].ProjectedTotal(PreviewEnemiesPerEncounter);
|
||
}
|
||
return total;
|
||
}
|
||
}
|
||
}
|