Component attached to a player that locally pulls that machine's owned player toward a given point while active. It stores pull center, radius, speed and timing, and on update applies periodic velocity to the player's physics body and lifts the player slightly off the ground while active.
using Sandbox;
using System;
namespace NZombies;
/// <summary>
/// A BOSS'S PULL ON THIS PLAYER — Oberon's black hole dragging them toward him for as long as it is open.
///
/// ⛔ ON THE MACHINE THAT OWNS THE BODY, AS THE DAZE IS (`SonicDaze`). The hole used to drag every player from the host's
/// `OberonBoss.TickPull`, writing `WorldPosition` and the body's velocity straight onto them — which moves the host's own
/// player and no one else's: a body another machine simulates is written back within a frame (INSTRUCTIONS.md, `IsProxy`).
/// So the host now says where and for how long (`NZNet.OberonPull`) and each machine pulls its own player, here. Solo is
/// the same pull as before, which only ever pulled the one player there was.
///
/// ⚠️ THE SAME MATH AS THE OLD LOOP, ten times a second: inside the reach and more than 64u from the middle, the body lifted
/// off the floor by `Placeable.GroundBreak` first — a grounded controller cancels velocity and re-snaps — then given the
/// pull's speed toward the middle. It removes itself when the pull closes.
/// </summary>
public sealed class BossPull : Component
{
Vector3 _at;
float _radius, _speed, _rate = 10f;
TimeUntil _from, _until, _next;
/// <summary>
/// Pull this machine's own player toward `at`, starting `delay` seconds from now and lasting `seconds`: `speed` a tick,
/// ten ticks a second, anyone inside `radius`. LOCAL — `NZNet.OberonPull`. A second pull replaces the first.
/// </summary>
public static void Apply( NZPlayer player, Vector3 at, float radius, float speed, float delay, float seconds )
{
if ( !player.IsValid() || player.IsProxy || seconds <= 0f ) return;
var p = player.Components.GetOrCreate<BossPull>();
p._at = at;
p._radius = MathF.Max( 1f, radius );
p._speed = speed;
p._from = MathF.Max( 0f, delay );
p._until = MathF.Max( 0f, delay ) + seconds;
p._next = 0f;
}
protected override void OnUpdate()
{
if ( _until <= 0f ) { Destroy(); return; }
if ( _from > 0f || !_next ) return;
_next = 1f / _rate;
var player = Components.Get<NZPlayer>( FindMode.EverythingInSelf );
if ( !player.IsValid() || player.IsProxy ) return;
var d = player.WorldPosition.Distance( _at );
if ( d > _radius || d < 64f ) return;
var c = player.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
var body = c.IsValid() ? c.Body : null;
if ( !body.IsValid() ) return;
var dir = (_at - player.WorldPosition).WithZ( 0f ).Normal;
player.WorldPosition += Vector3.Up * Placeable.GroundBreak;
body.Velocity += dir * _speed;
}
}