Transport/Teleporter.cs

A Teleporter component that implements a standing teleporter pad. It tracks availability, warmup/transit phases, latches riders (players) on the host, notifies clients via NZNet messages, freezes local player input and invulnerability during transit, and places riders at the destination with a spread and cooldown handling.

NetworkingFile Access
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace NZombies;

/// <summary>
/// A standing teleporter pad — the thing the player walks onto and presses E at.
///
/// ⛔ TAKES EVERYONE ON THE PAD, not just whoever pressed the key. That is the point of the pad
/// having a size at all: the group steps on together and travels together. A teleporter that moved
/// only the presser would split the team at the exact moment they chose to regroup.
///
/// ⚠️ ONE-WAY. Standing at B does nothing — a return trip is a second teleporter pointing back.
/// See TeleporterSpot for why.
///
/// ⛔ THE HOST RUNS IT; EVERY MACHINE PLAYS IT, AND MOVES ONLY ITS OWN PLAYER. See "the network" below.
/// </summary>
public sealed class Teleporter : Component
{
	public static readonly List<Teleporter> All = new();

	protected override void OnEnabled() => All.Add( this );
	protected override void OnDisabled() => All.Remove( this );

	/// <summary>The authored settings this pad was built from.</summary>
	[Property] public TeleporterSpot Spot { get; set; }

	/// <summary>Its index in the config, so messages can name it.</summary>
	[Property] public int Index { get; set; }

	/// <summary>
	/// How far from the pad's centre the key still reaches.
	///
	/// ⚠️ Derived from the pad rather than a constant, so a wide pad is usable from its edge. The
	/// margin is there because the player's ORIGIN is at their feet and they can stand with half
	/// a boot over the lip.
	/// </summary>
	public float UseRange => MathF.Max( 48f, (Spot?.PadSize ?? 72f) * 0.75f );

	/// <summary>What it costs per trip. 0 = free.</summary>
	public int Price => Spot?.Price ?? 0;

	/// <summary>
	/// When this pad is usable again.
	///
	/// ⛔ RUNTIME STATE, AND IT LIVES HERE. The spot is authored data that survives a round; a
	/// countdown written into it would be saved into the map and would come back mid-recharge on
	/// the next load. Same split WunderfizzSpot documents.
	/// </summary>
	float _readyAt;

	/// <summary>Seconds left before it can be used again, or 0 when it is ready.</summary>
	public float CoolingFor => MathF.Max( 0f, _readyAt - Time.Now );

	/// <summary>
	/// Why this pad will not work, or "" when it will.
	///
	/// ⚠️ The gates that depend on the WORLD only — power and flag. Affording it is a refusal with
	/// a price attached and belongs in <see cref="Use"/>.
	/// </summary>
	public string Unavailable( NZPlayer player )
	{
		if ( Spot is null ) return "";

		if ( Spot.RequiresPower && !Power.IsOn )
			return "Teleporter — needs power";

		if ( !DoorLinks.IsOpen( Spot.Link ) )
			return "Teleporter — locked";

		// ⚠️ CHECKED LAST of the world gates, so an unpowered pad says "needs power" rather than
		// counting down at a player who cannot use it either way.
		var left = CoolingFor;
		if ( left > 0f )
			return $"Teleporter — recharging ({left:0}s)";

		// ⛔ MID-SEQUENCE IS A REFUSAL, NOT A SECOND TRIP. Without this a second press during the
		// warmup would reset _phaseEnds and charge again — the pad would never fire while anyone
		// kept pressing, and every press would take their points.
		if ( _phase == Phase.Warmup )
			return $"Teleporter — leaving in {PhaseLeft:0.#}s";

		if ( _phase == Phase.Transit )
			return "Teleporter — in transit";

		return "";
	}

	/// <summary>
	/// Is this player standing on the pad?
	///
	/// ⚠️ A SQUARE TEST IN THE PAD'S OWN SPACE, and DELIBERATELY GENEROUS. Der Riese's pad is
	/// ROUND, so this square is the one that encloses it: everyone visibly standing on the pad
	/// passes, plus a little margin past its corners. Erring outward is the right direction here —
	/// the player pressing E wanted to bring the team, and someone left behind by a rule they
	/// cannot see reads as the teleporter being broken.
	///
	/// ⚠️ A radius test would be exact for the round model and WRONG for the placeholder box,
	/// which really is square. One test that is slightly loose for both beats two that disagree.
	///
	/// ⚠️ The Z window is generous upward (a player's own height) and tight downward, so someone
	/// on a walkway above the pad does not get dragged along.
	/// </summary>
	public bool IsOn( NZPlayer player )
	{
		if ( !player.IsValid() || Spot is null ) return false;

		var local = Rotation.FromYaw( Spot.Yaw ).Inverse * (player.WorldPosition - Spot.A);
		var half = MathF.Max( 8f, Spot.PadSize ) * 0.5f;

		if ( MathF.Abs( local.x ) > half || MathF.Abs( local.y ) > half ) return false;

		return local.z > -Spot.PadHeight - 8f && local.z < 80f;
	}

	/// <summary>Everyone currently standing on this pad.</summary>
	public List<NZPlayer> Riders()
		=> Scene.GetAllComponents<NZPlayer>().Where( IsOn ).ToList();

	/// <summary>
	/// Send everyone on the pad to B. Returns what to tell the player who pressed the key.
	///
	/// ⚠️ EVERY REFUSAL IS A SENTENCE, not a silent false — the same rule the perk machines and
	/// the Wunderfizz are written to.
	/// </summary>
	public string Use( NZPlayer player )
	{
		if ( !player.IsValid() || Spot is null ) return "";

		var blocked = Unavailable( player );
		if ( !string.IsNullOrEmpty( blocked ) ) return blocked;

		// ⚠️ A CLIENT'S PRESS IS STILL ON ITS WAY TO THE HOST — the pad has not been told it is warming up yet — and a second
		// press inside that trip would pay twice for one ride
		if ( NZGame.IsClient && _askedAt > 0f && Time.Now - _askedAt < AskGrace )
			return "Teleporter — waiting for the host";

		// ⛔ CHARGED ONCE, TO THE PRESSER, AND CHARGED NOW. Charging every rider would make a
		// group trip cost four times as much for no stated reason, and charging nobody makes the
		// price setting a lie. Upstream also takes the points at the press rather than at
		// departure, so the warmup is committed the moment it starts.
		var price = Price;
		if ( price > 0 )
		{
			if ( player.Points < price )
				return $"The teleporter costs {price} — you need {price - player.Points} more";

			if ( !player.TrySpend( price ) )
				return "Purchase failed";
		}

		// ⛔ THE HOST RUNS THE PAD. A client has paid — points are each player's own (`NZNet.PointsAre`), as a door's are — and
		// asks; the host starts it for everyone, or gives the points back if it no longer can (`HostAsk`).
		if ( NZGame.IsClient )
		{
			_askedAt = Time.Now;
			NZNet.TeleporterAsk( Index, price );
		}
		else StartWarmup( player );

		return $"Teleporter charging{(price > 0 ? $" — {price} spent" : "")}"
			+ $"  ·  leaves in {Spot.WarmupTime:0.#}s";
	}

	// ── the sequence ────────────────────────
	//
	// ⛔ A PHASE MACHINE, NOT A CHAIN OF TIMERS. Upstream uses `timer.Create` and then spends
	// the rest of the file re-checking `IsValid(self)` inside every callback, because a pad can be
	// removed while its timers are pending — and TeleporterManager.Rebuild destroys every pad on
	// any config change. A phase read from OnUpdate simply stops existing with the component.

	enum Phase { Idle, Warmup, Transit }

	Phase _phase = Phase.Idle;
	float _phaseEnds;
	bool _warmupFired;
	NZPlayer _presser;

	/// <summary>When this machine last asked the host to run the pad — a client, until the host's warmup arrives.</summary>
	float _askedAt;

	/// <summary>How many ride this trip, on every machine. This machine's own riders are <see cref="_riding"/>.</summary>
	int _aboard;

	/// <summary>How long a client's press waits for the host's answer before another press may pay again.</summary>
	const float AskGrace = 1.5f;

	/// <summary>
	/// How far past its end a client lets a phase run when the host's next word does not come — the host's pad rebuilt
	/// mid-trip, or the message lost — before finishing it on its own, so nobody is left frozen in transit.
	/// </summary>
	const float LateGrace = 3f;

	/// <summary>Who is mid-transit, and what their flags were before we changed them.</summary>
	readonly List<Rider> _riding = new();

	/// <summary>
	/// One rider's prior state.
	///
	/// ⛔ THE PREVIOUS VALUES ARE STORED, NOT ASSUMED. Restoring `UseInputControls = true` and
	/// `Invulnerable = false` blindly would hand movement back to a player something else had
	/// frozen, and would strip the dev menu's godmode off anyone who rode a pad with it on.
	///
	/// ⚠️ AND THEIR PLACE IN THE LINE, which fans them out at B — the host's order, so every machine puts its own rider in the
	/// right place and nobody lands inside anybody.
	/// </summary>
	readonly record struct Rider( NZPlayer Player, bool HadInput, bool WasInvulnerable, int Slot );

	/// <summary>
	/// EVERY BODY ON A TELEPORT, AS THE HOST SEES IT, AND UNTIL WHEN. A hit on one lands nowhere (`Health.Apply`), whoever's
	/// machine drives it — *"fix it so clients also dont get hit mid teleport"* (the co-op audit, 2026-09-27). Each machine
	/// makes only its own riders invulnerable (`Board`), so a zombie that reached a CLIENT's rider on the pad hit it on the
	/// host: the swing sounded, and the cursed flame it carried went out. Kept on the host: a pad's riders from departure to a
	/// moment after arrival (`TransitGrace`, for the owner's move to land), the blue altar's through its passage.
	/// </summary>
	static readonly Dictionary<GameObject, float> _transitUntil = new();

	/// <summary>How long after the trip a rider stays untouchable, for their own machine's move to reach the host.</summary>
	public const float TransitGrace = 0.75f;

	/// <summary>Is this body on a teleport now, as the host sees it? False everywhere else.</summary>
	public static bool InTransit( GameObject body )
		=> body.IsValid() && _transitUntil.TryGetValue( body, out var until ) && Time.Now < until;

	/// <summary>This body rides, for this long. HOST.</summary>
	public static void GuardTransit( GameObject body, float seconds )
	{
		if ( !body.IsValid() ) return;

		foreach ( var stale in _transitUntil.Where( kv => !kv.Key.IsValid() || Time.Now >= kv.Value ).Select( kv => kv.Key ).ToList() )
			_transitUntil.Remove( stale );

		_transitUntil[body] = Time.Now + seconds;
	}

	/// <summary>Is the pad mid-sequence? Blocks a second press and changes the prompt.</summary>
	public bool Busy => _phase != Phase.Idle;

	/// <summary>Seconds left in the current phase, or 0 when idle.</summary>
	public float PhaseLeft => _phase == Phase.Idle ? 0f : MathF.Max( 0f, _phaseEnds - Time.Now );

	/// <summary>How many are aboard right now, on any machine. 0 unless mid-transit.</summary>
	public int Aboard => _aboard;

	protected override void OnUpdate()
	{
		if ( Spot is null || _phase == Phase.Idle ) return;

		if ( _phase == Phase.Warmup )
		{
			// ⚠️ AT T-1, WHICH IS UPSTREAM'S OWN OFFSET (`timer.Simple(TeleporterTime - 1, ...)`).
			// The warmup cue and the departure portal are a one-second tell that it is about to
			// fire, not an acknowledgement of the press — those are different things, and the
			// second one is what gives a teammate time to jump on.
			if ( !_warmupFired && PhaseLeft <= 1f )
			{
				_warmupFired = true;
				NZSound.Play( NZSound.TeleporterWarmup, Spot.A );
				TeleportPortal.Departure( Spot.A );
			}

			// ⛔ ONLY THE HOST LATCHES THE RIDERS — a client is told who rides (`ApplyDepart`). One that never hears lets the
			// warmup go, rather than hold the pad at "leaving in 0s" for good.
			if ( NZGame.IsHost )
			{
				if ( PhaseLeft <= 0f ) BeginTransit();
			}
			else if ( Time.Now - _phaseEnds > LateGrace )
			{
				Log.Warning( $"[nz-tp] #{Index}: the host never said who rides — the warmup lapses" );
				_phase = Phase.Idle;
			}

			return;
		}

		// ⛔ AND ONLY THE HOST ENDS THE TRIP — a client is told (`ApplyArrive`). One that never hears arrives on its own, rather
		// than leave its player frozen in transit.
		if ( NZGame.IsHost )
		{
			if ( PhaseLeft <= 0f ) FinishTransit();
		}
		else if ( Time.Now - _phaseEnds > LateGrace )
		{
			Log.Warning( $"[nz-tp] #{Index}: the host never said the trip was over — arriving anyway" );
			Arrive();
		}
	}

	/// <summary>
	/// Latch the riders and take them out of the world for the transit.
	///
	/// ⛔ RIDERS ARE LATCHED HERE, NOT AT THE PRESS, matching upstream — it calls
	/// `ents.FindInSphere` inside `Teleport()`, which runs after the warmup. So the warmup is a
	/// window to pile on, and someone who steps off before it ends does not travel.
	/// </summary>
	void BeginTransit()
	{
		var riders = Riders();

		// ⚠️ The presser rides even if the stand-on test just missed them — they paid and
		// pressed at this pad, so charging them and leaving them behind is the one outcome nobody
		// would accept. They do NOT ride if they walked off the pad entirely.
		if ( _presser.IsValid() && !riders.Contains( _presser ) && IsOn( _presser ) )
			riders.Add( _presser );

		var transit = MathF.Max( 0.2f, Spot.TransitTime );
		var ids = new List<string>();
		_riding.Clear();

		foreach ( var r in riders )
		{
			if ( !r.IsValid() ) continue;

			// every rider, this machine's or not: untouchable here until a moment after the arrival (`InTransit`)
			GuardTransit( r.GameObject, transit + TransitGrace );

			// ⛔ ONLY A BODY THIS MACHINE DRIVES IS FROZEN HERE. Anyone else's is simulated on their own machine, which undoes a
			// freeze or a move written here within a frame — they are named in the departure, and their machine boards them.
			if ( PlayerPresence.Mine( r.GameObject ) ) Board( r, ids.Count, transit );
			ids.Add( NZPlayers.OwnerOf( r.GameObject ) );
		}

		_aboard = ids.Count;
		_phase = Phase.Transit;
		_phaseEnds = Time.Now + transit;

		// ⚠️ IN THE ORDER THEY WERE FOUND, which is the order they fan out at B
		NZNet.TeleporterDepart( Index, string.Join( "|", ids ), transit );
	}

	/// <summary>
	/// One of this machine's own players boards: input off, mortality off, and — the one at this screen — the transit overlay.
	/// Every machine, for the bodies it drives: the host's <see cref="BeginTransit"/>, a client's <see cref="ApplyDepart"/>.
	/// </summary>
	void Board( NZPlayer r, int slot, float transit )
	{
		var cc = r.Components.Get<PlayerController>();
		var hp = r.Components.Get<Health>();

		var hadInput = cc.IsValid() && cc.UseInputControls;
		var wasInv = hp.IsValid() && hp.Invulnerable;

		_riding.Add( new Rider( r, hadInput, wasInv, slot ) );

		// ⛔ INPUT ONLY, NOT THE CAMERA. `UseCameraControls` stays on so a rider can still
		// look around at the overlay — freezing the view as well reads as the game hanging.
		if ( cc.IsValid() ) cc.UseInputControls = false;

		// Upstream's `TeleportNextAllowedDamage = CurTime() + 4`, which is exactly its transit
		// length. A frozen player who cannot see is not a fair target.
		if ( hp.IsValid() ) hp.Invulnerable = true;

		// ⚠️ THE OVERLAY IS LOCAL-ONLY, so it starts only when the local player is aboard.
		// Playing it for a bystander would black out someone standing beside the pad.
		if ( r == PlayerCharacters.Local() )
		{
			TeleportOverlayState.Begin( transit );
			NZSound.Play( NZSound.TeleporterCharge, Spot.A );
		}
	}

	/// <summary>The trip is over: this machine's own riders down at B, and every other machine told to do the same. HOST.</summary>
	void FinishTransit()
	{
		var n = _aboard;

		Arrive();
		NZNet.TeleporterArrive( Index );

		Log.Info( $"[nz] teleported {n} player(s) to {Spot.B}" );
	}

	/// <summary>
	/// The trip over, on this machine: its own riders put down at B and given back to themselves, the pad heard at both ends,
	/// its cooldown begun. Every machine — the host's <see cref="FinishTransit"/>, a client's <see cref="ApplyArrive"/>.
	/// </summary>
	void Arrive()
	{
		foreach ( var rider in _riding )
		{
			if ( !rider.Player.IsValid() ) continue;

			Send( rider.Player, rider.Slot );
			Restore( rider );
		}

		// ⚠️ TWO CUES, AT BOTH ENDS. One sound at the destination leaves the pad silent to
		// anyone who did not travel, and the pad going off is exactly what the rest of the team
		// needs to hear. Departure fires where the pad is, arrival where the riders land.
		NZSound.Play( NZSound.TeleporterOut, Spot.A );
		NZSound.Play( NZSound.TeleporterIn, Spot.B );
		TeleportPortal.Arrival( Spot.B );

		// ⚠️ STARTED AFTER THE TRIP COMPLETED, never at the press. A sequence that was
		// interrupted — the pad removed, the config reloaded — must not leave a cooldown behind.
		if ( Spot.Cooldown > 0f )
			_readyAt = Time.Now + Spot.Cooldown;

		_riding.Clear();
		_aboard = 0;
		_presser = null;
		_askedAt = 0f;
		_phase = Phase.Idle;
	}

	// ── the network ─────────────────────────
	//
	// ⛔ THE PAD RAN ON THE PRESSER'S MACHINE ALONE, AND NOTHING SAID SO. No message existed: in co-op nobody else saw it charge
	// or heard it, and a teammate standing on it was moved only on the presser's screen — their own machine, which drives their
	// body, left them where they stood. User: *"the teleporter should have networking"*.
	//
	// ⚠️ NOW THE HOST RUNS IT AND EVERY MACHINE PLAYS IT. The host decides — the press, who rides, when they land — and each
	// machine freezes, blacks out and moves only its OWN player (`NZNet.PlaceAt`'s rule: a body can only be moved where it is
	// driven). The warmup cue, both portals and the cooldown run on every machine from the host's word, and the pads are
	// named by their index in the config, which every machine shares (`NZNet.ConfigLoaded`).

	/// <summary>This machine's pad of that config index, or null.</summary>
	public static Teleporter ByIndex( int index )
		=> All.FirstOrDefault( t => t.IsValid() && t.Index == index );

	/// <summary>The warmup begins, here and — told by the host — on every other machine (`NZNet.TeleporterWarmup`). HOST.</summary>
	void StartWarmup( NZPlayer presser )
	{
		var seconds = MathF.Max( 0.2f, Spot.WarmupTime );

		_phase = Phase.Warmup;
		_phaseEnds = Time.Now + seconds;
		_warmupFired = false;
		_presser = presser;

		NZNet.TeleporterWarmup( Index, seconds );
	}

	/// <summary>
	/// A client pressed this pad and paid. HOST — `NZNet.TeleporterAsk`: the warmup for everyone, or — the pad busy, cooling,
	/// locked or unpowered by the time the press arrived — the points back to the presser.
	/// </summary>
	public void HostAsk( string presser, int paid )
	{
		var body = NZPlayers.BodyOf( presser );
		var blocked = body.IsValid() ? Unavailable( body ) : "Teleporter — the host has no body for that player";

		if ( string.IsNullOrEmpty( blocked ) )
		{
			StartWarmup( body );
			return;
		}

		// ⚠️ BY THE PRESSER'S ID, NOT THROUGH A BODY: one the host cannot find a body for is owed the points all the same
		if ( paid > 0 ) NZNet.AwardPoints( presser, paid );
		Log.Info( $"[nz-tp] #{Index}: a press refused — {blocked}{(paid > 0 ? $" · {paid} given back" : "")}" );
	}

	/// <summary>The host began the warmup. EVERY OTHER MACHINE — `NZNet.TeleporterWarmup`: it spins up here too, its cue at T-1.</summary>
	public void ApplyWarmup( float seconds )
	{
		_phase = Phase.Warmup;
		_phaseEnds = Time.Now + seconds;
		_warmupFired = false;
		_askedAt = 0f;
	}

	/// <summary>
	/// The host latched who rides, in the order they fan out at B. EVERY OTHER MACHINE — `NZNet.TeleporterDepart`: the pad in
	/// transit, and this machine's own player boarded if they are named.
	/// </summary>
	public void ApplyDepart( string riders, float transit )
	{
		_riding.Clear();
		_phase = Phase.Transit;
		_phaseEnds = Time.Now + transit;

		var ids = string.IsNullOrEmpty( riders ) ? Array.Empty<string>() : riders.Split( '|' );
		_aboard = ids.Length;

		var me = PlayerCharacters.Local();
		var myId = Connection.Local?.Id.ToString();
		var slot = string.IsNullOrEmpty( myId ) ? -1 : Array.IndexOf( ids, myId );

		if ( me.IsValid() && slot >= 0 ) Board( me, slot, transit );
	}

	/// <summary>The host says the trip is over. EVERY OTHER MACHINE — `NZNet.TeleporterArrive`.</summary>
	public void ApplyArrive()
	{
		// ⚠️ ONCE: a machine that already arrived on its own (`LateGrace`) has nothing left to do
		if ( _phase == Phase.Transit ) Arrive();
	}

	/// <summary>Hand one rider back their movement and their mortality.</summary>
	static void Restore( Rider rider )
	{
		if ( !rider.Player.IsValid() ) return;

		var cc = rider.Player.Components.Get<PlayerController>();
		if ( cc.IsValid() ) cc.UseInputControls = rider.HadInput;

		var hp = rider.Player.Components.Get<Health>();
		if ( hp.IsValid() ) hp.Invulnerable = rider.WasInvulnerable;
	}

	/// <summary>
	/// ⛔ THE FROZEN-FOREVER GUARD, AND IT IS THE WORST BUG THIS FEATURE COULD HAVE.
	/// TeleporterManager.Rebuild DESTROYS every pad on any config change — placing another
	/// teleporter, loading a config, starting a game. A pad destroyed mid-transit would take its
	/// riders' restore with it and leave them unable to move for the rest of the round, with
	/// nothing on screen to say why.
	/// </summary>
	protected override void OnDestroy()
	{
		foreach ( var rider in _riding )
			Restore( rider );

		if ( _riding.Count > 0 )
		{
			TeleportOverlayState.ClearPending();
			Log.Warning( $"[nz] teleporter destroyed mid-transit — released {_riding.Count} rider(s)" );
		}

		_riding.Clear();
	}

	/// <summary>
	/// Put one player down at B.
	///
	/// ⚠️ Moving the GameObject IS the teleport — PlayerController reads its position from the
	/// transform, there is no Teleport method, and Velocity is read-only because it is derived.
	/// The same note PlayerSpawner.Place carries.
	///
	/// ⚠️ THE VIEW IS AIMED TOO. The controller keeps its own eye angles, so rotating the object
	/// alone puts you in the right place looking the wrong way.
	/// </summary>
	void Send( NZPlayer rider, int i )
	{
		if ( !rider.IsValid() ) return;

		var facing = Rotation.FromYaw( Spot.Yaw );

		// ⚠️ SPREAD, so a group does not arrive inside one another. Fanned left and right of B in
		// the order they were found.
		//
		// ⛔ THE INDEX IS PASSED IN, NOT LOOKED UP. Asking Riders() again inside this method reads
		// the pad AFTER the earlier riders have already left it, so every player after the first
		// would score index 0 and they would all land on the same spot — the exact bug the spread
		// exists to prevent. It is the host's order, carried in the departure (`Rider.Slot`).
		var offset = i <= 0
			? Vector3.Zero
			: facing.Right * ((i % 2 == 1 ? 1 : -1) * ((i + 1) / 2) * 24f);

		// ⚠️ THROUGH THE ONE DOOR EVERY PLACEMENT USES, on the machine that drives the body — it aims the view and zeroes the
		// body's speed too, so a rider who boarded mid-fall does not land at fall speed (`PlayerSpawner.PlaceLocal`).
		PlayerSpawner.PlaceLocal( rider.GameObject, Spot.B + offset, facing );
	}

	/// <summary>The pad within use range of a point, or null.</summary>
	public static Teleporter Near( Vector3 pos )
	{
		Teleporter best = null;
		float bestDist = float.MaxValue;

		foreach ( var t in All )
		{
			if ( !t.IsValid() || t.Spot is null ) continue;

			var d = pos.Distance( t.Spot.A );
			if ( d > t.UseRange || d >= bestDist ) continue;

			bestDist = d;
			best = t;
		}

		return best;
	}
}