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.
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" );
}
}