Player/Salvage.cs

Static Salvage utility for NZombies. Provides methods to award, refund, gift, spend and reset a secondary in-game currency stored on NZPlayer, plus console commands to inspect and tune salvage settings.

NetworkingFile Access
using Sandbox;
using System;
using System.Linq;

namespace NZombies;

/// <summary>
/// Salvage — the second currency, earned from kills and spent at machines.
///
/// ⚠️ EARN-ONLY RIGHT NOW, DELIBERATELY. Nothing spends salvage: the Arsenal, the
/// weapon tech tree, the Gunsmith and perk augments are all still unbuilt, and each
/// is a sink on the roadmap (ARSENAL_REMAKE.md). It is built ahead of them because
/// the earn RATE is worth feeling before anything is priced against it — the
/// alternative is inventing prices against an income nobody has watched accumulate.
///
/// ⚠️ Separate from points on purpose. Points are spent constantly on doors, boxes
/// and perks; salvage is meant to accumulate across a whole game toward one or two
/// large upgrades. Sharing one currency would make every small purchase compete
/// with the long-term one.
///
/// ⛔ THE STATE LIVES ON NZPlayer, NOT HERE. SERVER_ROADMAP.md §4 rule 2 — a static
/// that holds something a player owns is a bug waiting for a second player. This
/// class is behaviour and tuning only.
/// </summary>
public static class Salvage
{
	static SalvageSettings Cfg => ActiveConfig.Salvage;

	/// <summary>
	/// Pay a player.
	///
	/// ⚠️ Returns what was actually added, so a caller can report the real figure
	/// rather than what it asked for. There is no cap today; if one is ever added
	/// this is the seam that already reports the shortfall.
	/// </summary>
	public static int Award( NZPlayer player, int amount )
	{
		if ( !player.IsValid() || !Cfg.Enabled || amount <= 0 ) return 0;

		player.Salvage += amount;
		return amount;
	}

	/// <summary>Award one pickup's worth — the drop path's entry point.</summary>
	public static int AwardPickup( NZPlayer player )
		=> Award( player, VultureAugments.SalvagePerPickup( player, Cfg.PerPickup ) );

	/// <summary>Can this player afford something. Here for the sinks that do not
	/// exist yet, so they do not each invent their own comparison.</summary>
	public static bool CanAfford( NZPlayer player, int cost )
		=> player.IsValid() && player.Salvage >= cost;

	/// <summary>
	/// Charge a player, refusing rather than going negative.
	///
	/// ⚠️ Nothing calls this yet. It exists so the first sink cannot get the
	/// afford-check-then-deduct pattern subtly wrong — the two halves belong in one
	/// place, the way the points economy funnels every purchase through one Buy.
	/// </summary>
	public static bool TrySpend( NZPlayer player, int cost )
	{
		if ( cost <= 0 ) return true;
		if ( !CanAfford( player, cost ) ) return false;

		player.Salvage -= cost;
		return true;
	}

	/// <summary>
	/// Give back salvage a player spent — half an augment's price, when it is taken off.
	///
	/// ⚠️ NOT GATED ON `Enabled`, UNLIKE <see cref="Award"/>. Enabled is whether a map PAYS salvage
	/// for kills; this is the player's own salvage coming back, and a map with salvage turned off
	/// must not keep it.
	/// </summary>
	public static int Refund( NZPlayer player, int amount )
	{
		if ( !player.IsValid() || amount <= 0 ) return 0;

		player.Salvage += amount;
		return amount;
	}

	/// <summary>
	/// A gift of salvage — basalt's Easter egg's 5,000. Returns what was added.
	///
	/// ⚠️ NOT GATED ON `Enabled`, like <see cref="Refund"/> and unlike <see cref="Award"/>: Enabled is whether a map PAYS salvage
	/// for kills, and a reward is not a kill's pay. OWNER — salvage lives on the owner's body and nothing relays it, so the
	/// Easter egg's gifts reach each machine's own player (`NZNet.BossFightRewards`).
	/// </summary>
	public static int Gift( NZPlayer player, int amount )
	{
		if ( !player.IsValid() || amount <= 0 ) return 0;

		player.Salvage += amount;
		return amount;
	}

	/// <summary>
	/// Zero it.
	///
	/// ⚠️ Salvage does NOT survive a game, matching the original, which resets it on
	/// OnGameBegin. It is a within-run currency like points, not a profile unlock.
	/// </summary>
	public static void Reset( NZPlayer player )
	{
		if ( !player.IsValid() ) return;

		player.Salvage = 0;
	}

	// ── commands ─────────────────────────────────────────────────────────────

	static NZPlayer Me()
		=> NZPlayer.Local;

	/// <summary>Read or grant salvage: nz_salvage [amount]</summary>
	[ConCmd( "nz_salvage" )]
	public static void Cmd( int amount = 0 )
	{
		var p = Me();
		if ( !p.IsValid() ) { Log.Warning( "[nz-salvage] no player" ); return; }

		if ( amount != 0 )
		{
			// Negative is allowed here on purpose — testing a sink that does not
			// exist yet means being able to take salvage away by hand.
			p.Salvage = Math.Max( 0, p.Salvage + amount );
			Log.Info( $"[nz-salvage] {(amount > 0 ? "+" : "")}{amount}" );
		}

		Log.Info( $"[nz-salvage] holding {p.Salvage:N0}"
			+ $" · {Cfg.PerPickup} per pickup"
			+ $" · drops {Cfg.DropChance * 100f:0.#}% normal"
			+ $" / {Cfg.DropChanceSpecial * 100f:0.#}% special"
			+ $" / {Cfg.DropChanceBoss * 100f:0.#}% boss"
			+ $" · {(Cfg.Enabled ? "enabled" : "DISABLED")}" );

		// ⛔ THIS SAID "nothing spends it yet — no Arsenal, tech tree, Gunsmith or augments" AND
		// EVERY ONE OF THOSE NOW EXISTS. The Arsenal sells rarity tiers, armor tiers and ammo mods,
		// the weapon tech tree sells nodes, and perk augments are priced in salvage — the console
		// shows lines like "bought revive/M2 — 1,500 salvage". A sink list that names four things
		// that were missing is worse than no list once they ship.
		Log.Info( "[nz-salvage]   spent on: Arsenal (rarity, armor, ammo mods),"
			+ " weapon tech nodes, and perk augments · `nz_rich` tops it up in Survival" );
	}

	/// <summary>Zero it: nz_salvage_reset</summary>
	[ConCmd( "nz_salvage_reset" )]
	public static void ResetCmd()
	{
		var p = Me();
		if ( !p.IsValid() ) { Log.Warning( "[nz-salvage] no player" ); return; }

		Reset( p );
		Log.Info( "[nz-salvage] reset to 0" );
	}

	/// <summary>Tune it live: nz_salvage_set [perPickup] [dropChance]</summary>
	[ConCmd( "nz_salvage_set" )]
	public static void SetCmd( int perPickup = -1, float dropChance = -1f )
	{
		if ( perPickup >= 0 ) Cfg.PerPickup = perPickup;
		if ( dropChance >= 0f ) Cfg.DropChance = MathX.Clamp( dropChance, 0f, 1f );

		ActiveConfig.NotifyChanged();

		Log.Info( $"[nz-salvage] {Cfg.PerPickup} per pickup"
			+ $" · {Cfg.DropChance * 100f:0.#}% drop chance" );
	}
}