Client-side effect and component for the Mimic enemy's tentacle hold. MimicFx.Hold/Release are RPC entry points that attach or release the local NZPlayer; MimicGrab is a Component that prevents player movement, drags the player toward the Mimic's hold point, handles knife-cut freeing, plays gore sounds/particles, and restores controls when finished.
using Sandbox;
using System;
namespace NZombies;
/// <summary>
/// What the Mimic's tentacle does on a player's OWN machine (2026-10-07) — the Panzer claw's hold (`PanzerFx.Hold`, `PanzerGrab`),
/// its own count: the host decides the catch and the letting go (`NZNet.MimicHold` / `MimicRelease`), and the held player's
/// machine alone moves its body (`BossPull`'s reason: a body another machine simulates is written back within a frame).
/// </summary>
public static class MimicFx
{
/// <summary>
/// The tentacle has THIS machine's player (`NZNet.MimicHold`): no legs of their own, reeled to the Mimic <paramref name="mimic"/>'s
/// mouth and held there (`MimicGrab`, `MimicBoss.HoldPoint`) until it lets go, <paramref name="knives"/> knife swings cut it
/// (`MimicGrab.OnKnife`), or <paramref name="seconds"/> pass — a release lost on the way can hold nobody for good.
/// </summary>
public static void Hold( Guid body, Guid mimic, int knives, float seconds )
{
var me = NZPlayer.Local;
if ( !me.IsValid() || me.GameObject.Id != body || me.IsDown ) return;
var g = me.Components.GetOrCreate<MimicGrab>();
g.Begin( mimic, knives, seconds );
PowerupBannerState.Show( $"KNIFE THE TENTACLE! 0/{g.Needed}" );
}
/// <summary>
/// Let THIS machine's player go (`NZNet.MimicRelease`) and throw them by <paramref name="toss"/>, added to their speed (upstream's
/// `SetVelocity`; zero, no throw). Nothing when they already cut themselves loose.
/// </summary>
public static void Release( Guid body, Vector3 toss )
{
var me = NZPlayer.Local;
if ( !me.IsValid() || me.GameObject.Id != body ) return;
var g = me.Components.Get<MimicGrab>( FindMode.EverythingInSelf );
if ( !g.IsValid() || g.Done ) return;
g.End();
if ( me.IsDown || toss.Length < 1f ) return;
var c = me.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
var b = c.IsValid() ? c.Body : null;
if ( !b.IsValid() ) return;
// ⚠️ LIFTED OFF THE FLOOR FIRST, or a grounded controller cancels the throw and re-snaps (`AvogadroFx.LandShock`)
me.WorldPosition += Vector3.Up * Placeable.GroundBreak;
b.Velocity += toss;
ShoveGuard.Of( me )?.NoteLaunch();
}
}
/// <summary>
/// The tentacle's hold on this machine's own player (`MimicFx.Hold`), `PanzerGrab`'s mechanism: no movement of your own (look,
/// shoot and knife stay — the knife is the way out: <see cref="Needed"/> swings, <see cref="OnKnife"/>), dragged along the floor
/// to where the tentacle holds you (`MimicBoss.HoldPoint`: under its tip, out where it caught you, then reeled in to its mouth)
/// and kept there, your view pulled to its jaw (upstream's, `MimicBoss.ViewPull`), until it lets go, you cut it, you or it go
/// down, or its ceiling runs out. Removes itself, and gives your legs back, when it ends. `NZPlayers` leaves the legs off while
/// it exists.
/// </summary>
public sealed class MimicGrab : Component
{
/// <summary>The Mimic holding you, by network id — every machine has it.</summary>
public Guid MimicId { get; set; }
/// <summary>Knife swings that cut you free (`MimicBoss.KnifeToFree`, sent by the host).</summary>
public int Needed { get; private set; } = 2;
/// <summary>Over — cut, let go, or ended by a down or its death — and destroying itself.</summary>
public bool Done => _done;
/// <summary>The fastest it reels you in, and the spring that settles you once there.</summary>
const float Reel = 900f, Settle = 10f;
float _until, _nextLift;
bool _tookInput, _done;
int _swings;
MimicBoss _mimic;
/// <summary>A (new) catch: whose tentacle, how many swings get you out, and the most it can last.</summary>
public void Begin( Guid mimic, int needed, float seconds )
{
MimicId = mimic;
_mimic = null;
Needed = Math.Max( 1, needed );
_swings = 0;
_until = Time.Now + MathF.Max( 0.5f, seconds );
}
/// <summary>Over, at once: no drag after this (a throw lands on the body next), and destroyed.</summary>
public void End()
{
if ( _done ) return;
_done = true;
Destroy();
}
/// <summary>
/// A knife swing of yours while it holds you (`Knife.Swing` — hit or miss): a wet cut and blood at the tentacle and the count
/// on screen; the last one lets you go at once and tells the host (`NZNet.MimicCut`), which lets go for everyone — its release
/// finds nothing left to undo here.
/// </summary>
public void OnKnife()
{
if ( _done ) return;
var me = Components.Get<NZPlayer>( FindMode.EverythingInSelf );
if ( !me.IsValid() || me.IsDown ) return;
_swings++;
var grip = me.WorldPosition + Vector3.Up * 40f; // where the tentacle has you
NZSound.Play( NZSound.ZombieGoreLimb, grip );
MimicBoss.Blood( grip, 10, 110f, 1f );
if ( _swings < Needed )
{
PowerupBannerState.Show( $"KNIFE THE TENTACLE! {_swings}/{Needed}" );
return;
}
// ⚠️ OVER BEFORE THE HOST HEARS IT: a held host runs the cut — and the release it sends back — inside this call
_done = true;
PowerupBannerState.Show( "FREE" );
Log.Info( $"[nz-mimic] knifed the tentacle {_swings} times — free" );
Destroy();
NZNet.MimicCut();
}
MimicBoss Mimic()
{
if ( _mimic.IsValid() ) return _mimic;
var go = Scene.Directory.FindByGuid( MimicId );
_mimic = go.IsValid() ? go.Components.Get<MimicBoss>( FindMode.EverythingInSelfAndDescendants ) : null;
return _mimic;
}
// ⚠️ BOTH: whichever runs last before the physics step, the drag's velocity is in it (`PanzerGrab`'s)
protected override void OnUpdate()
{
Drag();
Look();
}
protected override void OnFixedUpdate() => Drag();
protected override void OnDestroy()
{
if ( !_tookInput ) return;
var c = Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
var me = Components.Get<NZPlayer>( FindMode.EverythingInSelf );
if ( c.IsValid() && !(me.IsValid() && me.IsOutOfRound) ) c.UseInputControls = true;
}
void Drag()
{
if ( _done ) return;
var me = Components.Get<NZPlayer>( FindMode.EverythingInSelf );
var mimic = Mimic();
if ( !me.IsValid() || me.IsDown || !mimic.IsValid() || mimic.Dead || Time.Now >= _until )
{
End();
return;
}
var c = me.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
var body = c.IsValid() ? c.Body : null;
if ( !body.IsValid() ) return;
// ⛔ NO RESISTING: no movement input while it has you — re-asserted every frame in case anything else hands it back
if ( c.UseInputControls ) { c.UseInputControls = false; _tookInput = true; }
c.WishVelocity = Vector3.Zero;
// ⛔ ALONG THE FLOOR, NEVER UP (the Panzer claw's 2026-10-06 lesson): flat toward where the tentacle holds you, fast while
// far, then a spring; lifted off the floor only while dragged, at most ten times a second, so a grounded controller does
// not cancel it
var to = (mimic.HoldPoint - me.WorldPosition).WithZ( 0f );
var d = to.Length;
if ( d > 30f && c.IsOnGround && Time.Now >= _nextLift )
{
_nextLift = Time.Now + 0.1f;
me.WorldPosition += Vector3.Up * Placeable.GroundBreak;
}
var speed = d > 60f ? Reel : MathF.Min( Reel, d * Settle );
var flat = d > 1f ? to / d * speed : Vector3.Zero;
body.Velocity = flat.WithZ( MathF.Min( body.Velocity.z, 0f ) );
ShoveGuard.Of( me )?.NoteLaunch();
}
/// <summary>Your view pulled to its mouth (upstream: half way to `jaw_fx_tag` every tick) — eased, so a flick still moves it.</summary>
void Look()
{
if ( _done || MimicBoss.ViewPull <= 0f ) return;
var me = Components.Get<NZPlayer>( FindMode.EverythingInSelf );
var mimic = Mimic();
if ( !me.IsValid() || !mimic.IsValid() ) return;
var c = me.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
if ( !c.IsValid() ) return;
var dir = mimic.Jaw - c.EyePosition;
if ( dir.Length < 1f ) return;
var a = Rotation.Slerp( c.EyeAngles.ToRotation(), Rotation.LookAt( dir.Normal ),
1f - MathF.Exp( -MimicBoss.ViewPull * Time.Delta ) ).Angles();
c.EyeAngles = new Angles( a.pitch, a.yaw, 0f );
}
}