Zombies/AstronautFx.cs

Static utility for Astronaut effects applied on the local player's machine. Implements feel effects for grab (daze) and headbutt (camera punch, random perk removal, health reduction) and logic to remove a random perk and adjust related player state.

Networking
using Sandbox;
using System;
using System.Linq;

namespace NZombies;

/// <summary>
/// The Astronaut's grab and headbutt, on the machine whose player they land on (2026-10-06): `NZNet.AstronautGrab` /
/// `NZNet.AstronautHeadbutt` go to everyone, and only the owner of that body acts — its movement and its health are its own
/// (the daze's reason, `SonicDaze`; the burn's reason, `MargwaBurn`: the host's copy of a client's health has never heard of
/// their Juggernog).
/// </summary>
public static class AstronautFx
{
	/// <summary>
	/// The grab: THIS machine's player dazed for <paramref name="seconds"/> — upstream's `NZAstroSlow(2)` (3 when he is
	/// angry), here the Shrieker's daze. ⚠️ UPSTREAM'S SLOW IS INERT: the status entity it creates is read by nothing in the
	/// gamemode, so in GMod the grab only made a noise. It is meant to hold you in reach of the headbutt, and here it does.
	/// </summary>
	public static void FeelGrab( Guid body, float seconds )
	{
		var me = NZPlayer.Local;
		if ( !me.IsValid() || me.GameObject.Id != body || me.IsDown || seconds <= 0f ) return;

		SonicDaze.Apply( me, seconds );
	}

	/// <summary>
	/// The headbutt landing on THIS machine's player: ONE PERK TAKEN, at random (the user, 2026-10-06: *"it must remove 1 random
	/// perk from the player when headbutting"* — BO1 Moon's astronaut steals one; upstream's port never got to it), then health
	/// down to <paramref name="floor"/> of the match's base health and no lower — upstream's `TakeDamage(Health() - 90)` against
	/// its base of 100, which can never kill — through armor, and a kick of the view (upstream's random `ViewPunch`).
	///
	/// ⚠️ ON THE VICTIM'S MACHINE BECAUSE PERKS LIVE THERE: `NZPlayer.Perks` is bought, read and lost locally and synced to no
	/// one, so the host's copy of a client could not take one away.
	/// </summary>
	public static void FeelHeadbutt( Guid body, float floor )
	{
		var me = NZPlayer.Local;
		if ( !me.IsValid() || me.GameObject.Id != body || me.IsDown ) return;

		CameraShake.Punch( me.WorldPosition, 0.9f, 64f );
		StealPerk( me );

		var hp = me.Hp;
		if ( !hp.IsValid() ) return;

		var keep = Math.Clamp( floor, 0.05f, 1f ) * Difficulty.MaxHealth;
		var dmg = hp.Current - keep;
		if ( dmg > 0.5f ) hp.Apply( dmg, false, null, ignoreArmor: true, source: "astronaut headbutt" );
	}

	/// <summary>
	/// Take one of this player's perks at random, with the same tidy-up a down does (`NZPlayer.LosePerksOnDown`): Juggernog's
	/// raised maximum put back, and Mule Kick's third gun trimmed (to Insurance's escrow when that is owned). Its augments stay,
	/// inert until it is bought again, as on a down. ⚠️ PERMA PERKS (the buyable ending's `PreventPerkLoss`) keep them all.
	/// </summary>
	static void StealPerk( NZPlayer me )
	{
		if ( me.PreventPerkLoss || me.Perks.Count == 0 ) return;

		var id = me.Perks[Game.Random.Int( 0, me.Perks.Count - 1 )];
		me.Perks.Remove( id );

		if ( id == "mulekick" && me.Inventory.IsValid() ) me.Inventory.TrimToCap();
		if ( id == "jugg" && me.Hp.IsValid() )
		{
			me.Hp.Max = Difficulty.MaxHealth;
			if ( me.Hp.Current > me.Hp.Max ) me.Hp.Reset( me.Hp.Max );
		}

		var name = PerkRegistry.All.FirstOrDefault( p => p.Id == id )?.Name ?? id;
		PowerupBannerState.Show( $"{name.ToUpperInvariant()} STOLEN" );
		NZSound.Play( "nz.astro.swell", me.WorldPosition + Vector3.Up * 50f );
		Log.Info( $"[nz-astro] the headbutt took {name} — {me.Perks.Count} perk(s) left" );
	}
}