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